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]()
演示

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);
}
}
结果如下:

调用get方法
指针指在4,没数据,只有默认的00

需要切换到读取模式
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

调用buffer.compact(),1、2、3位置的数据整体左移覆盖,注意,3位置还是64,指针指到3,下次写入时会在3位置开始写。

接着执行
buffer.put(new byte[]{0x65,0x6f});
结果如下:

配合channel使用

@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
读完全部的内容

ByteBuffer 的实现方式
- 堆内缓冲区(HeapByteBuffer):底层直接使用 byte[] 数组存储数据。
- 直接缓冲区(DirectByteBuffer):底层使用堆外内存(通过 Unsafe 或 malloc 分配),并不直接依赖 byte[]。通过地址指针操作内存。
- 通过内存分配方式来确定要哪种
内存分配方式
//堆内存分配
ByteBuffer buffer=ByteBuffer.allocate(10);
//直接内存分配
ByteBuffer buffer2=ByteBuffer.allocateDirect(10);
写入、读取
写入

读取

重复读取

示例
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());
}
}

标记读取

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指针位置
写入、读取字符串
方式一


方式二

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

解码


方式三


分散式读取
words.txt如下

分别读one到b1、two到b2、three到b3

b1结果

b2结果

b3结果

集中式写入

黏包与半包
- 黏包:发送方法了3条数据,每条结尾有\n,网络传输时,合并为一条
- 半包:接收方接收时,可能因为缓冲区的限制,把前面中间放到一个缓冲区,然后读取完后,把后面的又放到缓冲区
![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);

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

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

处理之后accept标记没了

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

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


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

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


客户端断开连接的处理
异常断开
客户端代码如下,连接后,主线程结束,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));
}
}
服务端代码执行到下面会报错,因为客户端的通道已关。下面虽然捕获了异常,但是事件未处理,又会进入循环报错。

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

正常断开


消息边界
问题演示
缓冲区设置为4字节,


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

解决方案

可写事件
小结

多线程优化NIO
设计大概
黄色框是客户端请求,boss线程负责accept事件,两个worker线程负责读写事件。

线程问题分析


输出:
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方法就会卡住,该方法原理就是这样。







浙公网安备 33010602011771号