Netty 聊天室项目案例

news2024/11/15 11:37:24

1. 登入

  1. 在连接建立好之后,客户端发送登入,将登入消息封装为LoginRequestMessage这个类的对象, ctx.writeAndFlush(loginRequestMessage);)使用ctx发送,注意入站处理器调用写相关方法,会触发出站处理器(从最后向前找)。
/**
 * 在连接建立好之后 触发active事件
 * @param ctx
 * @throws Exception
 */
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    //负责接收用户在控制台的输入,负责向服务器发送各种消息
    //改线程可以和netty的线程不相关联(不使用event loop group中的线程),独立接收输入
    new Thread(() -> {
        System.out.println("请输入用户名:");
        String username = scanner.nextLine();
        System.out.println("请输入密码");
        String password = scanner.nextLine();
        //应该校验用户名密码是否为空,这里省略

        LoginRequestMessage loginRequestMessage = new LoginRequestMessage(username, password);
        System.out.println(loginRequestMessage);
        //发送消息
        ctx.writeAndFlush(loginRequestMessage);//入站处理器调用写相关方法,会触发出站处理器(从最后向前找)
        System.out.println("等待后续操作...");

    },"system in");
}
  1. 用户发送完毕自己的登入用户名和密码后,客户端应该进行校验。但是校验代码比较长,直接使用ch.pipeline().addLast(new xxxHandler());的方式代码看起来比较冗余。因此将验证用户名和密码的代码直接封装到一个Handler类中:LoginRequestMessageHandler,在外部new出该实例,加入到pipeline中即可。该handler读取client传过来的消息(入栈处理器,解码),再写出登入成功、失败的消息(出栈处理器,编码)。

注意:SimpleChannelInboundHandler<LoginRequestMessage>
该handler只处理LoginRequestMessage,意思就是client发送再多种消息,当前这个handler只处理LoginRequestMessage这一种请求登入的消息,进行用户名密码验证。这样好处就是,不需要将所有类型的消息接收到,在判断是不是请求登入的消息,再进行处理。

@ChannelHandler.Sharable
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
        String username = msg.getUsername();
        String password = msg.getPassword();
        boolean login = UserServiceFactory.getUserService().login(username, password);//后期可以优化到数据库中查询,这里简单在内存中查
        LoginResponseMessage message;
        if (login) {
            SessionFactory.getSession().bind(ctx.channel(), username);
            message = new LoginResponseMessage(true, "登录成功");
        } else {
            message = new LoginResponseMessage(false, "用户名或密码不正确");
        }
        ctx.writeAndFlush(message);
    }
}

之后客户端new出这个类的实例,加入到pipeline中 socketChannel.pipeline().addLast(LOGIN_REQMSG_HANDLER);

  1. server发送登入验证之后,服务器要接收这个消息,加一个入站处理器,实现接收server的响应消息。
    • 注意,这里不能再在channelActive下面处理接收消息了,因为channelActive是连接建立完成之后才会触发的。应该加一个channelRead事件
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    //对比学习,LoginResponseMessage发送的类对应的是SimpleChannelInboundHandler<LoginRequestMessage>
    // 其只对应LoginReqMeg一种消息进行处理,现在这个handler是对所有的msg都进行处理,所以要判断类型。
    if (msg instanceof LoginResponseMessage){
        LoginResponseMessage loginResponseMessage = (LoginResponseMessage) msg;
        log.info("{}",loginResponseMessage);
        if (loginResponseMessage.isSuccess()){
            //多线程的原子性操作!
            SUCCESS_LOGIN.set(true);
        }
        // 唤醒 system in 线程
        WAIT_FOR_LOGIN.countDown();
    }
}
  1. 重点 ,实现线程之间的通信, 主要使用:CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1) 配合WAIT_FOR_LOGIN.countDown();就可以唤醒线程;WAIT_FOR_LOGIN.await();进入等待(await就是等待计数器变为0)。
  • CountDownLatch是一个同步工具类,它通过一个计数器来实现的,初始值为线程的数量。每当一个线程完成了自己的任务,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程就可以恢复执行任务。

在这里插入图片描述
应用场景:

  1. 某个线程需要在其他n个线程执行完毕后再向下执行(本项目2个线程例子)
  2. 多个线程并行执行同一个任务,提高响应速度(其他多线程在任务执行完毕后,await唤醒主线程,可以接下去做别的任务)

本项目中:
在client输入登入的用户名,密码后,线程要停下来await(释放了cpu),等待server验证登入结果。
如果server那边验证通过了,client线程放行,选择聊天室场景。
如果server验证失败,client这边线程也要继续,但是可以关闭管道。
上面就涉及两个线程之间的通信,一个netty的nio线程,一个自定义线程,需要一个线程等待。

2. client端进行 聊天场景选择

