Socket网络编程(六)——简易聊天室案例

目录

  • 聊天室数据传输设计
    • 客户端、服务器数据交互
    • 数据传输协议
    • 服务器、多客户端模型
    • 客户端如何发送消息到另外一个客户端
    • 2个以上设备如何交互数据?
  • 聊天室消息接收实现
    • 代码结构
    • client客户端重构
    • server服务端重构
      • 自身描述信息的构建
      • 重构TCPServer.java
      • 基于synchronized 解决多线程操作的安全问题
    • 聊天室Server/Client启动、测试
    • 源码下载

聊天室数据传输设计

  • 必要条件:客户端、服务器
  • 必要约束:数据传输协议
  • 原理:服务器监听消息来源、客户端链接服务器并发送消息到服务器

客户端、服务器数据交互

20240229-145006-Go.png

client 发送消息到服务器端,服务器端回复消息也就是回送消息。

数据传输协议

20240229-145145-zG.png

数据在传输的时候,需要在尾部追加换行符,也就是说原来5个字节的数据,在实际传输时,是有6个字节长度的。

服务器、多客户端模型

20240229-145351-SU.png
在客户端有多个情况下,客户端都会向服务器端进行发送消息;想要在PC发送消息给服务器端时,也让安卓、平板等终端都能收到,其操作应该是,当PC端发送一条消息到服务器端之后,服务器端得到该数据后,它会把这条数据发送(回送)给当前连接的客户端。而这些当前连接的客户端收到这条消息后,就实现了把PC消息发送到手机的过程。

20240229-145449-mB.png

客户端如何发送消息到另外一个客户端

每个客户端都是服务器也是客户端?
答:不是

2个以上设备如何交互数据?

答:约定一个基础的数据格式,这里使用回车换行符来作为信息的截断
客户端-服务器-转发到客户端,如下图:
20240229-145850-t9.png

User1发送消息到服务端,服务端将消息转发给其他的客户端(比如User2),从而实现聊天室的功能

聊天室消息接收实现

代码结构

20240229-170606-4z.png

代码分为四个module,分别为clink、constants、client、server。

  • clink:该module为提供工具类进行校验与流处理。
  • constants:基础的共用类代码
  • server:服务端代码,需要依赖 clink、constants两个module
  • client:客户端代码,需要依赖 clink、constants两个module

clink、constants的工具类,基础数据类参考前面 TCP点对点传输的代码逻辑

client客户端重构

初版代码和TCP点对点传输的基本一致,聊天室主要在TCPServer端进行转发,所以Client不需要代码重构。

server服务端重构

初版代码和TCP点对点传输的基本一致,要实现聊天室消息接收则需要进行重构。主要重构 TCPServer.java 、ClientHandler.java类。

ClientHandler.java - 消息转发
原有的消息在收到后就只是打印到控制台

// 打印到屏幕
System.out.println(str);

