[Netty实践] 简单聊天实现(四):Server集群改造

目录

一、介绍

二、解决方案

三、server端改造

五、客户端改造

四、测试

五、拓展


一、介绍

本章是拓展内容,主要实现的是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将不会受到无用消息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值