在深入 Netty 之前,我觉得有必要先对齐一下 Java NIO 的基础知识,因为 Netty 对底层网络 I/O 的操作就是基于 Java NIO 的,所以有必要了解一下。
到时候看源码,会有很多概念,例如 Channel、Selector、SelectionKey、Buffer 等等,这篇我们就来了解下这些名词到底代表着什么,分别是什么意思。
关于 Java NIO 相关的核心,总的来看包含以下三点,分别是:
翻译过来就是通道。
我们可以往通道里写数据,也可以从通道里读数据,它是双向的,而与之配套的是 Buffer,也就是你想要往一个通道里写数据,必须要将数据写到一个 Buffer 中,然后写到通道里。
从通道里读数据,必须将通道里的数据先读取到一个 Buffer 中,然后再操作。
在 NIO 中 Channel 有多种类型:
对标 Socket,我们可以直接将它当做所建立的连接。
通过 SocketChannel ,我们可以利用 TCP 协议进行读写网络数据。
可以对标 ServerSocket,也就是服务端创建的 Socket。
它的作用就是监听新建连的 TCP 连接,为新进一个连接创建对应的 SocketChannel。
之后,通过新建的 SocketChannel 就可以进行网络数据的读写,与对端交互。
可以看到它主要是用来接待新连接,这功能主要就是服务端做的,所以叫ServerSocketChannel。
看到 Datagram 应该就知道是 UDP 协议了,是无连接协议。
利用 DatagramChannel 可以直接通过 UDP 进行网络数据的读写。
文件通道,用来进行文件的数据读写。
我们日常开发主要是基于这些 TCP 协议,所以我们把精力放在 SocketChannel 和 ServerSocketChannel 上即可。
我们再回过头来继续看看 SocketChannel 和 ServerSocketChannel。
SocketChannel 主要在两个地方出现:
随后,客户端和服务端就可以通过这两个 SocketChannel 相互发送和接收数据。
ServerSocketChannel 主要出现在一个地方:服务端。
服务端需要绑定一个端口,然后监听新连接的到来,这个活儿就由 ServerSocketChannel 来干。
服务端内常常会利用一个线程,一个死循环,不断地接收新连接的到来。
ServerSocketChannel serverSocketChannel
= ServerSocketChannel.open();
......
while(true){
// 接收的新连接
SocketChannel socketChannel =
serverSocketChannel.accept();
.......
}
至此,想必你应该清楚 ServerSocketChannel 和 SocketChannel 的区别和作用了。
Buffer 说白了就是内存中可以读写的一块地方,叫缓冲区,用于缓存数据。
其实还真没啥好说的,最多就是讲讲 Java NIO Buffer 的 API。
但讲 API 得太死板了,所以自己上网搜搜吧。我就告知一个结论,这个 API 很不好用,稍微漏写了点,就容易出错 bug,而且还有很多优化的之处,所以 Netty 没用 Java NIO Buffer 而是自己实现了一个 Buffer,叫 ByteBuf。
等我们之后分析 ByteBuf 的时候再来盘一盘。现在你只需要知道 Buffer 主要用来缓存通道的读写数据即可。
对了,看到这可能会有人提出疑问,为什么 Channel 必须和 Buffer 搭配使用?
其实网络数据是面向字节的,但是我们读写的数据往往是多字节的,假设不用 Buffer ,那我们就得一个字节一个字节的调用读和调用写,想想是不是很麻烦?
所以我们搞个 Buffer,把数据拢一拢,这样之后的调用才能更好地处理完整的数据,方便异步的处理等等。
I/O多路复用的核心玩意。
一个 Selector 上可以注册多个 Channel ,我们从上面得知一个消息 Channel 就对应了一个连接,因此一个 Selector 可以管理多个 Channel 。
具体管理什么?
当任意 Channel 发生读写事件的时候,通过 Selector.select() 就可以捕捉到事件的发生,因此我们利用一个线程,死循环的调用 Selector.select(),这样可以利用一个线程管理多个连接,减少了线程数,减少了线程的上下文切换和节省了线程资源。
这就是 Selector 的核心功能,然后我们再来细说具体是怎样管理的。
首先,创建一个 Selector。
Selector selector = Selector.open();
然后,你需要将被管理的 Channel 注册到 Selector 上,并声明感兴趣的事件。
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
事件一共有以上四种类型,注册的时候可以同时对多种类型的事件感兴趣,例如:
SelectionKey key
= channel.register(selector,
Selectionkey.OP_READ | SelectionKey.OP_WRITE);
这样,当这个 Channel 发生读或写事件,我们调用 Selector.select()就可以得知有事件发生。
具体 Selector.select() 有三个重载方法:
返回值就是就绪的通道数,一般判断大于 0 即可进行后续的操作。
后续的操作就是调用:
Set selectedKeys = selector.selectedKeys();
获得了一个类型为 Set 的 selectedKeys 集合,那这个 selectedKeys 又是啥玩意?
我们来看一下它的方法和成员:
看到这些成员,其实我们就很清晰了,我们可以通过 selectedKey 得知当前发生的是什么事件,有 isAcceptable、isReadable 等等。
然后还能获得对应的 channel 进行相应的读写操作,还有获取 attachment 等等。
所以得到了 selectedKeys 就可以通过迭代器遍历所有发生事件的连接,然后进行操作。
大致使用的代码如下所示:
while(true) {
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove(); //执行完毕之后,需要在循环内移除自己
}
}
还有个方法就是 Selector.wakeup(),可以唤醒阻塞着的 Selector。
对了还有一点没说,就是如果 Channel 要和 Selector 搭配,那它必须得是非阻塞的,即配置
channel.configureBlocking(false);
从上面的操作,我们可以得知 Selector 处理事件的时候必须快,如果长时间处理某个事件,那么注册到 Selector 上的其他连接的事件就不会被及时处理,造成客户端阻塞。
至此,想必你应该清晰 Selector 具体是如何管理这么多连接的了。
参考:
页面更新:2024-05-09
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号