而实现聊天室功能需要将收到的消息进行通知出去。这里可以通过 CloseNotify() 接口进行实现。这里对该接口进行改造,并新增转发的接口方法来将消息通知回去。

    /*** 消息回调*/public interface ClientHandlerCallback {// 自身不安比通知void onSelfClosed(ClientHandler handler);// 收到消息时通知void onNewMessageArrived(ClientHandler handler,String msg);}

在将消息打印到屏幕的同时,将消息通知出去:

       // 打印到屏幕System.out.println(str);clientHandlerCallback.onNewMessageArrived(ClientHandler.this,str);

调用onNewMessageArrived()方法从而进行转发。这里主要是把当前收到的消息传递回去,同时也要把自身传递回去。

自身描述信息的构建

新增clientInfo类变量:

    private final String clientInfo;

自身描述信息初始化:

    public ClientHandler(Socket socket, ClientHandlerCallback clientHandlerCallback) throws IOException {this.socket = socket;this.readHandler = new ClientReadHandler(socket.getInputStream());this.writeHandler = new ClientWriteHandler(socket.getOutputStream());this.clientHandlerCallback = clientHandlerCallback;// 新增自身描述信息this.clientInfo = "A[" + socket.getInetAddress().getHostAddress() + "] P[" + socket.getPort() + "]";System.out.println("新客户端连接:" + clientInfo);}public String getClientInfo() {return clientInfo;}

重构TCPServer.java

重构 clientHandler.ClientHandlerCallback的两个回调方法,这里要将之提到TCPServer.java类上。

让TCPServer.java 实现 clientHandler.ClientHandlerCallback接口。并实现两个方法:

    @Overridepublic synchronized void onSelfClosed(ClientHandler handler) {}@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {}

并将 客户端构建溢出线程的remove操作迁移到 onSelfClosed() 方法实现内:

    @Overridepublic synchronized void onSelfClosed(ClientHandler handler) {clientHandlerList.remove(handler);}

原有的ClientHandler异步线程处理逻辑如下

        // 客户端构建异步线程ClientHandler clientHandler = new ClientHandler(client,handler -> clientHandlerList.remove(handler));

重构后,如下:

    // 客户端构建异步线程ClientHandler clientHandler = new ClientHandler(client,TCPServer.this);

消息转发

    /*** 转发消息给其他客户端* @param handler* @param msg*/@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {// 打印到屏幕System.out.println("Received-" + handler.getClientInfo() + ":" + msg);// 转发forwardingThreadPoolExecutor.execute(()->{for (ClientHandler clientHandler : clientHandlerList){if(clientHandler.equals(handler)){// 跳过自己continue;}// 向其他客户端投递消息clientHandler.send(msg);}});}

基于synchronized 解决多线程操作的安全问题

由于这里有对 clientHandlerList集合的删除、添加、遍历等操作,这涉及到对所有客户端的操作,在多线程的环境下,默认的List不是线程安全的,所以存在多线程的安全问题。

    public void stop() {if (mListener != null) {mListener.exit();}synchronized (TCPServer.this){for (ClientHandler clientHandler : clientHandlerList) {clientHandler.exit();}clientHandlerList.clear();}// 停止线程池forwardingThreadPoolExecutor.shutdownNow();}public synchronized void broadcast(String str) {for (ClientHandler clientHandler : clientHandlerList) {clientHandler.send(str);}}/*** 删除当前消息* @param handler*/@Overridepublic synchronized void onSelfClosed(ClientHandler handler) {clientHandlerList.remove(handler);}/*** 转发消息给其他客户端* @param handler* @param msg*/@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {// 打印到屏幕System.out.println("Received-" + handler.getClientInfo() + ":" + msg);// 转发}

这里加类锁来保证删除操作的线程安全。

关于添加操作的线程安全问题解决如下:

          try {// 客户端构建异步线程ClientHandler clientHandler = new ClientHandler(client,TCPServer.this);// 读取数据并打印clientHandler.readToPrint();// 添加同步处理synchronized (TCPServer.this) {clientHandlerList.add(clientHandler);}} catch (IOException e) {e.printStackTrace();System.out.println("客户端连接异常:" + e.getMessage());}

异步转发

        // 转发clientHandlerCallback.onNewMessageArrived(ClientHandler.this,str);

在ClientHandler.java中,上述代码所在的线程是主要线程,会一直有消息进来,所以不能做同步处理,那样会导致当前线程阻塞,从而导致后面进来的消息无法及时处理。

所以当 onNewMessageArrived()将消息抛出去之后,TCPServer.java的实现要采取异步转发的方式退给其他客户端。创建一个新的单例线程池来做转发的操作:

新增转发线程池:

    // 转发线程池private final ExecutorService forwardingThreadPoolExecutor;public TCPServer(int port) {this.port = port;this.forwardingThreadPoolExecutor = Executors.newSingleThreadExecutor();}

转发投递消息给其他客户端:

    /*** 转发消息给其他客户端* @param handler* @param msg*/@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {// 打印到屏幕System.out.println("Received-" + handler.getClientInfo() + ":" + msg);// 转发forwardingThreadPoolExecutor.execute(()->{synchronized (TCPServer.this){for (ClientHandler clientHandler : clientHandlerList){if(clientHandler.equals(handler)){// 跳过自己continue;}// 向其他客户端投递消息clientHandler.send(msg);}}});}

防止客户端下线后,依旧重复发送的问题:

ClientHandler.java - ClientWriteHandler

       /*** 发送到客户端* @param str*/void send(String str) {// 如果已经发送完成,就返回if(done){return;}executorService.execute(new WriteRunnable(str));}

聊天室Server/Client启动、测试

idea单个程序同时启动多个窗口的方法:

  1. 启动main方法
    20240301-171559-Pt.png

  2. 勾选运行运行多个
    20240301-171650-l3.png

  3. 保存退出就可以了

测试结果如下:

  1. 先启动服务端,再启动三个客户端
    20240301-171752-6g.png
    20240301-171809-S0.png

  2. 服务端和客户端发消息
    服务端发送:我是服务端
    客户端发送客户端1、客户端2、客户端3
    20240301-171954-5i.png
    20240301-172007-It.png

  3. 其中一个客户端退出,不影响其他客户端和服务端发送消息
    20240301-172133-bs.png

至此,socket简易,聊天室重构完成

源码下载

下载地址:https://gitee.com/qkongtao/socket_study/tree/master/src/main/java/cn/kt/socket/SocketDemo_chatroom

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

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

相关文章

Nginx多次代理后获取真实的用户IP访问地址

需求:记录用户操作记录,类似如下表格的这样 PS: 注意无论你的服务是Http访问还是Https 访问的都是可以的,我们服务之前是客户只给开放了一个端口,但是既要支持https又要支持http协议,nginx 是可以通过stream 模块配置双…

2023中国PostgreSQL数据库生态大会:洞察前沿趋势,探索无限可能(附核心PPT资料下载)

随着数字化浪潮的推进,数据库技术已成为支撑各行各业数字化转型的核心力量。2023中国PostgreSQL数据库生态大会的召开,无疑为业界提供了一个深入交流、共同探索PostgreSQL数据库技术未来发展趋势的平台。本文将带您走进这场盛会,解析大会的亮…

k8s Pod基础(概念,容器功能及分类,镜像拉取和容器重启策略)

目录 pod概念 Kubernetes设计Pod概念和特殊组成结构的用意 Pod内部结构: 网络共享: 存储共享: pause容器主要功能 pod创建方式 pod使用方式 pod分类 pod的容器分类 基础容器(infrastructure container)&…

元宇宙3D虚拟场景制作深圳华锐视点免费试用

随着元宇宙兴起,3D线上展厅得到了越来越多的关注和应用。基于VR虚拟现实技术的元宇宙3D线上展厅在线编辑系统,更是为企业在展览展示领域带来了前所未有的辅助。 高效便捷: 元宇宙3D线上展厅在线编辑无需复杂的施工和搭建过程,只需…

报错问题解决django.db.utils.OperationalError: (1049, “Unknown database ‘ mxshop‘“)

开发环境:ubuntu22.04 pycharm 功能:django连接使用mysql数据库,各项配置看似正常 报错: django.db.utils.OperationalError: (1049, "Unknown database mxshop") 分析检查原因: Setting的配置文件内&…

gcd+线性dp,[蓝桥杯 2018 国 B] 矩阵求和

一、题目 1、题目描述 经过重重笔试面试的考验,小明成功进入 Macrohard 公司工作。 今天小明的任务是填满这么一张表: 表有 �n 行 �n 列,行和列的编号都从 11 算起。 其中第 �i 行第 �j 个元素…

Spring MVC 和 Spring Cloud Gateway不兼容性问题

当启动SpringCloudGateway网关服务的时候,没注意好依赖问题,出现了这个问题: Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway. 解决办法就是:删除SpringMVC的依赖,即下列依赖。 &…

ChatGPT/GPT4科研应用与AI绘图及论文高效写作

原文:ChatGPT/GPT4科研应用与AI绘图及论文高效写作 第一:2024年AI领域最新技术 1.OpenAI新模型-GPT-5 2.谷歌新模型-Gemini Ultra 3.Meta新模型-LLama3 4.科大讯飞-星火认知 5.百度-文心一言 6.MoonshotAI-Kimi 7.智谱AI-GLM-4 第二:…

【C++从0到王者】第四十六站:图的深度优先与广度优先

文章目录 一、图的遍历二、广度优先遍历1.思想2.算法实现3.六度好友 三、深度优先遍历1.思想2.代码实现 四、其他问题 一、图的遍历 对于图而言,我们的遍历一般是遍历顶点,而不是边,因为边的遍历是比较简单的,就是邻接矩阵或者邻接…

《汇编语言》第3版 (王爽)检测点3.1解析

第三章 检测点3.1 (1).在Debug中,用“d 0:0 1f”查看内存,结果如下。 下面的程序执行前,AX 0,BX 0,写出每条汇编指令执行完后相关寄存器中的值。 mov ax,1 ;将1放入AX寄存器中,…

【零基础SRC】成为漏洞赏金猎人的第一课:加入玲珑安全漏洞挖掘班。

我们是谁 你是否对漏洞挖掘充满好奇?零基础或有基础但想更进一步?想赚取可观的漏洞赏金让自己有更大的自由度? 那么,不妨了解下我们《玲珑安全团队》。 玲珑安全团队,拥有多名实力讲师,均就职于互联网头…

一线互联网大厂中高级Android面试真题收录,记一次字节跳动Android社招面试

在开始回答前,先简单概括性地说说Linux现有的所有进程间IPC方式: 1. **管道:**在创建时分配一个page大小的内存,缓存区大小比较有限; 2. 消息队列:信息复制两次,额外的CPU消耗;不合…

指针与malloc动态内存申请,堆和栈的差异

定义了两个函数print_stack()和print_malloc(),分别演示了两种不同的内存分配方式:栈内存和堆内存。然后在main()函数中调用这两个函数,并将它们返回的指针打印出来。 由于print_stack()中的数组c是在栈上分配的,当函数返回后&…

Python装饰器的使用详解

目录 1、函数装饰器 1.1、闭包函数 1.2、装饰器语法 1.3、装饰带参数的函数 1.4、被装饰函数的身份问题 1.4.1、解决被装饰函数的身份问题 1.5、装饰器本身携带/传参数 1.6、嵌套多个装饰器 2、类装饰器 装饰器顾名思义作为一个装饰的作用,本身不改变被装…

Acwing 周赛135 解题报告 | 珂学家 | 反悔堆贪心

前言 整体评价 VP了这场比赛, T3挺有意思的,反悔贪心其实蛮套路的。 A. 买苹果 思路: 签到 n, x list(map(int, input().split())) print (n // x)B. 牛群 思路: 分类讨论 from collections import Counters input() cnt Counter(s)lists sorte…

WPF 【十月的寒流】学习笔记(2):MVVM中是怎么实现通知的

文章目录 前言相关链接代码仓库项目配置代码初始代码ViewPersonViewModel 尝试老办法通知解决方案ObservableCollectionBindingListICollectionView 总结 前言 我们这次详细了解一下列表通知的底层是怎么实现的 相关链接 十月的寒流 MVVM实战技巧之:可被观测的集合…

2024年【A特种设备相关管理(电梯)】考试总结及A特种设备相关管理(电梯)证考试

题库来源:安全生产模拟考试一点通公众号小程序 2024年A特种设备相关管理(电梯)考试总结为正在备考A特种设备相关管理(电梯)操作证的学员准备的理论考试专题,每个月更新的A特种设备相关管理(电梯…

C++:非静态成员默认初始化

C11之前只有常静态成员变量才能进行默认初始化,其它变量初始化时总要进行繁琐的过程 class A{int a; public:A():a(10){} };C11开始支持非静态成员的默认初始化,默认初始化和初始化参数列表同时初始化一个变量时会使用初始化参数列表,不进行…

JavaScript new、apply call 方法

new、apply、call、bind JavaScript 中的 apply、call和 bind 方法是前端代码开发中相当重要的概念,并且与 this 的指向密切相关 new new 关键词的主要作用 就是执行一个构造函数、返回一个实例对象 根据构造函数的情况,来确定是否可以接受参数的传递…

Huggingface初上手即ERNIE-gram句子相似性实战

大模型如火如荼的今天,不学点语言模型(LM)相关的技术实在是说不过去了。只不过由于过往项目用到LM较少,所以学习也主要停留在直面——动眼不动手的水平。Huggingface(HF)也是现在搞LM离不开的工具了。 出于…