目录
一、介绍
本章是拓展内容,主要实现的是Server集群。
当系统的用户多了之后,单机Server资源有限,无法提供socket连接时,我们需要部署Server集群,当Server支持集群之后,将存在以下问题:每个用户连接的是不同的Server,比如,zhangsan用户连接的是ServerA,lisi用户连接的是ServerB,当zhangsan用户向lisi用户发送消息时,由于两个用户客户端与服务端通信的Channel处于不同的服务端,该如何确保能够正常通信呢?
二、解决方案
以下提供的解决方案作为一种思路,实际业务中,还需要考虑更加合适的方法。
本文将通过redis的发布/订阅模式实现跨服务通信,即ServerA与ServerB启动时将会订阅xxx频道,当zhangsan用户向lisi用户发送消息时,由于ServerA并没有与lisi用户通信的通道,所以ServerA将会将消息发送至xxx频道,所有订阅到XXX频道的Server将会受到消息,Server将会通过用户名找到是否有与lisi用户通信的Channel,有的话进行消息发送,无的话则不进行处理。
三、server端改造
1、为了方便Server端集成redis,在Sevrer模块引入web和redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.10.RELEASE</version>
</dependency>
2、配置文件application.yml,以下两组port,用户开启两个服务端
server:
port: 8200
#port: 8201
netty:
port: 8888
#port: 9999
3、新增ChatSeverApplication
@SpringBootApplication
public class ChatServerApplication implements CommandLineRunner {
@Value("${netty.port}")
private Integer nettyPort;
public static void main(String[] args) {
SpringApplication.run(ChatServerApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
new CharServer().bind(nettyPort);
}
}
4、配置RedisTemplate
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 序列化设置
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
SingleMessageRequestHandler.setRedisTemplate(template);
return template;
}
}
5、新增UserMessageListener,该类用户接收频道消息
@Slf4j
@Component
public class UserMessageListener implements MessageListener {
@Resource
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, byte[] bytes) {
// 返回的是bytes字节数组,需要反序列化
RedisSerializer<?> keySerializer = redisTemplate.getKeySerializer();
RedisSerializer<?> valueSerializer = redisTemplate.getValueSerializer();
log.info("----------UserMessageListener接收到发布者消息----------");
log.info("频道:{}", keySerializer.deserialize(message.getChannel()));
log.info("pattern:{}", new String(bytes));
SingleMessageRequest singleMessageRequest = (SingleMessageRequest) valueSerializer.deserialize(message.getBody());
log.info("消息内容:{}", singleMessageRequest);
log.info("---------------------------------");
// 处理消息
Channel channel = ChannelManager.getChannel(singleMessageRequest.getSendTo());
if(channel == null) {
log.info("用户不在本服务上登录, 结束处理 ,singleMessageRequest:{}", singleMessageRequest);
return;
}
log.info("进行消息转发");
SingleMessageResponse singleMessageResponse = new SingleMessageResponse(singleMessageRequest.getSendFrom(), singleMessageRequest.getContent());
channel.writeAndFlush(singleMessageResponse);
}
}
6、将监听器与频道进行绑定
@Configuration
public class RedisListenerConfig {
@Resource
private UserMessageListener userMessageListener;
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 添加监听类(处理订阅消息的监听类)
container.addMessageListener(userMessageListener, new ChannelTopic("user-message"));
return container;
}
}
7、改造SingleMessageHandler,如果用户Channel不存在,则将发布消息至user-message频道
@Component
public class SingleMessageRequestHandler extends BaseHandler<SingleMessageRequest> {
private static RedisTemplate<String, Object> redisTemplate;
@Override
protected void channelRead0(ChannelHandlerContext ctx, SingleMessageRequest msg) throws Exception {
System.out.println("[SingleMessageRequestHandler]读取到客户端消息, channel: " + ctx.channel());
System.out.println("消息是: " + msg);
// 向目标用户发送
Channel targetChannel = ChannelManager.getChannel(msg.getSendTo());
if(targetChannel == null) {
// 向"user-message"频道发送消息
redisTemplate.convertAndSend("user-message", msg);
} else {
SingleMessageResponse singleMessageResponse = new SingleMessageResponse(msg.getSendFrom(), msg.getContent());
targetChannel.writeAndFlush(singleMessageResponse);
}
}
public static void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
SingleMessageRequestHandler.redisTemplate = redisTemplate;
}
}
五、客户端改造
1、修改配置文件
server:
port: 8081
#port: 8082
netty:
server:
port: 8888
#port: 9999
2、修改ClientApplication
@SpringBootApplication
public class ClientApplication {
@Value("${netty.server.port}")
private Integer nettyServePort;
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
@Bean
public Channel init(){
return new ChatClient().connect("127.0.0.1", nettyServePort);
}
}
四、测试
1、根据server的application.yml,分别启动两个server端
1)启动ServerA,占用端口8888
server:
port: 8200
netty:
port: 8888
2024-01-03 17:12:50.177 INFO 5124 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8200 (http) with context path ''
2024-01-03 17:12:50.492 INFO 5124 --- [ main] o.r.chat.server.ChatServerApplication : Started ChatServerApplication in 2.157 seconds (JVM running for 2.689)
chatServer 启动成功...
2)启动ServerB,占用端口9999
server:
port: 8201
netty:
port: 9999
2024-01-03 17:14:27.507 INFO 6872 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8201 (http) with context path ''
2024-01-03 17:14:27.810 INFO 6872 --- [ main] o.r.chat.server.ChatServerApplication : Started ChatServerApplication in 2.003 seconds (JVM running for 2.463)
chatServer 启动成功...
2、根据client的application.yml,分别启动两个client端
1)启动客户端A,与服务端A进行连接
server:
port: 8081
netty:
server:
port: 8888
chatClient 启动...
客户端进行连接, channel: [id: 0x185a40a6, L:/127.0.0.1:51430 - R:/127.0.0.1:8888]
2024-01-03 17:15:44.167 INFO 13684 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2024-01-03 17:15:44.292 INFO 13684 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2024-01-03 17:15:44.298 INFO 13684 --- [ main] o.ricardo.chat.client.ClientApplication : Started ClientApplication in 1.517 seconds (JVM running for 1.959)
2)启动客户端B,与服务端B进行连接
server:
port: 8082
netty:
server:
port: 9999
chatClient 启动...
客户端进行连接, channel: [id: 0x39ee8d03, L:/127.0.0.1:51597 - R:/127.0.0.1:9999]
2024-01-03 17:18:51.839 INFO 13652 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2024-01-03 17:18:51.973 INFO 13652 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8082 (http) with context path ''
2024-01-03 17:18:51.979 INFO 13652 --- [ main] o.ricardo.chat.client.ClientApplication : Started ClientApplication in 1.544 seconds (JVM running for 1.999)
2、模拟登陆
1)客户端A模拟zhangsan用户登录
http://localhost:8081/login?username=zhangsan
[LoginResponseHandler]读取到服务端消息, channel: [id: 0x185a40a6, L:/127.0.0.1:51430 - R:/127.0.0.1:8888]
消息是: LoginResponse(result=true, message=登录成功!)
2)客户端B模拟lisi用户登录
http://localhost:8082/login?username=lisi
[LoginResponseHandler]读取到服务端消息, channel: [id: 0x39ee8d03, L:/127.0.0.1:51597 - R:/127.0.0.1:9999]
消息是: LoginResponse(result=true, message=登录成功!)
3、模拟zhangsan用户向lisi用户发送消息
http://localhost:8081/send/single?sendTo=lisi&content=nihao
1)由于ServerA上並沒有与lisi客户端通信的Channel,所以将消息发布到user-message频道上,由于ServerA与SeverB都订阅了user-message频道,所以两个服务都将受到消息
SeverA: 不进行消息转发处理
2024-01-03 17:22:30.411 INFO 5124 --- [ container-2] o.r.c.server.config.UserMessageListener : ----------UserMessageListener接收到发布者消息----------
2024-01-03 17:22:30.411 INFO 5124 --- [ container-2] o.r.c.server.config.UserMessageListener : 频道:user-message
2024-01-03 17:22:30.411 INFO 5124 --- [ container-2] o.r.c.server.config.UserMessageListener : pattern:user-message
2024-01-03 17:22:30.446 INFO 5124 --- [ container-2] o.r.c.server.config.UserMessageListener : 消息内容:SingleMessageRequest(sendFrom=zhangsan, sendTo=lisi, content=nihao)
2024-01-03 17:22:30.446 INFO 5124 --- [ container-2] o.r.c.server.config.UserMessageListener : ---------------------------------
2024-01-03 17:22:30.446 INFO 5124 --- [ container-2] o.r.c.server.config.UserMessageListener : 用户不在本服务上登录, 结束处理 ,singleMessageRequest:SingleMessageRequest(sendFrom=zhangsan, sendTo=lisi, content=nihao)
ServerB: lisi客户端Channel在SeverB上,进行消息转发
2024-01-03 17:22:30.411 INFO 6872 --- [ container-2] o.r.c.server.config.UserMessageListener : ----------UserMessageListener接收到发布者消息----------
2024-01-03 17:22:30.411 INFO 6872 --- [ container-2] o.r.c.server.config.UserMessageListener : 频道:user-message
2024-01-03 17:22:30.411 INFO 6872 --- [ container-2] o.r.c.server.config.UserMessageListener : pattern:user-message
2024-01-03 17:22:30.462 INFO 6872 --- [ container-2] o.r.c.server.config.UserMessageListener : 消息内容:SingleMessageRequest(sendFrom=zhangsan, sendTo=lisi, content=nihao)
2024-01-03 17:22:30.462 INFO 6872 --- [ container-2] o.r.c.server.config.UserMessageListener : ---------------------------------
2024-01-03 17:22:30.462 INFO 6872 --- [ container-2] o.r.c.server.config.UserMessageListener : 进行消息转发
2) lisi客户端收到消息
[SingleMessageResponseHandler]读取到服务端消息, channel: [id: 0x39ee8d03, L:/127.0.0.1:51597 - R:/127.0.0.1:9999]
消息是: SingleMessageResponse(sendFrom=zhangsan, content=nihao)
五、拓展
上诉案例通过redis的发布/订阅模式,将所有Server进行注册从而实现消息的转发,但是,存在以下问题不能忽视:
由于所有Sever都注册在同一个频道上,意味着所有Sever都将进行一次消息转发处理,即使不是自己能够处理的消息,当消息量上来之后,所有Server都将处理大量的无用消息,造成资源浪费,影响Sever性能,所以以下提供解决思路,仅供参考,可行与否自行验证。
解决方案步骤:
1、每个Sever端启动时,与一条MQ队列或redis频道进行订阅,注意,此MQ队列或redis频道是Server独有的,不共享,也就是,不同Sever将绑定不同的MQ队列或redis频道。
2、当有客户端与Server端进行连接通信时,将登录用户username记录到redis中,key为username,value为MQ队列名或redis频道名。同时记录一份在本地Sever服务缓存中。
3、当zhangsan用户向lisi用户发送消息时,如果lisi用户与zhangsan用户不在同一个Sever中(通过本地查找),则从redis中获取到lisi用户对应的MQ队列名或redis频道名,再将消息发送至lisi用户对应的MQ队列或redis频道中,当订阅了MQ队列或redis频道的Server获取到消息之后,再进一步进行处理,其他Server将不会受到无用消息。