5. 断开重连
Netty 客户端需要实现断开重连机制,解决各种情况下的断开情况。例如说:
- Netty 客户端启动时,Netty 服务端处于挂掉,导致无法连接上。
- 在运行过程中,Netty 服务端挂掉,导致连接被断开。
- 任一一端网络抖动,导致连接异常断开。
具体的代码实现比较简单,只需要在两个地方增加重连机制。
- Netty 客户端启动时,无法连接 Netty 服务端时,发起重连。
- Netty 客户端运行时,和 Netty 断开连接时,发起重连。
考虑到重连会存在失败的情况,我们采用定时重连的方式,避免占用过多资源。
5.1 具体代码
① 在 NettyClient 中,提供 #reconnect() 方法,实现定时重连的逻辑。代码如下:
// NettyClient.javapublic 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@Overridepublic 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 消息处理器,在收到客户端的心跳请求时,回复客户端一条确认消息。代码如下:
@Componentpublic class HeartbeatRequestHandler implements MessageHandler { 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 类,为服务端处理客户端的认证请求。代码如下:
@Componentpublic class AuthRequestHandler implements MessageHandler { @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 类,为客户端处理服务端的认证响应。代码如下:
@Componentpublic class AuthResponseHandler implements MessageHandler { 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 类,为服务端处理客户端的私聊请求。代码如下:
@Componentpublic class ChatSendToOneHandler implements MessageHandler { @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 类,为客户端处理服务端的聊天响应。代码如下:
@Componentpublic class ChatSendResponseHandler implements MessageHandler { 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 类,为客户端处理服务端的转发消息的请求。代码如下:
@Componentpublic class ChatRedirectToUserRequestHandler implements MessageHandler { 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 类,为服务端处理客户端的群聊请求。代码如下:
@Componentpublic class ChatSendToAllHandler implements MessageHandler { @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='广播消息'}]