5. 断开重连
Netty 客户端需要实现断开重连机制,解决各种情况下的断开情况。例如说:
- Netty 客户端启动时,Netty 服务端处于挂掉,导致无法连接上。
- 在运行过程中,Netty 服务端挂掉,导致连接被断开。
- 任一一端网络抖动,导致连接异常断开。
具体的代码实现比较简单,只需要在两个地方增加重连机制。
- Netty 客户端启动时,无法连接 Netty 服务端时,发起重连。
- Netty 客户端运行时,和 Netty 断开连接时,发起重连。
思考到重连会存在失败的情况,我们采用定时重连的方式,避免占用过多资源。
5.1 具体代码
① 在 NettyClient 中,提供 #reconnect() 方法,实现定时重连的逻辑。代码如下:
// NettyClient.java
public void reconnect() {
eventGroup.schedule(new Runnable() {
@Override
public void run() {
logger.info("[reconnect][开始重连]");
try {
start();
} catch (InterruptedException e) {
logger.error("[reconnect][重连失败]", e);
}
}
}, RECONNECT_SECONDS, TimeUnit.SECONDS);
logger.info("[reconnect][{} 秒后将发起重连]", RECONNECT_SECONDS);
}
通过调用 EventLoop 提供的 #schedule(Runnable command, long delay, TimeUnit unit) 方法,实现定时逻辑。而在内部的具体逻辑,调用 NettyClient 的 #start() 方法,发起连接 Netty 服务端。
又由于 NettyClient 在 #start() 方法在连接 Netty 服务端失败时,又会调用 #reconnect()方法,从而再次发起定时重连。如此循环反复,知道 Netty 客户端连接上 Netty 服务端。如下图所示:

NettyClient 重连
② 在 NettyClientHandler 中,实现 #channelInactive(ChannelHandlerContext ctx) 方法,在发现和 Netty 服务端断开时,调用 Netty Client 的 #reconnect() 方法,发起重连。代码如下:
// NettyClientHandler.java
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 发起重连
nettyClient.reconnect();
// 继续触发事件
super.channelInactive(ctx);
}
5.2 简单测试
① 启动 Netty Client,不要启动 Netty Server,控制台打印日志如下图:

重连失败
可以看到 Netty Client 在连接失败时,不断发起定时重连。
② 启动 Netty Server,控制台打印如下图:

重连成功
可以看到 Netty Client 成功重连上 Netty Server。
6. 心跳机制与空闲检测
在上文中,艿艿推荐胖友阅读《TCP Keepalive 机制刨根问底》文章,我们可以了解到 TCP 自带的空闲检测机制,默认是 2 小时。这样的检测机制,从系统资源层面上来说是可以接受的。
但是在业务层面,如果 2 小时才发现客户端与服务端的连接实际已经断开,会导致中间超级多的消息丢失,影响客户的使用体验。
因此,我们需要在业务层面,自己实现空闲检测,保证尽快发现客户端与服务端实际已经断开的情况。实现逻辑如下:
- 服务端发现 180 秒未从客户端读取到消息,主动断开连接。
- 客户端发现 180 秒未从服务端读取到消息,主动断开连接。
思考到客户端和服务端之间并不是一直有消息的交互,所以我们需要增加心跳机制:
- 客户端每 60 秒向服务端发起一次心跳消息,保证服务端可以读取到消息。
- 服务端在收到心跳消息时,回复客户端一条确认消息,保证客户端可以读取到消息。
“
友情提示:
为什么是 180 秒?可以加大或者减小,看自己希望多快检测到连接异常。过短的时间,会导致心跳过于频繁,占用过多资源。
为什么是 60 秒?三次机会,确认是否心跳超时。
虽然听起来有点复杂,但是实现起来并不复杂哈。
6.1 服务端的空闲检测
在
NettyServerHandlerInitializer 中,我们添加了一个 ReadTimeoutHandler 处理器,它在超过指定时间未从对端读取到数据,会抛出 ReadTimeoutException 异常。如下图所示:

ReadTimeoutHandler
通过这样的方式,实现服务端发现 180 秒未从客户端读取到消息,主动断开连接。
6.2 客户端的空闲检测
“
友情提示:和「6.1 服务端的空闲检测」一致。
在
NettyClientHandlerInitializer 中,我们添加了一个 ReadTimeoutHandler 处理器,它在超过指定时间未从对端读取到数据,会抛出 ReadTimeoutException 异常。如下图所示:

