Netty 基石,Java NIO 核心知识

时间:2022-03-15 11:16:03

Netty 基石,Java NIO 核心知识

本文转载自微信公众号「yes的练级攻略」,作者是Yes呀 。转载本文请联系yes的练级攻略公众号。

你好,我是yes。

在深入 Netty 之前,我觉得有必要先对齐一下 Java NIO 的基础知识,因为 Netty 对底层网络 I/O 的操作就是基于 Java NIO 的,所以有必要了解一下。

到时候看源码,会有很多概念,例如 Channel、Selector、SelectionKey、Buffer 等等,这篇我们就来了解下这些名词到底代表着什么,分别是什么意思。

关于 Java NIO 相关的核心,总的来看包含以下三点,分别是:

  • Channel
  • Buffer
  • Selector

什么是 Channel

翻译过来就是通道。

我们可以往通道里写数据,也可以从通道里读数据,它是双向的,而与之配套的是 Buffer,也就是你想要往一个通道里写数据,必须要将数据写到一个 Buffer 中,然后写到通道里。

从通道里读数据,必须将通道的数据先读取到一个 Buffer 中,然后再操作。

在 NIO 中 Channel 有多种类型:

  • SocketChannel
  • ServerSocketChannel
  • DatagramChannel
  • FileChannel
  • SocketChannel

对标 Socket,我们可以直接将它当做所建立的连接。

通过 SocketChannel ,我们可以利用 TCP 协议进行读写网络数据。

ServerSocketChannel

可以对标 ServerSocket,也就是服务端创建的 Socket。

它的作用就是监听新建连的 TCP 连接,为新进一个连接创建对应的 SocketChannel。

之后,通过新建的 SocketChannel 就可以进行网络数据的读写,与对端交互。

可以看到它主要是用来接待新连接,这功能主要就是服务端做的,所以叫 ServerSocketChannel。

DatagramChannel

看到 Datagram 应该就知道是 UDP 协议了,是无连接协议。

利用 DatagramChannel 可以直接通过 UDP 进行网络数据的读写。

FileChannel

文件通道,用来进行文件的数据读写。

我们日常开发主要是基于 TCP 协议,所以我们把精力放在 SocketChannel 和 ServerSocketChannel 上即可。

我们再回过头来继续看看 SocketChannel 和 ServerSocketChannel。

SocketChannel 主要在两个地方出现:

  • 客户端,客户端创建一个 SocketChannel 用于连接至远程的服务端。
  • 服务端,服务端利用 ServerSocketChannel 接收新连接之后,为其创建一个 SocketChannel 。

随后,客户端和服务端就可以通过这两个 SocketChannel 相互发送和接收数据。

ServerSocketChannel 主要出现在一个地方:服务端。

服务端需要绑定一个端口,然后监听新连接的到来,这个活儿就由 ServerSocketChannel 来干。

服务端内常常会利用一个线程,一个死循环,不断地接收新连接的到来。

  1. ServerSocketChannel serverSocketChannel
  2. = ServerSocketChannel.open();
  3. ......
  4. while(true){
  5. // 接收的新连接
  6. SocketChannel socketChannel =
  7. serverSocketChannel.accept();
  8. .......
  9. }

至此,想必你应该清楚 ServerSocketChannel 和 SocketChannel 的区别和作用了。

Buffer

Buffer 说白了就是内存中可以读写的一块地方,叫缓冲区,用于缓存数据。

其实还真没啥好说的,最多就讲讲 Java NIO Buffer 的 API。

但讲 API 的太死板了,所以自己上网搜搜吧。我就告知一个结论,这个 API 很不好用,稍微漏写了点,就容易出 bug,而且还有很多优化的之处,所以 Netty 没用 Java NIO Buffer 而是自己实现了一个 Buffer,叫 ByteBuf。

等我们之后分析 ByteBuf 的时候再来盘一盘。现在你只需要知道 Buffer 主要用来缓存通道的读写数据即可。

对了,看到这可能会有人提出疑问,为什么 Channel 必须和 Buffer 搭配使用?

