- Buffer缓冲区
- Channel通道
- Selector选择器
1 Buffer缓冲区
本质是一个可以写入数据的内存块(类似于数组),可以更轻松地使用内存块,相对于直接对数组操作,buffer api更加容易操作和管理。
使用Buffer进行数据写入与读取,需要进行四个步骤:
- 将数据写入缓冲区
- 调用buffer.flip(),转换为读取模式
- 缓冲区读取数据
- 调用buffer.clear()或buffer.compact()清除缓冲区
在BIO网络模型中,数据的操作都是基于Stream对象,而输入输出流都是阻塞的;在NIO网络模型中,所有对于数据的操作,都是在缓冲区进行的,常用的缓冲区有ByteBuffer,其它的基本数据类型也有对应的缓冲区(除了bool类型)。
Buffer的三个属性:
- Capacity:缓冲区的内存大小
- Position:当前读写的位置
- Limit:读模式下为写入的位置,写模式下为Capacity值
ByteBuffer提供比其他类型缓冲区更特有的一些操作,是最常用的缓冲区。
2 Channel通道
分为两大类:
- 用于网络读写的SelectableChannel
- 用于文件操作的FileChannel
SocketChannel和ServerSocketChannel为SelectableChannel的子类;
Channel是全双工模式的,Stream只能在一个方向上。
2.1 SocketChannel
SocketChannel用于建立TCP网络连接,类似java.net.Socket。有两种创建SocketChannel形式:
- 客户端主动发起和服务器的连接
- 服务端获取的新连接
// 客户端主动发起连接的方式
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); //设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("http://163.com", 80));
channel.write(byteBuffer); // 发送请求数据 - 向通道写入数据
int bytesRead = socketChannel.read(byteBuffer); //读写服务端返回 - 读取缓冲区的数据
socketChannel.close(); // 关闭连接
write():在尚未写入任何内容时就可能返回了,需要在循环中调用write()。
read():可能直接返回而根本不读取任何数据,根据返回的int值判断读取了多少字节。
2.2 ServerSocketChannel
// 创建网络服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 绑定端口
while(true){
SocketChannel socketChannel = serverSocketChannel.accept(); // 获取新tcp连接通道
if(socketChannel!=null){
// tcp请求读取/相应
}
}
serverSocketChannel.accept():如果该通道处于非阻塞模式,那么如果没有挂起的连接,该方法将立即返回null,必须检查返回的SocketChannel是否为null。
3 Selector选择器
不断轮询注册在Selector上的Channel,可以检查一个或多个Channel,只需要一个线程负责Selector的轮询,就可以处理成千上万的请求。
一个线程使用Selector监听多个Channel的不同事件:
- Connect连接(SelectionKey.OP_CONNECT)
- Accept准备就绪(OP_ACCEPT)
- Read读取(OP_READ)
- Write写入(OP_WRITE)
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectorKey key = channel.register(selector, SelectionKey.OP_READ); // 注册感兴趣的事件
while(true){
int readyChannels = selector.select(); // select收到新的事件,方法才会返回
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
// 判断不同的事件类型,执行对应的逻辑处理
// key.isAcceptable()
// key.isConnectable()
// key.isReadable()
// key.isWritable()
// keyIterator.remove()
}
}
4 NIO服务端处理流程
- 打开serverSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道;
- 绑定监听的端口,设置连接为非阻塞模式;
- 创建reactor线程,创建多路复用器并启动线程;
- 将serverSocketChannel注册到reactor线程的多路复用器上,监听accept事件;
- selector在run方法的无限循环中轮询准备就绪的key,selector通过selectionKey可以获取就绪channel的集合;
- selector监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路;
- 设置客户端链路为非阻塞模式;
- 将新接入的客户端连接注册到reactor线程的多路复用器上,监听读操作,用来读取客户端发送的网络消息;
- 异步读取客户端的请求到缓冲区;
- 对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将编解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排;
- 将pojo对象encode成ByteBuffer,调用socketChannel的异步write接口,将消息异步发送到客户端(注意:如果发送区TCP缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入TCP缓存区)
参考文章
结语
本人所有博客仅用于学习记录,不做任何商业用途,如涉及侵权,还请联系删除,感谢阅读,欢迎留言,一起进步~