之前完成了登入,现在完成登入之后,要进行聊天场景的配置,分别有:单聊、小组聊天、创建群聊、查看小组成员、退出群聊、加入群聊等。

 // 如果登录失败
if (!LOGIN.get()) {
    ctx.channel().close();
    return;
}
while (true) {
    System.out.println("==================================");
    System.out.println("send [username] [content]");
    System.out.println("gsend [group name] [content]");
    System.out.println("gcreate [group name] [m1,m2,m3...]");
    System.out.println("gmembers [group name]");
    System.out.println("gjoin [group name]");
    System.out.println("gquit [group name]");
    System.out.println("quit");
    System.out.println("==================================");
    String command = null;
    try {
        command = scanner.nextLine();
    } catch (Exception e) {
        break;
    }
    if(EXIT.get()){
        return;
    }
  • 即:要求用户按照上面的格式进行输入,因为scanner.nextLine()拿到的是一个字符串,就需要把这个字符串进行解析,以空格进行分割,第一个关键字代表聊天的性质(通过这个选择发送消息的封装格式),第二个关键字是名称,第三个关键字依次类推。

下面站在client角度进行场景分析:

2.1 场景一:ChatRequestMessage 请求聊天信息

最关键的消息是从 当前用户 发送着,接收消息者,消息内容
在这里插入图片描述
从字符串中解析出的字段一,进行配置,write写出消息,触发出站操作,对该消息进行自定义协议封装。

在这里插入图片描述

2.2 场景二 :GroupChatRequestMessage 请求给某个群聊发送消息

主要看构造方法
在这里插入图片描述在这里插入图片描述

2.3 场景三: GroupCreateRequestMessage 请求创建群聊

  1. 对应输入System.out.println(“gcreate [group name] [m1,m2,m3…]”);
  2. 解析输入,第三个字段是用户,创建群里用户应该去重,可以放在set集合中
    在这里插入图片描述

2.4 场景四: GroupMembersRequestMessage获取群里的成员

在这里插入图片描述

2.5 场景五、六:GroupJoinRequestMessage、GroupQuitRequestMessage 加入、退出群聊

在这里插入图片描述

  • 该模块client整体代码
// 在连接建立后触发 active 事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    // 负责接收用户在控制台的输入,负责向服务器发送各种消息
    new Thread(() -> {
        System.out.println("请输入用户名:");
        String username = scanner.nextLine();
        if(EXIT.get()){
            return;
        }
        System.out.println("请输入密码:");
        String password = scanner.nextLine();
        if(EXIT.get()){
            return;
        }
        // 构造消息对象
        LoginRequestMessage message = new LoginRequestMessage(username, password);
        System.out.println(message);
        // 发送消息
        ctx.writeAndFlush(message);
        System.out.println("等待后续操作...");
        try {
            WAIT_FOR_LOGIN.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 如果登录失败
        if (!LOGIN.get()) {
            ctx.channel().close();
            return;
        }
        while (true) {
            System.out.println("==================================");
            System.out.println("send [username] [content]");
            System.out.println("gsend [group name] [content]");
            System.out.println("gcreate [group name] [m1,m2,m3...]");
            System.out.println("gmembers [group name]");
            System.out.println("gjoin [group name]");
            System.out.println("gquit [group name]");
            System.out.println("quit");
            System.out.println("==================================");
            String command = null;
            try {
                command = scanner.nextLine();
            } catch (Exception e) {
                break;
            }
            if(EXIT.get()){
                return;
            }
            String[] s = command.split(" ");
            switch (s[0]){
                case "send":
                    ctx.writeAndFlush(new ChatRequestMessage(username, s[1], s[2]));
                    break;

                case "gsend":
                    ctx.writeAndFlush(new GroupChatRequestMessage(username, s[1], s[2]));
                    break;
                case "gcreate":
                    Set<String> set = new HashSet<>(Arrays.asList(s[2].split(",")));
                    set.add(username); // 加入自己
                    ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], set));
                    break;
                case "gmembers":
                    ctx.writeAndFlush(new GroupMembersRequestMessage(s[1]));
                    break;
                case "gjoin":
                    ctx.writeAndFlush(new GroupJoinRequestMessage(username, s[1]));
                    break;
                case "gquit":
                    ctx.writeAndFlush(new GroupQuitRequestMessage(username, s[1]));
                    break;
                case "quit":
                    ctx.channel().close();
                    return;
            }
        }
    }, "system in").start();
}

3. 服务器端对client各个聊天场景请求进行回应

3.1 封装思想

  • 为了简洁,将每个入站处理器封装到类中,都继承:extends SimpleChannelInboundHandler<XXXXRequestMessage>,这个每个client请求消息都能直接对应到目标的入站处理器,无需在channelRead方法之内做判断后分配。
    在这里插入图片描述
  • 技巧:
    可以在写好匿名内部类之后使用idea进行重构,先构建成为独立的内部类
    在这里插入图片描述
    在将内部类提出到独立的文件中
    在这里插入图片描述

