在上一篇中,主要带大家深度剖析了「 生产者元数据 」的拉取、管理全流程,今天我们就来聊聊 Kafka 是如何对 Java NIO 进行封装的 ,本系列总共分为3篇,主要剖析以下几个问题:
- 针对 Java NIO 的 SocketChannel,kafka 是如何封装统一的传输层来实现最基础的网络连接以及读写操作的?
- 剖析 KafkaChannel 是如何对传输层、读写 buffer 操作进行封装的?
- 剖析工业级 NIO 实战:如何基于位运算来控制事件的监听以及拆包、粘包是如何实现的?
- 剖析 Kafka 是如何封装 Selector 多路复用器的?
- 剖析 Kafka 封装的 Selector 是如何初始化并与 Broker 进行连接以及网络读写的?
- 剖析 Kafka 网络发送消息和接收响应的整个过程是怎样的?
本篇只讨论前3个问题,剩余的放到后2篇中。
认真读完这篇文章,我相信你会对 Kafka 封装 Java NIO 源码有更加深刻的理解。
这篇文章干货很多,希望你可以耐心读完。
01 总体概述
上篇剖析了「 生产者元数据的拉取和管理的全过程 」,此时发送消息的时候就有了元数据,但是还没有进行网络通信,而网络通信是一个相对复杂的过程,对于 Java 系统来说网络通信一般会采用 NIO 库来实现,所以 Kafka 对 Java NIO 封装了统一的框架,来实现多路复用的网络 I/O 操作 。
为了方便大家理解,所有的源码只保留骨干。
02 Kafka 对 Java NIO 的封装
如果大家对 Java NIO 不了解的话,可以看下这个文档,这里就不过多介绍了。
https://pdai.tech/md/java/io/java-io-nio.html
我们来看看 Kafka 对 Java NIO 组件做了哪些封装? 这里先说下结果,后面会深度剖析。
- TransportLayer:它是一个接口,封装了底层 NIO 的 SocketChannel。
- NetworkReceive:封装了 NIO 的 ByteBuffer 中的读 Buffer, 对网络编程中的粘包、拆包经典实现 。
- NetworkSend:封装了 NIO 的 ByteBuffer 中的写 Buffer。
- KafkaChannel:对 TransportLayer、NetworkReceive、NetworkSend 进一步封装,屏蔽了底层的实现细节,对上层更友好。
- KafkaSelector:封装了 NIO 的 Selector 多路复用器组件。
接下来我们挨个对上面组件进行剖析。
02 TransportLayer 封装过程
TransportLayer 接口是对 NIO 中 「 SocketChannel 」 的封装。它的实现类总共有 2 个:
- PlaintextTransportLayer:明文网络传输实现。
- SslTransportLayer:SSL 加密网络传输实现。
本篇只剖析 PlaintextTransportLayer 的实现。
github 源码地址如下:
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/network/PlaintextTransportLayer.java
public class PlaintextTransportLayer implements TransportLayer {
// java nio 中 SelectionKey 事件
private final SelectionKey key;
// java nio 中的SocketChannel
private final SocketChannel socketChannel;
// 安全相关
private final Principal principal = KafkaPrincipal.ANONYMOUS;
// 初始化
public PlaintextTransportLayer(SelectionKey key) throws IOException {
// 对 NIO 中 SelectionKey 类的对象引用
this.key = key;
// 对 NIO 中 SocketChannel 类的对象引用
this.socketChannel = (SocketChannel) key.channel();
}
}
从上面代码可以看出,该类就是 对底层 NIO 的 socketChannel 封装引用 。将构造函数的 SelectionKey 类对象赋值给 key,然后从 key 中取出对应的 SocketChannel 赋值给 socketChannel,这样就完成了初始化工作。
接下来,我们看看几个重要方法是如何使用这2个 NIO 组件的。
02.1 finishConnect()
@Override
// 判断网络连接是否完成
public boolean finishConnect() throws IOException {
// 1. 调用socketChannel的finishConnect方法,返回该连接是否已经连接完成
boolean connected = socketChannel.finishConnect();
// 2. 如果网络连接完成以后就删除对OP_CONNECT事件的监听,同时添加对OP_READ事件的监听
if (connected)
// 事件操作
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
// 3. 最后返回网络连接
return connected;
}
该方法主要用来 判断网络连接是否完成 ,如果完成就关注 「 OP_READ 」 事件,并取消 「 OP_CONNECT 」 事件。
- 首先调用 socketChannel 通道的 finishConnect() 判断连接是否完成。
- 如果网络连接完成以后就删除对 OP_CONNECT 事件的监听,同时添加对 OP_READ 事件的监听,因为连接完成后就可能接收数据了。
- 最后返回网络连接 connected。
二进制位运算事件监听
这里通过「 二进制位运算 」巧妙的解决了网络事件的监听操作,实现非常经典。
通过 socketChannel 在 Selector 多路复用器注册事件返回 SelectionKey ,SelectionKey 的类型包括:
- OP_READ:可读事件,值为:1<<0 == 1 == 00000001。
- OP_WRITE:可写事件,值为:1<<2 == 4 == 00000100。
- OP_CONNECT:客户端连接服务端的事件,一般为创建 SocketChannel 客户端 channel,值为:1<<3 == 8 ==00001000。
- OP_ACCEPT:服务端接收客户端连接的事件,一般为创建 ServerSocketChannel 服务端 channel,值为:1<<4 == 16 == 00010000。
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
首先"~"符号代表按位取反,"&"代表按位取与,通过 key.interestOps() 获取当