在实现自定义消息队列(MQ)的 BrokerServer 时,核心的业务逻辑与网络通信交互均集中在processConnection 方法及其关联的process方法中。本文将结合设计思路,逐行拆解这两个核心方法的执行流程、设计思想与代码细节,帮助理解 MQ 服务器如何处理客户端的各类请求。
一、processConnection:单个客户端连接的生命周期管理
1. 方法核心定位
processConnection是处理单个 TCP 连接 的核心方法。从板书的网络架构图中能看到,一个 TCP 连接中可能包含多个 Channel(通道),而该方法的核心职责是:维护单个 TCP 连接的输入输出流,循环读取客户端请求、调用业务逻辑处理、返回响应,并在连接关闭时完成资源清理。
2. 代码逻辑拆解
java
private void processConnection(Socket clientSocket) {
// 1. 基于Socket获取输入输出流,用于网络数据交互
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 2. 封装成DataInputStream/DataOutputStream,方便按指定格式读写数据
try (DataInputStream dataInputStream = new DataInputStream(inputStream);
DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
// 3. 循环处理该连接上的所有请求(一个连接可多次交互)
while (true) {
// 步骤1:读取并解析客户端请求
Request request = readRequest(dataInputStream);
// 步骤2:根据请求类型,执行业务逻辑并生成响应
Response response = process(request, clientSocket);
// 步骤3:将响应写回客户端
writeResponse(dataOutputStream, response);
}
}
} catch (EOFException | SocketException e) {
// 捕获EOF异常(客户端关闭连接)、Socket异常(连接断开),终止循环
System.out.println("[BrokerServer] connection 关闭! 客户端的地址: " + clientSocket.getInetAddress().toString()
+ ":" + clientSocket.getPort());
} catch (IOException | ClassNotFoundException | MqException e) {
// 处理其他IO异常、序列化异常、自定义MQ异常
System.out.println("[BrokerServer] connection 出现异常!");
e.printStackTrace();
} finally {
// 4. 连接关闭/异常时,释放资源并清理会话
try {
clientSocket.close();
// 清理该Socket对应的所有Channel会话(一个连接对应多个Channel)
clearClosedSession(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 关键设计点
- 流的封装 :使用
DataInputStream和DataOutputStream是为了固定数据读写格式,对应板书中 "请求 / 响应的格式约定"------ 先写类型(int)、再写长度(int)、最后写载荷(byte []),保证数据解析的一致性。 - 循环处理 :一个 TCP 连接不会只处理一次请求,而是通过
while(true)持续交互,对应板书中 "一个 TCP 连接可包含多个 Channel,多次请求 / 响应" 的设计。 - 异常捕获 :
EOFException是DataInputStream读取到流末尾时抛出的,代表客户端主动关闭连接,此时需正常终止循环;其他异常则打印日志,保证服务器不崩溃。 - 资源清理 :
finally中关闭 Socket 并调用clearClosedSession,避免连接泄漏,对应板书中 "连接关闭时需清理会话(sessions)" 的要求。
二、readRequest 与 writeResponse:请求 / 响应的格式解析
在处理请求前,需先按约定格式读取数据;返回响应时,也需按相同格式封装数据,这两个方法是网络通信的 "格式桥梁"。
1. readRequest:读取并解析客户端请求
java
private Request readRequest(DataInputStream dataInputStream) throws IOException {
Request request = new Request();
// 读取请求类型(如0x1=创建Channel、0x9=消息发布)
request.setType(dataInputStream.readInt());
// 读取请求载荷的长度
request.setLength(dataInputStream.readInt());
// 按长度读取载荷数据(序列化后的对象字节)
byte[] payload = new byte[request.getLength()];
int n = dataInputStream.read(payload);
if (n != request.getLength()) {
throw new IOException("读取请求格式出错!");
}
request.setPayload(payload);
return request;
}
核心逻辑:严格按照 "类型 + 长度 + 载荷" 的格式读取,若读取的字节数与预期长度不符,直接抛出异常,避免数据解析错误。
2. writeResponse:写回响应数据
java
private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {
dataOutputStream.writeInt(response.getType());
dataOutputStream.writeInt(response.getLength());
dataOutputStream.write(response.getPayload());
// 刷新缓冲区,确保数据立即发送(而非缓存)
dataOutputStream.flush();
}
核心逻辑 :按相同格式封装响应,flush()是关键 ------ 若不刷新,数据会留在缓冲区,导致客户端无法及时收到响应。
三、process:请求的核心业务处理逻辑
process方法是整个 BrokerServer 的业务核心 ,对应板书中 "请求类型与操作映射" 的设计 ------ 根据请求的type值,执行不同的 MQ 核心操作(创建交换机、声明队列、消息发布 / 消费等)。
1. 代码整体结构
java
private Response process(Request request, Socket clientSocket) throws IOException, ClassNotFoundException, MqException {
// 1. 反序列化请求载荷,获取基础参数(rid、channelId等)
BasicArguments basicArguments = (BasicArguments) BinaryTool.fromBytes(request.getPayload());
System.out.println("[Request] rid=" + basicArguments.getRid() + ", channelId=" + basicArguments.getChannelId()
+ ", type=" + request.getType() + ", length=" + request.getLength());
boolean ok = true; // 标记操作是否成功
// 2. 根据请求type,执行对应业务逻辑
if (request.getType() == 0x1) {
// 创建Channel:将channelId与Socket绑定到sessions哈希表
sessions.put(basicArguments.getChannelId(), clientSocket);
System.out.println("[BrokerServer] 创建 channel 完成! channelId=" + basicArguments.getChannelId());
} else if (request.getType() == 0x2) {
// 销毁Channel:从sessions中移除对应的channelId
sessions.remove(basicArguments.getChannelId());
System.out.println("[BrokerServer] 销毁 channel 完成! channelId=" + basicArguments.getChannelId());
} else if (request.getType() == 0x3) {
// 声明交换机:调用虚拟主机的exchangeDeclare方法
ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) basicArguments;
ok = virtualHost.exchangeDeclare(arguments.getExchangeName(), arguments.getExchangeType(),
arguments.isDurable(), arguments.isAutoDelete(), arguments.getArguments());
} else if (request.getType() == 0x4) {
// 删除交换机
ExchangeDeleteArguments arguments = (ExchangeDeleteArguments) basicArguments;
ok = virtualHost.exchangeDelete(arguments.getExchangeName());
} else if (request.getType() == 0x5) {
// 声明队列
QueueDeclareArguments arguments = (QueueDeclareArguments) basicArguments;
ok = virtualHost.queueDeclare(arguments.getQueueName(), arguments.isDurable(),
arguments.isExclusive(), arguments.isAutoDelete(), arguments.getArguments());
} else if (request.getType() == 0x6) {
// 删除队列
QueueDeleteArguments arguments = (QueueDeleteArguments) basicArguments;
ok = virtualHost.queueDelete((arguments.getQueueName()));
} else if (request.getType() == 0x7) {
// 队列绑定交换机
QueueBindArguments arguments = (QueueBindArguments) basicArguments;
ok = virtualHost.queueBind(arguments.getQueueName(), arguments.getExchangeName(), arguments.getBindingKey());
} else if (request.getType() == 0x8) {
// 解绑队列与交换机
QueueUnbindArguments arguments = (QueueUnbindArguments) basicArguments;
ok = virtualHost.queueUnbind(arguments.getQueueName(), arguments.getExchangeName());
} else if (request.getType() == 0x9) {
// 发布消息
BasicPublishArguments arguments = (BasicPublishArguments) basicArguments;
ok = virtualHost.basicPublish(arguments.getExchangeName(), arguments.getRoutingKey(),
arguments.getBasicProperties(), arguments.getBody());
} else if (request.getType() == 0xa) {
// 订阅消息(消费消息):注册消费者回调,实现消息推送
BasicConsumeArguments arguments = (BasicConsumeArguments) basicArguments;
ok = virtualHost.basicConsume(arguments.getConsumerTag(), arguments.getQueueName(), arguments.isAutoAck(),
new Consumer() {
@Override
public void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {
// 1. 根据channelId(consumerTag)获取客户端Socket
Socket clientSocket = sessions.get(consumerTag);
if (clientSocket == null || clientSocket.isClosed()) {
throw new MqException("[BrokerServer] 订阅消息的客户端已经关闭!");
}
// 2. 构造推送响应(type=0xc)
SubScribeReturns subScribeReturns = new SubScribeReturns();
subScribeReturns.setChannelId(consumerTag);
subScribeReturns.setRid("");
subScribeReturns.setOk(true);
subScribeReturns.setConsumerTag(consumerTag);
subScribeReturns.setBasicProperties(basicProperties);
subScribeReturns.setBody(body);
byte[] payload = BinaryTool.toBytes(subScribeReturns);
// 3. 封装响应对象
Response response = new Response();
response.setType(0xc); // 0xc表示服务器主动推送消息
response.setLength(payload.length);
response.setPayload(payload);
// 4. 推送数据到客户端 此处的 dataOutputStream 这个对象不能 close !!!
// 如果 把 dataOutputStream 关闭, 就会直接把 clientSocket 里的 outputStream 也关了.
// 此时就无法继续往 socket 中写入后续数据了.
DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());
writeResponse(dataOutputStream, response);
}
});
} else if (request.getType() == 0xb) {
// 消息确认:客户端消费成功后,调用basicAck确认
BasicAckArguments arguments = (BasicAckArguments) basicArguments;
ok = virtualHost.basicAck(arguments.getQueueName(), arguments.getMessageId());
} else {
// 未知请求类型,抛出异常
throw new MqException("[BrokerServer] 未知的 type! type=" + request.getType());
}
// 3. 构造并返回响应对象
BasicReturns basicReturns = new BasicReturns();
basicReturns.setChannelId(basicArguments.getChannelId());
basicReturns.setRid(basicArguments.getRid());
basicReturns.setOk(ok);
byte[] payload = BinaryTool.toBytes(basicReturns);
Response response = new Response();
response.setType(request.getType());
response.setLength(payload.length);
response.setPayload(payload);
System.out.println("[Response] rid=" + basicReturns.getRid() + ", channelId=" + basicReturns.getChannelId()
+ ", type=" + response.getType() + ", length=" + response.getLength());
return response;
}
2. 核心逻辑拆解
(1)请求参数反序列化
所有请求的载荷(payload)都是序列化后的对象,通过BinaryTool.fromBytes反序列化为BasicArguments及其子类(如ExchangeDeclareArguments),这是 MQ 请求参数传递的标准方式,对应板书中 "请求载荷为序列化后的参数对象" 的设计。
(2)消息订阅的特殊逻辑(核心难点)
当请求 type 为0xa(订阅消息)时,核心是注册消费者回调:
- 客户端订阅后,服务器通过
virtualHost.basicConsume注册Consumer回调; - 当有消息到达队列时,回调
handleDelivery方法被触发; - 方法中通过
consumerTag(即 channelId)从sessions获取客户端 Socket,将消息主动推送给客户端(而非客户端轮询); - 注意:创建
DataOutputStream时不能关闭流,否则会关闭客户端 Socket 的输出流,导致后续无法推送消息。
(3)响应构造与返回
无论执行哪种操作,最终都构造BasicReturns对象作为响应载荷,包含channelId、rid(请求 ID,用于匹配请求)、ok(操作结果),再序列化为字节数组后封装为Response返回,保证客户端能清晰知晓操作结果。
3. 关键细节优化
- sessions 哈希表的使用 :
sessions是ConcurrentHashMap,保证多线程下的线程安全,用于存储 "channelId-Socket" 的映射,对应板书中 "一个 TCP 连接对应多个 Channel,sessions 存储所有会话" 的设计; - clearClosedSession 方法 :遍历
sessions,收集该 Socket 对应的所有 channelId,再批量移除,避免 "一边遍历一边删除" 导致的迭代器失效问题,这是集合操作的重要规范。
四、总结
从整体流程来看,BrokerServer 的请求处理链路是:客户端建立 TCP 连接 → processConnection 管理连接生命周期 → readRequest 解析请求 → process 执行业务逻辑(结合板书的请求类型映射) → writeResponse 返回响应。
其中,processConnection负责连接的基础管理,process是业务核心,二者配合完成了 MQ 服务器的网络通信与核心业务逻辑。而代码中对数据格式的严格约定、多线程安全的处理、资源的及时清理,都是保证 MQ 服务器稳定运行的关键 ------ 这也是想要传递的 MQ 服务器设计核心。