3.2 通信逻辑

  • 现在例如用户A要给用户B发消息:clientA->server->clientB
    • 在每个入站处理器内,需要先找到client发送目的地对应的channel,如果在,说明通信的对方clientB在线,那server就能够成功发送消息给B,之间的桥梁就是:server和clientB之间的channel,只要clientB和server连接着,那么这个channel就在,就可以成功把消息发过去
    • 那如何判断channel到底还在不在?------>之前在每个用户登入的时候,就已经将用户名对应的channel绑定,并把channel放到session中,后期需要这个channel就可以用session直接根据用户名获取即可。如果发现session中没有目标的channel,那说明通信对方clientB还没和服务器连起来,发送失败!
    • 注意,上面说的session是自己定义的,主要由模板方法设计思想,Session是自己写的接口,不是HttpServlet在Web开发中的那个session。自定义的这个session底层使用ConcurrentHashMap来实现。
      在这里插入图片描述
      上图是绑定用户名和channel到session中,下图是获取对应的channel进行消息发送在这里插入图片描述

3.3 具体场景之:向群聊发送消息 GroupChatRequestMessage

  • client向某个群聊发送消息后,server这边收到需要向这个群聊的每一个成员都发送消息,即需要拿到每个成员与server之间对应的channel,使用channel 发送消息。在这里插入图片描述
  • 如何通过群聊的名称获取群聊每个组员的channel?—相关方法已经封装到了模板方法中
    • 先通过群聊名称拿到群聊中的组员名字
    @Override
    public Set<String> getMembers(String name) {
        return groupMap.getOrDefault(name, Group.EMPTY_GROUP).getMembers();
    }
- 再通过每个组员名字的set集合拿到channel集合,使用了stream流进行操作
    @Override
    public List<Channel> getMembersChannel(String name) {
        return getMembers(name).stream()
                .map(member -> SessionFactory.getSession().getChannel(member))
                .filter(Objects::nonNull) //处理还在线的
                .collect(Collectors.toList());
    }

4. 持续更新中…

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

金升阳|过压保护是什么意思?过压保护电路的构建

过压保护是指在电路中加入一种保护措施&#xff0c;以避免电路过压而导致器件损坏、安全事故等情况的发生。在实际工程中&#xff0c;过压保护电路通常由过压检测电路和过压保护器件组成。本文将详细介绍过压保护的原理、过压保护电路的构建方法、常见的过压保护器件以及应用实…

【LLMs 入门实战 】第二式:MiniGPT4 模型学习与实战

2023年4月17日&#xff0c;多模态问答模型MiniGPT-4发布&#xff0c;实现了GPT-4里的宣传效果《MiniGPT-4: Enhancing Vision-language Understanding with Advanced Large Language Models》《MiniGPT-4&#xff1a;使用高级大语言模型增强视觉语言理解》 模型介绍模型架构微调…

如何在conda环境中正确地使用pip

导言&#xff1a; 在conda环境下使用pip安装库时&#xff0c;许多时候会出现一些奇怪的现象&#xff0c;即用pip安装完成后在该conda环境下却没有该库。本文将说明该问题出现的原因&#xff0c;修复方式和如何正确地在conda环境中使用pip。 问题现象 复现&#xff1a; 新建环…

1-html

一 HTML 初体验 1 HTML 定义 HTML 超文本标记语言——HyperText Markup Language。 超文本是什么&#xff1f;链接标记是什么&#xff1f; 标记也叫标签&#xff0c;带尖括号的文本 2 标签语法 标签成对出现&#xff0c;中间包裹内容<>里面放英文字母&#xff08;标签…

Vim 自定义配色

本文首发于我的个人博客&#xff0c;欢迎点击访问&#xff0c;无广告节面简洁&#xff01; 最近重新开始学习Vim装上了WSL2&#xff0c;但发现Windows Terminal和vim的组合还是有很多问题需要解决的&#xff0c;由其默认的配色在某些状态下根本看不清字体&#xff0c;所以折腾…

小满vue3笔记(含源码解读)

第一章 1.mvvm架构 2.回顾vue2对比vue3 区别&#xff1a; vue2选项式api vue3组合式api 关于这两个的区别&#xff0c;你可以不准确的理解为&#xff0c;选项式api更贴近原生标准html文件结构&#xff1b; 而组合式api就像在html标签中写css&#xff1b;当然做了优化&…

最新大麦网抢票脚本-Python实战

学习时候的一个小例子&#xff0c;python挺有趣的&#xff0c;希望技术可以更进步 我也不多说啥了直接上图 系统:win10 python 版本:3.8.10 需要安装的库:selenium 安装方法: pip install selenium 抢H5版本也写了&#xff0c;但是速度有点慢2…5 就不发了 不如用autojs写感…

