背景
ServerCnxn
代表了一个客户端与一个server的连接,其有两种实现,分别是NIOServerCnxn
和NettyServerCnxn
,类图如下:
本文介绍ZooKeeper是如何通过NIOServerCnxn
实现网络IO的.
处理read事件
发生时机
当SocketChannel
上有数据可读时,worker thread调用NIOServerCnxn.doIO()
进行读操作
粘包拆包问题
处理读事件比较麻烦的问题就是通过TCP发送的报文会出现粘包拆包问题,Zookeeper为了解决此问题,在设计通信协议时将报文分为3个部分:
- 请求头和请求体的长度(4个字节)
- 请求头
- 请求体
注:(1)请求头和请求体也细分为更小的部分,但在此不做深入研究,只需知道请求的前4个字节是请求头和请求体的长度即可.(2)将请求头和请求体称之为payload
在报文头增加了4个字节的长度字段,表示整个报文除长度字段之外的长度.服务端可根据该长度将粘包拆包的报文分离或组合为完整的报文.NIOServerCnxn
读取数据流程如下:
- NIOServerCnxn中有两个属性,一个是lenBuffer,容量为4个字节,用于读取长度信息.一个是incomingBuffer,其初始化时即为lenBuffer,但是读取长度信息后,就为incomingBuffer分配对应的空间用于读取payload
- 根据请求报文的长度分配incomingBuffer的大小
- 将读到的字节存放在incomingBuffer中,直至读满(由于第2步中为incomingBuffer分配的长度刚好是报文的长度,此时incomingBuffer中刚好时一个报文)
- 处理报文
代码如下:
void doIO(SelectionKey k) throws InterruptedException {
try {
...
/*
处理读操作的流程
1.最开始incomingBuffer就是lenBuffer,容量为4.第一次读取4个字节,即此次请求报文的长度
2.根据请求报文的长度分配incomingBuffer的大小
3.将读到的字节存放在incomingBuffer中,直至读满
(由于第2步中为incomingBuffer分配的长度刚好是报文的长度,此时incomingBuffer中刚好时一个报文)
4.处理报文
*/
if (k.isReadable()) {
//若是客户端请求,此时触发读事件
//初始化时incomingBuffer即时lengthBuffer,只分配了4个字节,供用户读取一个int(此int值就是此次请求报文的总长度)
int rc = sock.read(incomingBuffer);
if (rc < 0) {
throw new EndOfStreamException(
"Unable to read additional data from client sessionid 0x"
+ Long.toHexString(sessionId)
+ ", likely client has closed socket");
}
/*
只有incomingBuffer.remaining() == 0,才会进行下一步的处理,否则一直读取数据直到incomingBuffer读满,此时有两种可能:
1.incomingBuffer就是lenBuffer,此时incomingBuffer的内容是此次请求报文的长度.
根据lenBuffer为incomingBuffer分配空间后调用readPayload().
在readPayload()中会立马进行一次数据读取,(1)若可以将incomingBuffer读满,则incomingBuffer中就是一个完整的请求,处理该请求;
(2)若不能将incomingBuffer读满,说明出现了拆包问题,此时不能构造一个完整的请求,只能等待客户端继续发送数据,等到下次socketChannel可读时,继续将数据读取到incomingBuffer中
2.incomingBuffer不是lenBuffer,说明上次读取时出现了拆包问题,incomingBuffer中只有一个请求的部分数据.
而这次读取的数据加上上次读取的数据凑成了一个完整的请求,调用readPayload()
*/
if (incomingBuffer.remaining() == 0) {
boolean isPayload;
if (incomingBuffer == lenBuffer) {
// start of next request
//解析上文中读取的报文总长度,同时为"incomingBuffer"分配len的空间供读取全部报文
incomingBuffer.flip();
//为incomeingBuffer分配空间时还包括了判断是否是"4字命令"的逻辑
isPayload = readLength(k);
incomingBuffer.clear();
} else {
//2.incomingBuffer不是lenBuffer,此时incomingBuffer的内容是payloa