ReadTimeoutHandler
通过这样的方式,实现客户端发现 180 秒未从服务端读取到消息,主动断开连接。
6.3 心跳机制
Netty 提供了 IdleStateHandler 处理器,提供空闲检测的功能,在 Channel 的读或者写空闲时间太长时,将会触发一个 IdleStateEvent 事件。
这样,我们只需要在 NettyClientHandler 处理器中,在接收到 IdleStateEvent 事件时,客户端向客户端发送一次心跳消息。如下图所示:

客户端心跳
- 其中,HeartbeatRequest 是心跳请求。
同时,我们在服务端项目中,创建了一个 HeartbeatRequestHandler 消息处理器,在收到客户端的心跳请求时,回复客户端一条确认消息。代码如下:
@Component
public class HeartbeatRequestHandler implements MessageHandler<HeartbeatRequest> {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void execute(Channel channel, HeartbeatRequest message) {
logger.info("[execute][收到连接({}) 的心跳请求]", channel.id());
// 响应心跳
HeartbeatResponse response = new HeartbeatResponse();
channel.writeAndFlush(new Invocation(HeartbeatResponse.TYPE, response));
}
@Override
public String getType() {
return HeartbeatRequest.TYPE;
}
}
- 其中,HeartbeatResponse 是心跳确认响应
6.4 简单测试
启动 Netty Server 服务端,再启动 Netty Client 客户端,耐心等待 60 秒后,可以看到心跳日志如下:
// ... 客户端
2020-06-22 08:24:47.275 INFO 57005 --- [ntLoopGroup-2-1] c.i.s.l.n.c.handler.NettyClientHandler : [userEventTriggered][发起一次心跳]
2020-06-22 08:24:47.335 INFO 57005 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationEncoder : [encode][连接(44223e18) 编码了一条消息(Invocation{type='HEARTBEAT_REQUEST', message='{}'})]
2020-06-22 08:24:47.408 INFO 57005 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(44223e18) 解析到一条消息(Invocation{type='HEARTBEAT_RESPONSE', message='{}'})]
2020-06-22 08:24:47.409 INFO 57005 --- [pool-1-thread-1] c.i.s.l.n.m.h.HeartbeatResponseHandler : [execute][收到连接(44223e18) 的心跳响应]
// ... 服务端
2020-06-22 08:24:47.388 INFO 56998 --- [ntLoopGroup-3-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(34778465) 解析到一条消息(Invocation{type='HEARTBEAT_REQUEST', message='{}'})]
2020-06-22 08:24:47.390 INFO 56998 --- [pool-1-thread-1] c.i.s.l.n.m.h.HeartbeatRequestHandler : [execute][收到连接(34778465) 的心跳请求]
2020-06-22 08:24:47.399 INFO 56998 --- [ntLoopGroup-3-1] c.i.s.l.n.codec.InvocationEncoder : [encode][连接(34778465) 编码了一条消息(Invocation{type='HEARTBEAT_RESPONSE', message='{}'})]
7. 认证逻辑
“
友情提示:从本小节开始,我们就具体看看业务逻辑的处理示例。
认证的过程,如下图所示:

