Java - NIO

Buffer

Buffer结构

像数组,有以下重要的属性

  • capacity,容量
  • position,读写指针,标记读到哪或者写到哪
  • limit,读取、写入的长度的限制,即能读取多少字节,能写入多少字节。
    一开始创建数组为空
    image
    刚开始是写模式
    image
    想要读取时,切换到读模式(flip),注意指针的变化
    image
    每调用一次ByteBuffer的get()方法,position指针右移一次,直到limit指针
    image
    读取完后,调用clear方法,清空,切换到写模式
    image
    下面的情况是,读取了两个字节后,由于某些原因,没有继续往下读取,position指针指到c,调用Compact方法,c、d数据左移覆盖,原c、d不变,相当于拷贝c、d到a、b的位置,指针指到绿色d后面的位置
    image

演示

image

public class TestBuffer2 {

    public static void main(String[] args) {
        ByteBuffer buffer=ByteBuffer.allocate(10);
        //以十六进制形式写入,0x61对应十进制97,写入的是a
        buffer.put((byte) 0x61);
        ByteBufferUtil.debugAll(buffer);
    }
}

接着演示

public class TestBuffer2 {

    public static void main(String[] args) {
        ByteBuffer buffer=ByteBuffer.allocate(10);
        //以十六进制形式写入,0x61对应十进制97,写入的是a
        buffer.put((byte) 0x61);
        ByteBufferUtil.debugAll(buffer);
        buffer.put(new byte[]{0x62,0x63,0x64});//b  c  d
        ByteBufferUtil.debugAll(buffer);
    }
}

结果如下:
image

调用get方法

指针指在4,没数据,只有默认的00
image
需要切换到读取模式

public class TestBuffer2 {

    public static void main(String[] args) {
        ByteBuffer buffer=ByteBuffer.allocate(10);
        //以十六进制形式写入,0x61对应十进制97,写入的是a
        buffer.put((byte) 0x61);
        ByteBufferUtil.debugAll(buffer);
        buffer.put(new byte[]{0x62,0x63,0x64});//b  c  d
        ByteBufferUtil.debugAll(buffer);
        //System.out.println(buffer.get());
        //切换到读取模式,position指针指到0
        buffer.flip();
        System.out.println(buffer.get());
        ByteBufferUtil.debugAll(buffer);
    }
}

结果如下,读完后position指针指到1
image
调用buffer.compact(),1、2、3位置的数据整体左移覆盖,注意,3位置还是64,指针指到3,下次写入时会在3位置开始写。
image
接着执行

buffer.put(new byte[]{0x65,0x6f});

结果如下:
image

配合channel使用

image

@Slf4j(topic = "c.test")
public class TestBuffer {
    public static void main(String[] args) throws FileNotFoundException {
        //获取channel
        try (FileChannel channel = new FileInputStream("D:\\javadev\\temp\\g.txt").getChannel()) {
            //准备缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(10);
            //读取
            channel.read(buffer);
            //切换读模式
            buffer.flip();
            while (buffer.hasRemaining()){//是否还有剩余的数据
                byte b = buffer.get();
                System.out.println((char) b);
            }
        } catch (IOException e) {

        }
    }
}

输出如下,因为缓冲区的大小为10,最多读取10个字节

1
2
3
4
5
6
7
8
9
0

读完全部的内容
image

ByteBuffer 的实现方式

  • 堆内缓冲区(HeapByteBuffer)‌:底层直接使用 byte[] 数组存储数据。
  • 直接缓冲区(DirectByteBuffer)‌:底层使用堆外内存(通过 Unsafe 或 malloc 分配),并不直接依赖 byte[]。通过地址指针操作内存。
  • 通过内存分配方式来确定要哪种

内存分配方式

        //堆内存分配
        ByteBuffer buffer=ByteBuffer.allocate(10);
        //直接内存分配
        ByteBuffer buffer2=ByteBuffer.allocateDirect(10);

写入、读取

写入

image

读取

image

重复读取

image

示例
public class TestBuffer4 {

    public static void main(String[] args) {
        ByteBuffer buffer =ByteBuffer.allocate(10);
        buffer.put(new byte[]{'a','b','c','d'});
        //切换到读
        buffer.flip();
        //读取4个字节到新建的字节数组
        buffer.get(new byte[4]);
        ByteBufferUtil.debugAll(buffer);
        //position重置为0
        buffer.rewind();
        ByteBufferUtil.debugAll(buffer);
        System.out.println((char)buffer.get());
    }
}

image

标记读取

image

public class TestBuffer5 {

    public static void main(String[] args) {
        ByteBuffer buffer =ByteBuffer.allocate(10);
        buffer.put(new byte[]{'a','b','c','d'});
        //切换到读
        buffer.flip();
        //读了两次,指针到2
        System.out.println((char) buffer.get());
        System.out.println((char) buffer.get());
        //将2记下来
        buffer.mark();
        //读了两次,指针到4
        System.out.println((char) buffer.get());
        System.out.println((char) buffer.get());
        //指针恢复到2
        buffer.reset();
        //又再次读
        System.out.println((char) buffer.get());
        System.out.println((char) buffer.get());
    }
}

输出如下:

a
b
c
d
c
d
  • get(int i),指定位置读取,不会改变position指针位置

写入、读取字符串

方式一

image
image

方式二

image
注意position,自动切换到读模式
image
解码
image
image

方式三

image
image