其实网络数据是面向字节的,但是我们读写的数据往往是多字节的,假设不用 Buffer ,那我们就得一个字节一个字节的调用读和调用写,想想是不是很麻烦?

所以我们搞个 Buffer,把数据拢一拢,这样之后的调用才能更好地处理完整的数据,方便异步的处理等等。

Selector

I/O多路复用的核心玩意。

一个 Selector 上可以注册多个 Channel ,我们从上面得知一个 Channel 就对应了一个连接,因此一个 Selector 可以管理多个 Channel 。

具体管理什么?

当任意 Channel 发生读写事件的时候,通过 Selector.select() 就可以捕捉到事件的发生,因此我们利用一个线程,死循环的调用 Selector.select(),这样可以利用一个线程管理多个连接,减少了线程数,减少了线程的上下文切换和节省了线程资源。

这就是 Selector 的核心功能,然后我们再来细说具体是怎样管理的。

首先,创建一个 Selector。

  1. Selector selector = Selector.open();

然后,你需要将被管理的 Channel 注册到 Selector 上,并声明感兴趣的事件。

  1. SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

Netty 基石,Java NIO 核心知识

事件一共有以上四种类型,注册的时候可以同时对多种类型的事件感兴趣,例如:

  1. SelectionKey key
  2. = channel.register(selector,
  3. Selectionkey.OP_READ | SelectionKey.OP_WRITE);

这样,当这个 Channel 发生读或写事件,我们调用 Selector.select() 就可以得知有事件发生。

具体 Selector.select() 有三个重载方法:

  • int selectNow(),不论是否有无事件发生,立即返回
  • int select(long timeout),至多阻塞 timeout 时间(或被唤醒),如果提早有事件发生,提早返回
  • int select(),一直阻塞着,直到有事件发生(或被唤醒)

返回值就是就绪的通道数,一般判断大于 0 即可进行后续的操作。

后续的操作就是调用:

  1. Set selectedKeys = selector.selectedKeys();

获得了一个类型为 Set 的 selectedKeys 集合,那这个 selectedKeys 又是啥玩意?

我们来看一下它的方法和成员:

Netty 基石,Java NIO 核心知识

看到这些成员,其实我们就很清晰了,我们可以通过 selectedKey 得知当前发生的是什么事件,有 isAcceptable、isReadable 等等。

然后还能获得对应的 channel 进行相应的读写操作,还有获取 attachment 等等。

所以得到了 selectedKeys 就可以通过迭代器遍历所有发生事件的连接,然后进行操作。

大致使用的代码如下所示:

  1. while(true) {
  2. int readyNum = selector.select();
  3. if (readyNum == 0) {
  4. continue;
  5. }
  6. Set selectedKeys = selector.selectedKeys();
  7. Iterator keyIterator = selectedKeys.iterator();
  8. while(keyIterator.hasNext()) {
  9. SelectionKey key = keyIterator.next();
  10. if(key.isAcceptable()) {
  11. // a connection was accepted by a ServerSocketChannel.
  12. } else if (key.isConnectable()) {
  13. // a connection was established with a remote server.
  14. } else if (key.isReadable()) {
  15. // a channel is ready for reading
  16. } else if (key.isWritable()) {
  17. // a channel is ready for writing
  18. }
  19. keyIterator.remove(); //执行完毕之后,需要在循环内移除自己
  20. }
  21. }

还有个方法就是 Selector.wakeup(),可以唤醒阻塞着的 Selector。

对了还有一点没说,就是如果 Channel 要和 Selector 搭配,那它必须得是非阻塞的,即配置

  1. channel.configureBlocking(false);

从上面的操作,我们可以得知 Selector 处理事件的时候必须快,如果长时间处理某个事件,那么注册到 Selector 上的其他连接的事件就不会被及时处理,造成客户端阻塞。

至此,想必你应该清晰 Selector 具体是如何管理这么多连接的了。

参考:https://ifeve.com/java-nio-all/

原文链接:https://mp.weixin.qq.com/s/0p3hId2GCy48yyGH55A5Qg