认证流程
7.1 AuthRequest
创建 AuthRequest 类,定义用户认证请求。代码如下:
public class AuthRequest implements Message {
public static final String TYPE = "AUTH_REQUEST";
/**
* 认证 Token
*/
private String accessToken;
// ... 省略 setter、getter、toString 方法
}
这里我们使用 accessToken 认证令牌进行认证。
由于一般情况下,我们使用 HTTP 进行登录系统,然后使用登录后的身份标识(例如说 accessToken 认证令牌),将客户端和当前用户进行认证绑定。
7.2 AuthResponse
创建 AuthResponse 类,定义用户认证响应。代码如下:
public class AuthResponse implements Message {
public static final String TYPE = "AUTH_RESPONSE";
/**
* 响应状态码
*/
private Integer code;
/**
* 响应提示
*/
private String message;
// ... 省略 setter、getter、toString 方法
}
7.3 AuthRequestHandler
“
服务端…
创建 AuthRequestHandler 类,为服务端处理客户端的认证请求。代码如下:
@Component
public class AuthRequestHandler implements MessageHandler<AuthRequest> {
@Autowired
private NettyChannelManager nettyChannelManager;
@Override
public void execute(Channel channel, AuthRequest authRequest) {
// <1> 如果未传递 accessToken
if (StringUtils.isEmpty(authRequest.getAccessToken())) {
AuthResponse authResponse = new AuthResponse().setCode(1).setMessage("认证 accessToken 未传入");
channel.writeAndFlush(new Invocation(AuthResponse.TYPE, authResponse));
return;
}
// <2> ... 此处应有一段
// <3> 将用户和 Channel 绑定
// 思考到代码简化,我们先直接使用 accessToken 作为 User
nettyChannelManager.addUser(channel, authRequest.getAccessToken());
// <4> 响应认证成功
AuthResponse authResponse = new AuthResponse().setCode(0);
channel.writeAndFlush(new Invocation(AuthResponse.TYPE, authResponse));
}
@Override
public String getType() {
return AuthRequest.TYPE;
}
}
代码比较简单,胖友看看 <1>、<2>、<3>、<4> 上的注释。
7.4 AuthResponseHandler
“
客户端…
创建 AuthResponseHandler 类,为客户端处理服务端的认证响应。代码如下:
@Component
public class AuthResponseHandler implements MessageHandler<AuthResponse> {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void execute(Channel channel, AuthResponse message) {
logger.info("[execute][认证结果:{}]", message);
}
@Override
public String getType() {
return AuthResponse.TYPE;
}
}
打印个认证结果,方便调试。
7.5 TestController
“
客户端…
创建 TestController 类,提供 /test/mock 接口,模拟客户端向服务端发送请求。代码如下:
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private NettyClient nettyClient;
@PostMapping("/mock")
public String mock(String type, String message) {
// 创建 Invocation 对象
Invocation invocation = new Invocation(type, message);
// 发送消息
nettyClient.send(invocation);
return "success";
}
}
7.6 简单测试
启动 Netty Server 服务端,再启动 Netty Client 客户端,然后使用 Postman 模拟一次认证请求。如下图所示:

Postman 模拟认证请求
同时,可以看到认证成功的日志如下:
// 客户端...
2020-06-22 08:41:12.364 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationEncoder : [encode][连接(9e086597) 编码了一条消息(Invocation{type='AUTH_REQUEST', message='{"accessToken": "yunai"}'})]
2020-06-22 08:41:12.390 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(9e086597) 解析到一条消息(Invocation{type='AUTH_RESPONSE', message='{"code":0}'})]
2020-06-22 08:41:12.392 INFO 57583 --- [pool-1-thread-1] c.i.s.l.n.m.auth.AuthResponseHandler : [execute][认证结果:AuthResponse{code=0, message='null'}]
// 服务端...
2020-06-22 08:41:12.374 INFO 56998 --- [ntLoopGroup-3-2] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(791f122b) 解析到一条消息(Invocation{type='AUTH_REQUEST', message='{"accessToken": "yunai"}'})]
2020-06-22 08:41:12.379 INFO 56998 --- [ntLoopGroup-3-2] c.i.s.l.n.codec.InvocationEncoder : [encode][连接(791f122b) 编码了一条消息(Invocation{type='AUTH_RESPONSE', message='{"code":0}'})]
8. 单聊逻辑
私聊的过程,如下图所示:

