Kafka源码分析12-Producer处理拆包粘包完美方案

651 阅读6分钟

欢迎大家关注 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源码设计与实现

Kafka 源码分析系列