[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将不会受到无用消息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/478472.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[Linux]多线程(在Linux中的轻量级进程(LWP),怎么使用线程(接口))

目录 一、在Linux中的轻量级进程&#xff08;LWP&#xff09; 二、多线程的接口 1.创建线程&#xff08;pthread_create&#xff09; 2.线程ID&#xff08;pthread_self&#xff09; 3.线程终止 终止某个线程而不终止整个进程的三种方法&#xff1a; return pthread_…

Python Windows系统 虚拟环境使用

目录 1、安装 2、激活 3、停止 1、安装 1&#xff09;为项目新建一个目录&#xff08;比如&#xff1a;目录命名为learning_log&#xff09; 2&#xff09;在终端中切换到这个目录 3&#xff09;执行命令&#xff1a;python -m venv ll_env&#xff0c;即可创建一个名为ll…

基于ssm的医院住院管理系统论文

摘 要 随着时代的发展&#xff0c;医疗设备愈来愈完善&#xff0c;医院也变成人们生活中必不可少的场所。如今&#xff0c;已经2021年了&#xff0c;虽然医院的数量和设备愈加完善&#xff0c;但是老龄人口也越来越多。在如此大的人口压力下&#xff0c;医院住院就变成了一个问…

YOLOv2学习

YOLOv2学习 引入 Anchor boxes摘要数据集组合方法&#xff08;Dataset Combination Method&#xff09;联合训练算法&#xff08;Joint Training Algorithm&#xff09;改进Batch NormalizationHigh Resolution Classifier分类器预训练分辨率调整**Convolutional With Anchor B…

鸿蒙Harmony应用开发—ArkTS-全局UI方法(警告弹窗)

显示警告弹窗组件&#xff0c;可设置文本内容与响应回调。 说明&#xff1a; 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 本模块功能依赖UI的执行上下文&#xff0c;不可在UI上下文不明确的地方使用&#xff0c;参见UI…

云手机为电商提供五大出海优势

出海电商行业中&#xff0c;各大电商平台的账号安全是每一个电商运营者的重中之重&#xff0c;账号安全是第一生产力&#xff0c;也是店铺运营的基础。因此多平台多账号的防关联管理工具成了所有电商大卖家的必备工具。云手机最核心的优势就是账户安全体系&#xff0c;本文将对…

网络仿真3-NS2协议修改和移植

Ns2实现原理 OTcl与C关联 执行路径&#xff1a;Tcl->Otcl->C 返回路径&#xff1a;C->Otcl->Tcl NS2协议修改和移植 NS2中的各种网络协议在底层通过C实现&#xff0c;在网络仿真过程中最终通过调用底层C代码实现网络行为、算法、功能等各种仿真 NS2协议修改&…

vue3+threejs新手从零开发卡牌游戏(三):尝试在场景中绘制一张卡牌

首先我们思考下&#xff0c;一张最简单的卡牌有哪些东西构成&#xff1a;卡牌样式和卡牌数据。一张卡牌有正面和背面&#xff0c;有名称、属性、种族、攻击力等数据&#xff0c;我们先不考虑数据&#xff0c;先尝试在场景中绘制一张卡牌出来。 一、寻找卡牌素材 为了简单我直…

2024第二届经济管理、贸易与信息技术创新国际会议(ICEMITI2024)

2024第二届经济管理、贸易与信息技术创新国际会议&#xff08;ICEMITI2024&#xff09; 会议简介 2024第二届经济管理、贸易与信息技术创新国际会议&#xff08;ICEMITI2024&#xff09;将在中国郑州举行。这是一个重要的学术会议&#xff0c;旨在讨论当前经济管理、贸易和信…

python中类的导入与使用

1、类的介绍 与C中面向对象思想类似&#xff0c;有时候为了方便&#xff0c;需要专门创建一个类&#xff0c;将相关的函数全部写入到该类中&#xff0c;方便后续创建对象&#xff0c;再使用类中函数。那么如何创建完类&#xff0c;在其他文件中使用类中函数&#xff0c;这是这篇…

Design Script 官方案例解析3:函数

在本练习中,我们将创建一个通用定义,该定义将通过输入的点列表创建球体。这些球体的半径由每个点的 Z 特性驱动。 首先从 0 到 100 的十个值范围开始。将这些值插入 Point.ByCoordinates 节点,以创建对角线。 创建代码块,然后使用一行代码引入定义:def sphereByZ(inputPt)…

“找不到msvcr100.dll”或“msvcr100.dll丢失”的多种解决方法分享

当计算机系统中msvcr100.dll文件发生丢失时&#xff0c;导致某些应用程序无法正常运行。msvcr100.dll是Microsoft Visual C Redistributable Package的一部分&#xff0c;对于许多基于Windows操作系统的应用程序正常运行至关重要。小编将介绍5种解决msvcr100.dll丢失问题的方法…

LeetCode-热题100:17.电话号码的字母组合

题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a; digits “23” 输出&a…

云打印和无线打印有什么区别?

近段时间&#xff0c;云打印的概念越来越火热&#xff0c;而与此同时&#xff0c;无线打印也逐渐被大家所接受。那么云打印和无线打印有什么区别&#xff1f;今天带大家来了解一下。 云打印和无线打印有什么区别&#xff1f; 想要了解云打印和无线打印的区别&#xff0c;首先我…

org.springframework.boot:type=Admin,name=SpringApplication异常

javax.management.InstanceNotFoundException: org.springframework.boot:typeAdmin,nameSpringApplication 问题描述&#xff1a; IDEA 新建 SpringBoot 项目&#xff0c;启动时后台报错&#xff1a; javax.management.InstanceNotFoundException: org.springframework.boot…

图论必备:Dijkstra、Floyd与Bellman-Ford算法在最短路径问题中的应用

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;アンビバレント—Uru 0:24━━━━━━️&#x1f49f;──────── 4:02 &#x1f504; ◀️ ⏸ ▶️ ☰ …

电脑文件msvcp100.dll丢失原因,如何快速修复msvcp100.dll

电脑文件msvcp100.dll丢失原因&#xff0c;最近有朋友在问这个&#xff0c;显然会问这个的人&#xff0c;一般都是遇到了msvcp100.dll丢失的问题了&#xff0c;今天我们就来详细的给大家说说msvcp100.dll这个文件吧&#xff0c;我们只有了解了msvcp100.dll这个文件&#xff0c;…

uniapp 云开发省钱之调整函数执行内存大小

我这个5块钱一个月的服务空间配置&#xff1a; 现在还只有少量的用户和自己测试之用&#xff0c;目前消耗的情况&#xff1a; 云函数的使用量还是挺高的&#xff0c;目前还是正好能覆盖一个月的使用量&#xff0c;等用户量上来肯定是不行的&#xff0c;所以得想想办法压榨一下云…

Docker 笔记(七)--打包软件生成镜像

目录 1. 背景2. 参考3. 文档3.1 使用docker container commit命令构建镜像3.1.1 [Docker官方文档-docker container commit](https://docs.docker.com/reference/cli/docker/container/commit/)Description&#xff08;概述&#xff09;Options&#xff08;选项&#xff09;Exa…

软考 网络工程师 每日学习打卡 2024/3/21

学习内容 第8章 网络安全 本章主要讲解网络安全方面的基础知识和应用技术。针对考试应该掌握诸如数据加密、报文认 证、数字签名等基本理论&#xff0c;在此基础上深入理解网络安全协议的工作原理&#xff0c;并能够针对具体的 网络系统设计和实现简单的安全解决方案。 本章共有…