私聊流程
服务端负责将客户端 A 发送的私聊消息,转发给客户端 B。
8.1 ChatSendToOneRequest
创建 ChatSendToOneRequest 类,发送给指定人的私聊消息的请求。代码如下:
public class ChatSendToOneRequest implements Message {
public static final String TYPE = "CHAT_SEND_TO_ONE_REQUEST";
/**
* 发送给的用户
*/
private String toUser;
/**
* 消息编号
*/
private String msgId;
/**
* 内容
*/
private String content;
// ... 省略 setter、getter、toString 方法
}
8.2 ChatSendResponse
创建 ChatSendResponse 类,聊天发送消息结果的响应。代码如下:
public class ChatSendResponse implements Message {
public static final String TYPE = "CHAT_SEND_RESPONSE";
/**
* 消息编号
*/
private String msgId;
/**
* 响应状态码
*/
private Integer code;
/**
* 响应提示
*/
private String message;
// ... 省略 setter、getter、toString 方法
}
8.3 ChatRedirectToUserRequest
创建 ChatRedirectToUserRequest 类, 转发消息给一个用户的请求。代码如下:
public class ChatRedirectToUserRequest implements Message {
public static final String TYPE = "CHAT_REDIRECT_TO_USER_REQUEST";
/**
* 消息编号
*/
private String msgId;
/**
* 内容
*/
private String content;
// ... 省略 setter、getter、toString 方法
}
“
友情提示:写完之后,艿艿突然发现少了一个 fromUser 字段,表明来自谁的消息。
8.4 ChatSendToOneHandler
“
服务端…
创建 ChatSendToOneHandler 类,为服务端处理客户端的私聊请求。代码如下:
@Component
public class ChatSendToOneHandler implements MessageHandler<ChatSendToOneRequest> {
@Autowired
private NettyChannelManager nettyChannelManager;
@Override
public void execute(Channel channel, ChatSendToOneRequest message) {
// <1> 这里,假装直接成功
ChatSendResponse sendResponse = new ChatSendResponse().setMsgId(message.getMsgId()).setCode(0);
channel.writeAndFlush(new Invocation(ChatSendResponse.TYPE, sendResponse));
// <2> 创建转发的消息,发送给指定用户
ChatRedirectToUserRequest sendToUserRequest = new ChatRedirectToUserRequest().setMsgId(message.getMsgId())
.setContent(message.getContent());
nettyChannelManager.send(message.getToUser(), new Invocation(ChatRedirectToUserRequest.TYPE, sendToUserRequest));
}
@Override
public String getType() {
return ChatSendToOneRequest.TYPE;
}
}
代码比较简单,胖友看看 <1>、<2> 上的注释。
8.5 ChatSendResponseHandler
“
客户端…
创建 ChatSendResponseHandler 类,为客户端处理服务端的聊天响应。代码如下:
@Component
public class ChatSendResponseHandler implements MessageHandler<ChatSendResponse> {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void execute(Channel channel, ChatSendResponse message) {
logger.info("[execute][发送结果:{}]", message);
}
@Override
public String getType() {
return ChatSendResponse.TYPE;
}
}
打印个聊天发送结果,方便调试。
8.6 ChatRedirectToUserRequestHandler
“
客户端
创建
ChatRedirectToUserRequestHandler 类,为客户端处理服务端的转发消息的请求。代码如下:
@Component
public class ChatRedirectToUserRequestHandler implements MessageHandler<ChatRedirectToUserRequest> {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void execute(Channel channel, ChatRedirectToUserRequest message) {
logger.info("[execute][收到消息:{}]", message);
}
@Override
public String getType() {
return ChatRedirectToUserRequest.TYPE;
}
}
打印个聊天接收消息,方便调试。
8.7 简单测试
① 启动 Netty Server 服务端。
② 启动 Netty Client 客户端 A。然后使用 Postman 模拟一次认证请求(用户为 yunai)。如下图所示:

Postman 模拟认证请求
③ 启动 Netty Client 客户端 B。注意,需要设置 –server.port 端口为 8081,避免冲突。如下图所示:

IDEA 设置
然后使用 Postman 模拟一次认证请求(用户为 tutou)。如下图所示:

Postman 模拟认证请求
④ 最后使用 Postman 模拟一次 yunai 芋艿给 tutou 土豆发送一次私聊消息。如下图所示:

