设为首页 加入收藏

TOP

Netty(1)——NIO基础(二)
2023-07-25 21:42:04 】 浏览:60
Tags:Netty NIO 基础
  1. 通过源码可以看出,调用ByteBuffer.allocate(10)的时候,我们初始化了一个HeapByteBuffer对象,并将其capacitylimit均设置为10,position被设置为0。
点击查看代码
// ByteBuffer.java
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}
// HeapByteBuffer.java,调用的HeapByteBuffer中的构造方法
HeapByteBuffer(int cap, int lim) {            // package-private
    super(-1, 0, lim, cap, new byte[cap], 0);
}

image

  1. 当调用channel.read(buf)向Buffer中写入数据时,根据源码分析,其最终会调入ByteBufferput()方法中,HeapByteBuffer对其的实现如下,nextPutIndex()方法检查当前position是否大于等于limit,如果小于limit,则将原position返回,并将原position加1。
点击查看代码
// HeapByteBuffer.java
public ByteBuffer put(byte x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}

image

  1. 当写入完成后,我们调用flip()方法,所谓将Buffer切换为读模式,其实源码中就是将position和limit的位置重新赋值。如此操作后,position就是我们读取数据的起点,limit就是我们读取数据的终点。
点击查看代码
// Buffer.java
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

image

  1. 切换读模式后,就可以调用buffer.get()方法来获取一个字节,通过源码可以看出,nextGetIndex()方法检查当前position是否大于等于limit,如果小于limit,则将原位置返回,并将原位置加1。
点击查看代码
// HeapByteBuffer.java
public byte get() {
    return hb[ix(nextGetIndex())];
}

image

  1. 当读取所有数据后,可以调用buffer.clear()方法或buffer.compact()方法将Buffer切换为写模式。

    • clear(): 直接将Buffer重置为初始状态,忽略还没有读完的数据。
    • compact():将还没读完的数据复制到缓冲区头部,然后从没读完的数据后可以开始写入新的数据
点击查看代码
// Buffer.java
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

// HeapByteBuffer.java
public ByteBuffer compact() {
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());  // 将还没读完的数据拷贝到数组头部
    position(remaining());  // 将position重置为剩余待读数据之后
    limit(capacity());  // 将limit重置为capacity
    discardMark();
    return this;
}

image

mark和reset

mark是Buffer中的另一个属性,它的主要用途是记录一个position的位置,后续调用reset()方法后会将position重置到mark的位置。mark可以不被定义,但如果设置了mark的值,则它不能为负值且不能大于position的值。

粘包和半包

比如我们想要发送三行数据

Hello world.\n
It is my life.\n
I love you.\n

为了提高发送效率,通常我们会将这三行字符串合并到一个Buffer中进行发送。另一端在接收到消息时,由于协议并不理解消息的内容,因此用户在读取数据时,有可能读取出来如下两个包。

Hello world.\nIt is my life.\nI Lo
ve you.\n

这里第一个包出现了原来的两条数据在一个包中的情况,这就叫做粘包。第一行最后的数据将原来的一条数据截断了,这就叫做半包。

我们可以通过如下方式处理粘包和半包的问题。

点击查看代码
public class TestStickyAndHalfPackage {
    public static void main(String[] args) {
        ByteBuffer p1 = ByteBuffer.allocate(64);
        p1.put("Hello world.\nIt is my life.\nI lo".getBytes());
        split(p1);
        p1.put("ve you.\n".getBytes());
        split(p1);
    }

    private static void split(ByteBuffer buffer) {
        buffer.flip();
        for (int i = 0; i < buffer.limit(); ++i) {
            if (buffer.get(i) == '\n') {
                int len = i + 1 - buffer.position();
                ByteBuffer line = ByteBuffer.allocate(len);
                for (int j = 0; j < len; ++j) {
                    line.put(buffer.get());
                }
                line.flip();
                System.out.print(StandardCharsets.UTF_8.decode(line));
            }
        }
        buffer.compact();  // 将没有读完的数据移动到buffer头部,这里是处理半包和粘包的关键
    }
}

Selector(选择器)

概念继承于操作系统IO模型中多路复用IO模型中的selector,其主要作用是,用户可以把所有读写Channel都注册在某个Selector上,Selector会不断的轮询注册在上面的所有channel,如果某个channel为读写等事件做好准备,那么就处于就绪状态,通过Selector可以不断轮询发现出就绪的channel,进行后续的IO操作。为何要做这种设计呢?

如果每一个Channel都需要一个线程来为其IO过程提供服务,则会占用大量的内存,CPU需要在很多线程间进行切换,有太多额外开销,而且随着连接数量增加,线程数量会达到上限,无法支持大连接数。

有一种解决方案是使用有固定线程数量的线程池来处理所有连接请求,但线程池中的线程一旦被占用,就要阻塞等待IO完成才能被其他连接使用,如果IO请求花费时间很长,那会导致后续的大量IO请求需要排队等待。这种

首页 上一页 1 2 3 下一页 尾页 2/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇day01-Spring基本介绍 下一篇通过Terraform创建GCP Pubsub

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目