分散式读取

words.txt如下
image
分别读one到b1、two到b2、three到b3
image
b1结果
image
b2结果
image
b3结果
image

集中式写入

image

黏包与半包

  • 黏包:发送方法了3条数据,每条结尾有\n,网络传输时,合并为一条
  • 半包:接收方接收时,可能因为缓冲区的限制,把前面中间放到一个缓冲区,然后读取完后,把后面的又放到缓冲区
    image
解决方法

image

Selector

非阻塞

ServerSocketChannel默认阻塞模式,调用accept方法,会阻塞,等待客户端连接。非阻塞下,调用accept方法,不会阻塞,方法直接返回null

演示
@Slf4j(topic = "c.test")
public class Server1 {
    public static void main(String[] args) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(16);
        ServerSocketChannel ssc=  ServerSocketChannel.open();
        //设通道为非阻塞模式
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        List<SocketChannel> channels=new ArrayList<>();
        while (true){
            log.debug("connecting");
            //非阻塞模式下获取到的对象是null
            SocketChannel sc =ssc.accept();
            log.debug("connected...{}",sc);
        }
    }
}

不断输出:

16:31:30.770 [main] DEBUG c.test - connecting
16:31:30.770 [main] DEBUG c.test - connected...null
16:31:30.770 [main] DEBUG c.test - connecting
16:31:30.770 [main] DEBUG c.test - connected...null
16:31:30.770 [main] DEBUG c.test - connecting
16:31:30.770 [main] DEBUG c.test - connected...null
16:31:30.770 [main] DEBUG c.test - connecting
16:31:30.770 [main] DEBUG c.test - connected...null
16:31:30.770 [main] DEBUG c.test - connecting
16:31:30.770 [main] DEBUG c.test - connected...null
16:31:30.770 [main] DEBUG c.test - connecting
16:31:30.770 [main] DEBUG c.test - connected...null
16:31:30.770 [main] DEBUG c.test - connecting
16:31:30.770 [main] DEBUG c.test - connected...null
16:31:30.770 [main] DEBUG c.test - connecting
执行完下面的代码后,程序创建一个集合,和一个SelectionKey对象59a6e353,对象关注accept事件,关联通道ssc(ServerSocketChannel),对象放进集合里
SelectionKey sscKey = ssc.register(selector, 0, null);  
sscKey.interestOps(SelectionKey.OP_ACCEPT);

image

接着执行到selector.select(),等待通道关注的事件(accept)发生,有事件发生后(客户端连接),创建另一个集合selectedKeys(绿色的),将发生事件的通道的对象SelectionKey放进去。绿色集合里的ssckey等待处理。
这行代码就是准备遍历处理,这个集合里的就是发生的所关注的事件的通道的对象,Iterator iter = selector.selectedKeys().iterator();

image

下面代码,取出该通道
ServerSocketChannel channel =(ServerSocketChannel) key.channel();
处理事件,SocketChannel即客户端的socket连接。
SocketChannel sc = channel.accept();
或者是另一种处理,取消连接,key.cancel();

image

处理之后accept标记没了
image
如果没有处理,accept还在,那么下次循环时,会不断地进入这个循环
image

accept连接,将SocketChannel注册进selector,设置关注的事件为读事件,selector集合内就注册了两个通道

image
image

接着selector.select(),客户端发送消息,往下运行,selector.selectedKeys().iterator(),绿色集合内有两个,遍历对象,假设遍历第一个59a6e353,这个对象就是刚刚那个监听8080端口的通道对象,执行到ServerSocketChannel channel =(ServerSocketChannel) key.channel(),取出了该对象,执行channel.accept(),因为设置ServerSocketChannel为非阻塞,所以返回了null,后续就会空指针异常

image

ServerSocketChannel channel =(ServerSocketChannel) key.channel();
//返回的sc是null
SocketChannel sc = channel.accept();
因此一定要加上iter.remove();

image
image

客户端断开连接的处理

异常断开
客户端代码如下,连接后,主线程结束,socketChannel通道关闭,异常断开,会向客户端发送read事件
public class Client12 {
    public static void main(String[] args) throws InterruptedException, IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("127.0.0.1",8080));
    }
}
服务端代码执行到下面会报错,因为客户端的通道已关。下面虽然捕获了异常,但是事件未处理,又会进入循环报错。

image

加上,key.cancel(),取消事件

image

正常断开

image
image

消息边界

问题演示

缓冲区设置为4字节,
image
image
utf-8编码方式,你好,6个字节,缓冲区4字节,因此发了两次read事件,读了两次。
image

解决方案

image

可写事件

小结

image

多线程优化NIO

设计大概

黄色框是客户端请求,boss线程负责accept事件,两个worker线程负责读写事件。
image

线程问题分析

image
image
输出:

11:35:10.065 [worker-0] c.test - before selector.select
11:35:13.566 [Boss] c.test - before register..../127.0.0.1:1512
主线程调用worker.register()启动worker线程,
主线程要调用sc.register(worker.selector,SelectionKey.OP_READ,null)方法,
worker线程里的selector对象和worker.selector是同一对象。
worker线程调用selector.select(),会卡住sc.register(worker.selector,SelectionKey.OP_READ,null),
因为selector.select()会阻塞等待,然后sc.register方法就会卡住,该方法原理就是这样。
posted @ 2026-02-16 18:11  dvdhellohaha  阅读(13)  评论(0)    收藏  举报