Postman 模拟私聊请求
同时,可以看到客户端 A 向客户端 B 发送私聊消息的日志如下:
// 客户端 A...(芋艿)
2020-06-22 08:48:09.505 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(9e086597) 编码了一条消息(Invocation{type='CHAT_SEND_TO_ONE_REQUEST', message='{toUser: "tudou", msgId: "1", content: "你猜"}'})]
2020-06-22 08:48:09.510 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(9e086597) 解析到一条消息(Invocation{type='CHAT_SEND_RESPONSE', message='{"code":0,"msgId":"1"}'})]
2020-06-22 08:48:09.511 INFO 57583 --- [ool-1-thread-69] c.i.s.l.n.m.c.ChatSendResponseHandler : [execute][发送结果:ChatSendResponse{msgId='1', code=0, message='null'}]
2020-06-22 08:48:35.148 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(9e086597) 编码了一条消息(Invocation{type='CHAT_SEND_TO_ONE_REQUEST', message='{toUser: "tutou", msgId: "1", content: "你猜"}'})]
2020-06-22 08:48:35.150 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(9e086597) 解析到一条消息(Invocation{type='CHAT_SEND_RESPONSE', message='{"code":0,"msgId":"1"}'})]
2020-06-22 08:48:35.150 INFO 57583 --- [ool-1-thread-70] c.i.s.l.n.m.c.ChatSendResponseHandler : [execute][发送结果:ChatSendResponse{msgId='1', code=0, message='null'}]
// 服务端 ...
2020-06-22 08:48:35.149 INFO 56998 --- [ntLoopGroup-3-2] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(791f122b) 解析到一条消息(Invocation{type='CHAT_SEND_TO_ONE_REQUEST', message='{toUser: "tutou", msgId: "1", content: "你猜"}'})]
2020-06-22 08:48:35.149 INFO 56998 --- [ntLoopGroup-3-2] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(791f122b) 编码了一条消息(Invocation{type='CHAT_SEND_RESPONSE', message='{"code":0,"msgId":"1"}'})]
2020-06-22 08:48:35.149 INFO 56998 --- [ntLoopGroup-3-3] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(79cb3a1e) 编码了一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"你猜","msgId":"1"}'})]
// 客户端 B...(秃头)
2020-06-22 08:48:18.277 INFO 59613 --- [ntLoopGroup-2-1] c.i.s.l.n.c.handler.NettyClientHandler : [userEventTriggered][发起一次心跳]
2020-06-22 08:48:18.278 INFO 59613 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationEncoder : [encode][连接(24fbc3e8) 编码了一条消息(Invocation{type='HEARTBEAT_REQUEST', message='{}'})]
2020-06-22 08:48:18.280 INFO 59613 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(24fbc3e8) 解析到一条消息(Invocation{type='HEARTBEAT_RESPONSE', message='{}'})]
2020-06-22 08:48:18.281 INFO 59613 --- [pool-1-thread-4] c.i.s.l.n.m.h.HeartbeatResponseHandler : [execute][收到连接(24fbc3e8) 的心跳响应]
2020-06-22 08:48:35.150 INFO 59613 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(24fbc3e8) 解析到一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"你猜","msgId":"1"}'})]
2020-06-22 08:48:35.151 INFO 59613 --- [pool-1-thread-5] l.n.m.c.ChatRedirectToUserRequestHandler : [execute][收到消息:ChatRedirectToUserRequest{msgId='1', content='你猜'}]
9. 群聊逻辑
群聊的过程,如下图所示:

群聊流程
服务端负责将客户端 A 发送的群聊消息,转发给客户端 A、B、C。
“
友情提示:思考到逻辑简洁,艿艿提供的本小节的示例,并不是一个一个群,而是所有人在一个大的群聊中哈~
9.1 ChatSendToAllRequest
创建 ChatSendToOneRequest 类,发送给所有人的群聊消息的请求。代码如下:
public class ChatSendToAllRequest implements Message {
public static final String TYPE = "CHAT_SEND_TO_ALL_REQUEST";
/**
* 消息编号
*/
private String msgId;
/**
* 内容
*/
private String content;
// ... 省略 setter、getter、toString 方法
}
“
友情提示:如果是正经的群聊,会有一个 groupId 字段,表明群编号。
9.2 ChatSendResponse
和「8.2 ChatSendResponse」小节一致。
9.3 ChatRedirectToUserRequest
和「8.3 ChatRedirectToUserRequest」小节一致。
9.4 ChatSendToAllHandler
“
服务端…
创建 ChatSendToAllHandler 类,为服务端处理客户端的群聊请求。代码如下:
@Component
public class ChatSendToAllHandler implements MessageHandler<ChatSendToAllRequest> {
@Autowired
private NettyChannelManager nettyChannelManager;
@Override
public void execute(Channel channel, ChatSendToAllRequest message) {
// <1> 这里,假装直接成功
ChatSendResponse sendResponse = new ChatSendResponse().setMsgId(message.getMsgId()).setCode(0);
channel.writeAndFlush(new Invocation(ChatSendResponse.TYPE, sendResponse));
// <2> 创建转发的消息,并广播发送
ChatRedirectToUserRequest sendToUserRequest = new ChatRedirectToUserRequest().setMsgId(message.getMsgId())
.setContent(message.getContent());
nettyChannelManager.sendAll(new Invocation(ChatRedirectToUserRequest.TYPE, sendToUserRequest));
}
@Override
public String getType() {
return ChatSendToAllRequest.TYPE;
}
}
代码比较简单,胖友看看 <1>、<2> 上的注释。
9.5 ChatSendResponseHandler
和「8.5 ChatSendResponseHandler」小节一致。
9.6 ChatRedirectToUserRequestHandler
和「8.6
ChatRedirectToUserRequestHandler」小节一致。
9.7 简单测试
① 启动 Netty Server 服务端。
② 启动 Netty Client 客户端 A。然后使用 Postman 模拟一次认证请求(用户为 yunai)。如下图所示:

