摘要:上一篇讲 Processor 怎么启动、怎么把新连接注册到 Selector。但连接注册完只是第一步------真正的问题是:Producer 发过来的字节,Processor 是怎么读上来、怎么解析成一个完整 Request 的?这篇从 poll 到入队,把请求接收全链路讲透。
关键词:Kafka Processor、OP_READ、ByteBuffer、粘包拆包、RequestHeader、二进制协议
前情:连接注册了,然后呢
上一篇讲了 configureNewConnections() 把 Acceptor 塞进来的 SocketChannel 注册到 Selector 的 OP_READ。但注册完只是"监听"------真正的字节还在网络上。
Processor 的核心循环里有一步 selector.poll(),这个调用会阻塞直到有连接可读。一旦 poll 返回,Processor 就开始读字节。
01 完整接收链路

整个过程分六步:
java
// Processor.run() 中 poll 之后
selector.poll(pollTimeoutMs);
// 遍历所有就绪的连接
for (KChannel channel : selector.completedReceives()) {
// 1. 从 SocketChannel 读到 ByteBuffer
// 2. 解析 RequestHeader
// 3. 按 apiKey 解析 RequestBody
// 4. 组装 Request 对象
// 5. 送入 RequestChannel
Request request = buildRequest(channel);
requestChannel.sendRequest(request);
}
关键:读字节和解析是在同一个 Processor 线程里做的。如果一次 poll 读到半包,ByteBuffer 保留着,等下次 OP_READ 继续往里塞。
02 Kafka 的二进制协议

Kafka 不是 HTTP,是自定义的二进制协议。每条请求的格式是:
[4字节length][apiKey:2][apiVersion:2][correlationId:4][clientId:变长][RequestBody:变长]
为什么先读 length?因为 TCP 是流式协议,没有消息边界。4 字节 length 告诉 Processor 这条消息总共多少字节。不够就等下次读,够了才开始解析。这和 HTTP 的 Content-Length 思路一样。
解析代码大致是这样:
java
// 从 ByteBuffer 解析
int length = buffer.getInt(); // 消息总长度
short apiKey = buffer.getShort(); // 请求类型
short version = buffer.getShort(); // 版本号
int correlationId = buffer.getInt(); // 关联ID
String clientId = readString(buffer); // 客户端ID
// 根据 apiKey 找到对应的 Schema
ApiKeys api = ApiKeys.forId(apiKey);
Struct requestBody = api.requestSchema(version).read(buffer);
// 组装完整 Request
RequestHeader header = new RequestHeader(apiKey, version, correlationId, clientId);
Request request = Request.parseRequest(header, requestBody);
apiKey 是请求类型的编号:Produce=0、Fetch=1、Metadata=3、JoinGroup=11......每个 apiKey 对应一个请求处理器。
03 ByteBuffer 怎么处理粘包拆包

TCP 是流,没有消息边界。Kafka 的处理方式很经典------length 前缀 + ByteBuffer 累积。
三种情况:
完整消息:一次 poll 读够了 4+N 字节,直接解析入队,清空 buffer。
半包(拆包):只读了 length 的前 2 字节,或 length 够了但 body 只到一半。ByteBuffer 不清空,保留现有数据,等下次 OP_READ 继续往里读。读够了再解析。
粘包:一次读到了 2 条完整消息。解析第一条入队,buffer 里剩下的第二条继续处理。循环直到 buffer 不够一条完整消息为止。
这个设计的核心是:ByteBuffer 是有状态的------它记录了当前读到哪了。Processor 不需要每条消息都重新读,只需要维护好 position 和 limit。
04 为什么不在业务线程里读
有人可能会问:为什么读字节和解析要放在 Processor 线程,不让业务线程池去做?
因为 NIO Selector 必须在同一个线程里操作。如果业务线程去读 SocketChannel,Selector 的状态就乱了。所以 Processor 负责 IO(读字节、解析),业务线程负责逻辑处理(写磁盘、复制副本)。这是典型的"IO 和业务分离"模型。
数据从网络到业务线程的完整路径:
- Processor 读字节 → 解析成 Request
- Request 塞进 RequestChannel 的队列
- RequestHandler 线程池从队列取 Request
- 执行业务逻辑(写日志、处理请求)
- Response 塞进 Processor 的响应队列
- Processor 把 Response 写回 Channel
写在最后
Processor 接收请求的核心就两件事:读字节(处理半包粘包)、解析协议(length→header→body→Request)。理解了这两步,就理解了 Kafka 网络层怎么从字节流变成业务对象。