前端vue入门(纯代码)15

【16.Vue的过渡与动画】 1.点击切换按钮&#xff1a;实现某一元素的显示/隐藏 Test.vue文件中 <template><div><!-- 点击事件触发后&#xff0c;isShow取反 --><!-- 通过点击按钮让h1标签出现或者消失 --><button click"isShow !isShow&qu…

哈工大计算机网络课程网络层协议详解之:DHCP协议

哈工大计算机网络课程网络层协议详解之&#xff1a;DHCP协议 文章目录 哈工大计算机网络课程网络层协议详解之&#xff1a;DHCP协议如何获得IP地址&#xff1f;硬编码动态主机配置协议-DHCP&#xff1a;&#xff08;Dynamic Host Configuration Protocol&#xff09; 动态主机配…

设计模式篇(Java):前言(UML类图、七大原则)

编写软件过程中&#xff0c;程序员面临着来自耦合性&#xff0c;内聚性以及可维护性&#xff0c;可扩展性&#xff0c;重用性&#xff0c;灵活性等多方面的挑战&#xff0c;设计模式是为了让程序(软件)&#xff0c;具有更好&#xff1a; 代码重用性 (即&#xff1a;相同功能的…

Web自动化测试平台的设计与落地

目录 前言 一、目标和定位 二、平台特点 三、系统架构 四、相关技术栈 五、UI概览 六、待完善部分 总结&#xff1a; 前言 我最初开始接触Web自动化测试的时候&#xff0c;没有直接的领路人&#xff0c;测试行业知识也远不及如今这么丰富和易获取&#xff0c;当时我对于…

Hello算法学习笔记之搜索

一、二分查找 1.从数组中找到target的索引 注意&#xff1a;while条件是< O&#xff08;logn&#xff09; 二分查找并非适用于所有情况&#xff0c;原因如下&#xff1a; 二分查找仅适用于有序数据。若输入数据无序&#xff0c;为了使用二分查找而专门进行排序&#xff…

视频处理器对LED显示屏的作用

视频处理器在LED显示屏中扮演着重要的角色&#xff0c;其作用如下&#xff1a; 图像和视频信号处理&#xff1a;视频处理器负责对输入的图像和视频信号进行处理和优化&#xff0c;以确保在LED显示屏上呈现出高质量的图像和视频内容。它可以对图像进行去噪、锐化、色彩校正、亮度…

【数据结构】复杂度

目录 &#x1f4d6;什么是数据结构&#xff1f;&#x1f4d6;什么是算法&#xff1f;&#x1f4d6;算法效率&#x1f4d6;时间复杂度&#x1f516;大O的渐进表示法&#x1f516;常见时间复杂度计算举例&#x1f516;面试题&#xff1a;消失的数字 &#x1f4d6;空间复杂度&…

I2C协议应用(嵌入式学习)

I2C协议&应用 0. 前言1. 概念2. 特点&工作原理3. 应用示例代码模板HAL模板 0. 前言 I2C是Inter-Integrated Circuit的缩写&#xff0c;它是一种广泛使用的串行通信协议。它由飞利浦&#xff08;现在是NXP Semiconductors&#xff09;开发&#xff0c;并已成为各种电子…

无迹卡尔曼滤波在目标跟踪中的作用(一)

在前一节中&#xff0c;我们介绍了扩展卡尔曼滤波算法EKF在目标跟踪中的应用&#xff0c;其原理是 将非线性函数局部线性化&#xff0c;舍弃高阶泰勒项&#xff0c;只保留一次项 &#xff0c;这就不可避免地会影响结果的准确性&#xff0c;除此以外&#xff0c;实际中要计算雅各…

软件测试面试试卷,答对90%直接入职大厂

一&#xff0e;填空 1、 系统测试使用&#xff08; C &#xff09;技术, 主要测试被测应用的高级互操作性需求, 而无需考虑被测试应用的内部结构。 A、 单元测试 B、 集成测试 C、 黑盒测试 D、白盒测试 2、单元测试主要的测试技术不包括&#xff08;B &…

Linux 如何刷新 DNS 缓存

Linux 如何刷新 DNS 缓存 全文&#xff1a;如何刷新 DNS 缓存 (macOS, Linux, Windows) Unix Linux Windows 如何刷新 DNS 缓存 (macOS, FreeBSD, RHEL, CentOS, Debian, Ubuntu, Windows) 请访问原文链接&#xff1a;https://sysin.org/blog/how-to-flush-dns-cache/&#…

Elasticsearch:install

ElasticSearch Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。 Elasticsearch结合Kibana、Logstash、Beats&#xff0c;也就是elastic stack(ELK)。被广泛应用在日志分析、实时监控&#xff08;CPU、Memory、Program&#xff09;等领域。 elasticsearch是…