Postman 模拟认证请求
③ 启动 Netty Client 客户端 B。注意,需要设置 –server.port 端口为 8081,避免冲突。

IDEA 设置
④ 启动 Netty Client 客户端 C。注意,需要设置 –server.port 端口为 8082,避免冲突。

IDEA 设置
⑤ 最后使用 Postman 模拟一次发送群聊消息。如下图所示:

Postman 模拟群聊请求
同时,可以看到客户端 A 群发给所有客户端的日志如下:
// 客户端 A...
2020-06-22 08:55:44.898 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(9e086597) 编码了一条消息(Invocation{type='CHAT_SEND_TO_ALL_REQUEST', message='{msgId: "2", content: "广播消息"}'})]
2020-06-22 08:55:44.901 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(9e086597) 解析到一条消息(Invocation{type='CHAT_SEND_RESPONSE', message='{"code":0,"msgId":"2"}'})]
2020-06-22 08:55:44.901 INFO 57583 --- [ol-1-thread-148] c.i.s.l.n.m.c.ChatSendResponseHandler : [execute][发送结果:ChatSendResponse{msgId='2', code=0, message='null'}]
2020-06-22 08:55:44.901 INFO 57583 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(9e086597) 解析到一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"广播消息","msgId":"2"}'})]
2020-06-22 08:55:44.903 INFO 57583 --- [ol-1-thread-149] l.n.m.c.ChatRedirectToUserRequestHandler : [execute][收到消息:ChatRedirectToUserRequest{msgId='2', content='广播消息'}]
// 服务端...
2020-06-22 08:55:44.898 INFO 56998 --- [ntLoopGroup-3-2] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(791f122b) 解析到一条消息(Invocation{type='CHAT_SEND_TO_ALL_REQUEST', message='{msgId: "2", content: "广播消息"}'})]
2020-06-22 08:55:44.901 INFO 56998 --- [ntLoopGroup-3-2] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(791f122b) 编码了一条消息(Invocation{type='CHAT_SEND_RESPONSE', message='{"code":0,"msgId":"2"}'})]
2020-06-22 08:55:44.901 INFO 56998 --- [ntLoopGroup-3-2] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(791f122b) 编码了一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"广播消息","msgId":"2"}'})]
2020-06-22 08:55:44.901 INFO 56998 --- [ntLoopGroup-3-3] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(79cb3a1e) 编码了一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"广播消息","msgId":"2"}'})]
2020-06-22 08:55:44.901 INFO 56998 --- [ntLoopGroup-3-4] c.i.s.l.n.codec.InvocationEncoder : [decode][连接(9dc03826) 编码了一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"广播消息","msgId":"2"}'})]
// 客户端 B...
2020-06-22 08:55:44.902 INFO 59613 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(24fbc3e8) 解析到一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"广播消息","msgId":"2"}'})]
2020-06-22 08:55:44.902 INFO 59613 --- [ool-1-thread-83] l.n.m.c.ChatRedirectToUserRequestHandler : [execute][收到消息:ChatRedirectToUserRequest{msgId='2', content='广播消息'}]
// 客户端 C...
2020-06-22 08:55:44.901 INFO 61597 --- [ntLoopGroup-2-1] c.i.s.l.n.codec.InvocationDecoder : [decode][连接(9128c71c) 解析到一条消息(Invocation{type='CHAT_REDIRECT_TO_USER_REQUEST', message='{"content":"广播消息","msgId":"2"}'})]
2020-06-22 08:55:44.903 INFO 61597 --- [ool-1-thread-16] l.n.m.c.ChatRedirectToUserRequestHandler : [execute][收到消息:ChatRedirectToUserRequest{msgId='2', content='广播消息'}]


