NIO基础-Netty系列-1
概览
最近弄几篇NIO基础相关的内容,用于Netty源码解析使用。因为没有这些知识就产生不了问题,也就无法深入一个成熟的网络IO框架源码进行学习。
NIO三大核心组件:
1,Channel
2,Buffer
3,Selector
先概述一下三者的概念和之间的关系,再逐个了解组件的API打个基础。
对于IO通信,必然需要连接起来才能通信,Channel可以理解成一个连接连端的通道,有了通道就可以读写数据了,Buffer是和Channel交互的读写数据的组件,读操作就是从Channel中的数据读到Buffer上,写就是把数据从Buffer上写到Channel上。在通信的流程中,会有很多个通道产生IO事件需要处理,Selector可以理解成由它来监听全部的通道上发生的事件,然后我们通过Selector可以获得我们感兴趣的IO事件,然后进行不同的操作。
Channel
这里就涉及两个Channel类型:
-
SocketServerChannel
监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道。
-
SocketChannel
用于Socket套接字TCP连接的数据读写。
先进行连接,然后读写数据,所以对于被连接的一端,是用SocketServerChannel来监听连接请求的,监听到后就创建好SocketChannel,我们可以对SocketChannel进行监听,进行读写操作。这个流程在实现代码中体现,可以先记住这个意思就行。
接下去对关键的API进行解释:
ServerSocketChannel#open
开启一个服务端的Socket Channel。
ServerSocketChannel#bind(SocketAddress)
Server端作为接受连接的一方,怎么说也要先绑定一个端口吧,否则人家想连接你都找不到地址。所以执行open开启一个Channel后一般就执行bind绑定到一个本地地址(SocketAddress)。这样客户端往这个地址连接的时候,服务端的Channel才能接受到。
AbstractSelectableChannel#configureBlocking
这个方法由SocketServerChannel
和SocketChannel
的抽象父类提供,用于设置Channel是阻塞还是非阻塞的。这个设置关系到channel的一些方法是否阻塞的特性,在后面的会被涉及。
ServerSocketChannel#accept
开启一个Channel,绑定好了地址,如果有客户端来连了,就用这个方法来接受并返回一个SocketChannel
,和前面概述的内容对应到了,非常的顺利成章,这个方法就涉及前面设置的是否阻塞,如果设置的阻塞,那么这个方法就会阻塞直到有客户端来连接,如果设置的是非阻塞,那么就看此时有没有新连接,如果有就返回一个SocketChannel
,否则返回null。
SelectableChannel#register(Selector, int)
Selector前面介绍过是用来监听Channel的,作为被监听者,需要有一个注册的动作,并且在注册的时候告诉监听者监听什么内容,这个内容在SelectionKey中列举,一共也就四个:
- OP_READ 读就绪
- OP_WRITE 写就绪
- OP_CONNECT 连接操作
- OP_ACCEPT 接受连接
需要理解这些Key表示的是一种就绪的事件,不是事件本身,比如我们监听OP_ACCEPT,那么在后续询问Selector的时候,Selector告诉我们的答案是有一个连接已经过来可以接受这个连接了,所以接下去要做的是去接受这个连接。
SocketChannel#read(ByteBuffer)
从SocketChannel读取数据到ByteBuffer。
SocketChannel.connect(SocketAddress remote)
向远程地址发起连接。作为客户端连接服务端使用。
SocketChannel.finishConnect()
判断前面connect方法是否结束。
Buffer
JDK实现了各种类型的Buffer:
IntBuffer, CharBuffer, FloatBuffer, DoubleBuffer, ShortBuffer, LongBuffer, ByteBuffer
重点关注ByteBuffer就可以了,我们知道它是和Chennel交互数据的,可以稍微了解一下它的结构:
内部有一个byte[]
的数据块,读写数据就是操作这个数组,初始化的时候会确定一个容量,用capacity
来标识,然后使用position
来表示目前读写的位置,比如在读的时候从0开始递增。还有一个limit
字段表示读写的最大上限。Buffer有读写模式,会有切换模式的操作,切换模式后position
和limit
定义是调整的,所以值也会调整。
后面将仔细分析Buffer的实现原理。
ByteBuffer#allocate
分配一个新的字节缓冲区,初始化操作
ByteBuffer#put(byte)
put方法用于写数据,position会随着写入的数据递增。
ByteBuffer#get()
get方法用于从Buffer获取数据
Buffer#flip
反转操作,也就是切换模式,具体操作是把limit设置成当前的position,position设置成0。
Buffer#clear
清理操作,position设置成0,limit设置成capacity。
Selector
Selector 用于监控Channel上的就绪事件,这个能力是实现多路复用的IO模型的关键。我们可以在Channel通过register方法注册到Selector上的时候传入感兴趣的就绪事件,然后通过select方法检测是否有就绪事件。
Selector#select
选择出就绪事件,返回数量,大于0表示有就绪事件。无参数的方法是阻塞一直到可以返回数量或者被weakup或线程被中断,我们可以通过重载的方法(select(long timeout)
)中参数调整这个方法的阻塞时间,也可以选择selectNow()
执行一次选择操作,立即返回。
Selector#wakeup
效果就是把前面select方法阻塞还未返回的操作唤醒立即返回。如果此时没有调用select的那么下次的调用立即返回。
Selector#selectedKeys
获得事件就绪的SelectionKey类型集合,我们可以通过SelectionKey获得事件就绪的Channel,然后就可以对这些Channel进行相应的读写等操作。
SelectionKey#interestOps(int)
设置监听的事件,这个方法可以调整对应的SocketChannel监听的感兴趣事件。
SelectionKey#interestOps()
获取SelectionKey的就绪事件兴趣集,一般先调用这个方法在调用前面的方法调整。
SelectionKey#attach(Object ob)
绑定一个对象
SelectionKey#attachment()
返回前面绑定的对象
入门代码
先代码入个门,我们已经了解了一些NIO的核心组件的关键Api,那么我们为了实现一个网络通信的功能,应该组装起这些API呢?先实现一个监听端口,能够获得连接,并且从连接读取数据这样一个基本的功能代码。
服务端代码:
public class NioServer {
public static void start() throws IOException {
// 开启选择器
Selector selector = Selector.open();
// 开启server socket channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置成非阻塞模式
serverSocketChannel.configureBlocking(false);
// server 监听端口绑定
serverSocketChannel.bind(new InetSocketAddress(12022));
// channel 注册到选择器上,IO事件为Accept
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// select 操作 阻塞等待Accept状态就绪
while (selector.select() > 0) {
// 获取全部就绪的selectedKeys
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
// 遍历selectedKeys
while (selectionKeys.hasNext()) {
SelectionKey selectionKey = selectionKeys.next();
// 就绪状态为Accept
if (selectionKey.isAcceptable()) {
// 接受一个连接 得到一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
// 设置成非阻塞模式
socketChannel.configureBlocking(false);
// 把SocketChannel 注册到Selector 感兴趣的事件是读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 分配一个新的字节缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 从Channel中读取数据到Buffer
while ((socketChannel.read(byteBuffer)) > 0) {
// 翻转Buffer
byteBuffer.flip();
// 清理Buffer
byteBuffer.clear();
}
String readStr = new String(byteBuffer.array());
System.out.print("" + readStr);
socketChannel.close();
}
selectionKeys.remove();
}
// serverSocketChannel.close();
}
}
public static void main(String[] args) throws IOException {
start();
}
客户端代码:
public static void start() throws IOException {
InetSocketAddress inetSocketAddress = new InetSocketAddress(12022);
// 开启一个Socket Channel 并且连接远程地址
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞
socketChannel.configureBlocking(false);
// 连接远程地址
socketChannel.connect(inetSocketAddress);
// 等待连接成功
while (!socketChannel.finishConnect()) {
}
// 分配Buffer空间
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 写入Buffer
byteBuffer.put("hello world".getBytes());
// 切换成读模式
byteBuffer.flip();
// 把Buffer 写入Socket Channel
socketChannel.write(byteBuffer);
// 关闭写连接
socketChannel.shutdownOutput();
// 关闭socket Channel
socketChannel.close();
}
public static void main(String[] args) throws IOException {
start();
}
参考: