欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
上文 Kafka源码分析10-Producer终于和broker建立连接了将producer的请求发送到服务端,服务端如何处理请求我们暂时不管,本文将分析Producer接收服务端发送回的响应,如何处理拆包和粘包问题,绝对是教科书级代码。
注册OP_READ时间
在什么时候我们注册了OP_READ时间,通过selector来接收服务端发送回来的响应?我们回想一下网络完成连接是的方法finishConnect():
public boolean finishConnect() throws IOException {
//完成的最后的网络的连接
boolean connected = socketChannel.finishConnect();
if (connected)
//取消了OP_CONNECT事件
//增加了OP_READ 事件 我们这儿的这个key对应的KafkaChannel是不是就可以接受服务
//端发送回来的响应了。
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
return connected;
}
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT | SelectionKey.OP_READ)
- SeletionKey,里面封装了Selector对一个连接关注那个连接上的哪些事件,OP_CONNECT,OP_WRITE,OP_READ,取消对OP_CONNECT事件的关注,增加对OP_READ事件的一个关注,主要都是通过二进制位运算来实现的
- 一旦建立好连接之后,天然的就会去监听这个连接的OP_READ事件
- 要发送请求的时候,会把这个请求暂存到KafkaChannel里去,同时让Selector监视他的OP_WRITE事件,增加一种OP_WRITE事件,同时保留了OP_READ事件,此时Selector会同时监听这个连接的OP_WRITE和OP_READ事件
- 发送完了请求之后,对事件的监听会怎么样呢?一旦写完请求之后,就会把OP_WRITE事件取消监听,就是此时不关注这个写请求的事件了,此时仅仅保留关注OP_READ事件
梳理和总结一下,通过Kafka客户端源码的研究,对NIO的编程可以有非常好的认识和进步,就是完全掌握利用底层的NIO进行开发的技术,对不同事件的监听和取消监听,是通过二进制位运算的方式来实现的。但是其实人家NIO是支持同时监听一个连接上的多种事件的,就是通过位运算的。key.interestOps() & ~ SelectionKey.OP_READ | SelectionKey.OP_WRITE,底层的NIO网络编程里是非常有实践意义的。
pollSelectionKeys()
private void pollSelectionKeys(Iterable<SelectionKey> selectionKeys,
boolean isImmediatelyConnected,
long currentTimeNanos) {
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
// 省略。。
/* if channel is ready read from any connections that have readable data */
if (channel.ready() && key.isReadable() && !hasStagedReceive(channel)) {
NetworkReceive networkReceive;
//接受服务端发送回来的响应(请求)
//networkReceive 代表的就是一个服务端发送回来的响应
// 里面不断的读取数据 读取数据的代码我们之前就已经分析过
// 里面还涉及粘包和拆包的问题
while ((networkReceive = channel.read()) != null)
addToStagedReceives(channel, networkReceive);
}
// 省略。。
} catch (Exception e) {
String desc = channel.socketDescription();
if (e instanceof IOException)
log.debug("Connection with {} disconnected", desc, e);
else
log.warn("Unexpected error from {}; closing connection", desc, e);
close(channel, true);
}
}
}
// 放入暂存响应中
private void addToStagedReceives(KafkaChannel channel, NetworkReceive receive) {
//channel代表的就是一个网络的连接,一台kafka的主机就对应了一个channel连接。
if (!stagedReceives.containsKey(channel))
stagedReceives.put(channel, new ArrayDeque<NetworkReceive>());
Deque<NetworkReceive> deque = stagedReceives.get(channel);
// 往队列里面存放接收到的响应
deque.add(receive);
}
主要是在channel.read()接收服务端发送响应:
public NetworkReceive read() throws IOException {
NetworkReceive result = null;
if (receive == null) {
receive = new NetworkReceive(maxReceiveSize, id);
}
//一直在读取数据。
receive(receive);
//是否读完一个完整的响应消息
if (receive.complete()) {
receive.payload().rewind();
result = receive;
receive = null;
}
return result;
}
private long receive(NetworkReceive receive) throws IOException {
return receive.readFrom(transportLayer);
}
public long readFrom(ScatteringByteChannel channel) throws IOException {
return readFromReadableChannel(channel);
}
// 从channel读取代码 核心
public long readFromReadableChannel(ReadableByteChannel channel) throws IOException {
int read = 0;
//size是一个4字节大小的内存空间
//如果size还有剩余的内存空间。 size 处理拆包问题
if (size.hasRemaining()) {
//先读取4字节的数据,(代表的意思就是后面跟着的消息体的大小)
int bytesRead = channel.read(size);
if (bytesRead < 0)
throw new EOFException();
read += bytesRead;
//一直要读取到当这个size没有剩余空间
//说明已经读取到了一个4字节的int类型的数了。
if (!size.hasRemaining()) {
size.rewind();
int receiveSize = size.getInt();
if (receiveSize < 0)
throw new InvalidReceiveException("Invalid receive (size = " + receiveSize + ")");
if (maxSize != UNLIMITED && receiveSize > maxSize)
throw new InvalidReceiveException("Invalid receive (size = " + receiveSize + " larger than " + maxSize + ")");
//分配一个内存空间,这个内存空间的大小
//就是刚刚读出来的那个4字节的int的大小。
/**
* 处理粘包问题 buffer
*/
this.buffer = ByteBuffer.allocate(receiveSize);
}
}
if (buffer != null) {
//去读取数据
int bytesRead = channel.read(buffer);
if (bytesRead < 0)
throw new EOFException();
read += bytesRead;
}
return read;
}
(1)粘包问题:一个请求里面会带有多个响应,多个消息在一起给你发送回来
业内解决方法:int类型的数(消息大小,一般4字节) 消息(int类型的数)
(2)拆包问题:本来属于一个消息,但是被拆成多个消息发送回来
有两个地方会发生拆包 int类型的数(消息大小) 消息体
kafka处理粘包
例子:199响应消息(1)238响应消息(2)355响应消息(3)
-
此时会从channel中读取4个字节的数字,写入到size ByteBuffer(4个字节),就是如果已经读取到了4个字节,position就会变成4,就会跟limit是一样的,此时就代表着size ByteBuffer的4个字节已经读满了
-
ByteBuffer.getInt(),就会默认从ByteBuffer当前position的位置获取4个字节,转换为一个int类型的数字返回给你
-
接下来就会直接把channel里的一条响应消息的数据读取到一个跟他的大小一致的ByteBuffer中去,粘包问题的解决,就是完美的通过每条消息基于一个4个字节的int数字(他们自己的大小)来进行分割
-
拆包,假如说size是4个字节,你一次read就读取到了2个字节,连size都没有读取完毕,出现了拆包,此时怎么办呢?或者你读取到了一个size,199个字节,但是在读取响应消息的时候,就读取到了162个字节,拆包问题,响应消息没有读取完毕
-
ByteBuffer.rewind,把position设置为0,一个ByteBuffer写满之后,调用rewind,把position重置为0,此时就可以从ByteBuffer里读取数据了
kafka处理拆包
199 响应消息 238响应消息352响应消息
- 在读取消息的时候,4个字节的size都没读完,2个字节,或者是199个字节的消息就读到了162个字节,拆包问题怎么来处理的呢?
- position = 0,limit = 4
- 现在读取1个字节,position = 1;读取2个字节,position = 2,此时remaining是2,还剩下个2字节是可以读取的
- 这一次这个poll里面,对这个broker的读取事件的处理就完事儿了,就读到了2个字节,什么都没有,下一次如果再次执行poll,发现又有数据可以读取了,此时的话呢,就会再次运行到这里去
- NetworkReceive还是停留在那里,所以呢可以继续读取
- 剩余只能读2个字节,所以最多就只能读取2个字节到里面去,4个字节凑满了,此时就说明size数字是可以读取出来了,解决了size的拆包的问题,第二次拆包问题发生了,199个字节的消息,只读取到了162个字节
- 37个字节是剩余可以读取的
- 下一次又发现这个broker有OP_READ可以读取的时候,再次进来,继续读取数据
总结
这段代码,你在外面绝对见不到的,完美的处理了发送请求和读取响应的粘包和拆包的问题,用NIO来编程,主要要自己考虑的其实就是粘包和拆包的问题。
参考文档:
史上最详细kafka源码注释(kafka-0.10.2.0-src)
kafka技术内幕-图文详解Kafka源码设计与实现