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://xiahunao.cn/news/2822282.html

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

相关文章

Android 12.0 framework关于systemUI定制之导航栏透明背景的功能实现

1.概述 在12.0的系统rom产品定制化开发中,在对于系统原生SystemUI的导航栏背景在沉浸式导航栏的 情况下默认是会随着背景颜色的变化而改变的,在一些特定背景下导航栏的背景也是会改变的,所以由于产品开发需要 要求需要设置导航栏背景为透明的,所以就需要在Activity创建的时…

第十五天-爬虫项目实战

目录 1.介绍 2.代码 1.main.py 2.PageSider.py 3.DetailSpider.py 4.DataParse.py 5.Constant.py 6.HanderRequest.py 1.介绍 1. 使用多线程爬取网站 2.爬取数据后保存至excel 3.爬取网站(仅做测试)网创类项目爬取:https://www.maomp.com/ 4..实现效果 …

Java已死?凛冽寒风,刺骨现实

文章首发于公众号:职谷智享 2024年,寒风凛冽,不仅吹进了人们的衣领,也吹进了程序员的心里。曾经风光无限的Java,如今似乎正在走向末路。 招聘需求骤减,求职者如过江之鲫 猎聘网的数据显示,20…

Linux 任务进程命令练习

1、通过ps命令的两种选项形式查看进程信息 2、通过top命令查看进程 3、通过pgrep命令查看sshd服务的进程号 4、查看系统进程树 5、使dd if/dev/zero of/root/file bs1M count8190 命令操作在前台运行 6、将第5题命令操作调入到后台并暂停 7、使dd if/dev/zero of/root/file2 bs…

Flutter中Future和Stream关系

Future和Stream类是Dart异步编程的核心。 Future 表示一个不会立即完成的计算过程。与普通函数直接返回结果不同的是异步函数返回一个将会包含结果的 Future。该 Future 会在结果准备好时通知调用者。 Stream 是一系列异步事件的序列。其类似于一个异步的 Iterable,…

力扣SQL50 大的国家 查询

Problem: 595. 大的国家 Code select name,population,area from World where area > 3000000 or population > 25000000;

国外站群服务器科普:定义、用途与价值

在数字化时代,服务器扮演着至关重要的角色,而站群服务器则是其中的一种特殊形态。尤其对于需要在全球范围内进行业务部署的企业或个人来说,国外站群服务器成为了不可或缺的工具。那么,国外站群服务器究竟是什么呢?它有哪些用途呢…

【Web安全靶场】sqli-labs-master 54-65 Challenges 与62关二分法和like模糊搜索

sqli-labs-master 54-65 Challenges 其他关卡和靶场见专栏… 文章目录 sqli-labs-master 54-65 Challenges第五十四关-联合注入第五十五关-联合注入第五十六关-联合注入第五十七关-联合注入第五十八关-报错注入第五十九关-报错注入第六十关-报错注入第六十一关-报错注入第六十…

抖音视频评论批量采集软件|视频数据提取工具

开发背景: 随着抖音视频的流行和使用频率增加,用户对批量采集抖音视频评论的需求逐渐凸显。传统的下载方式效率低下,无法满足快速采集数据的要求。为了解决这一问题,我们开发了一款基于C#的抖音视频评论批量采集软件,旨…

React富文本编辑器开发(三)

现在我们的编辑器显示的内容很单一,这自然不是我们的目标,让呈现的内容多元化是我们的追求。这就需要让编辑器能够接收多元素的定义。从初始数据的定义我们可以推断数据的格式远不止一种,那么其它类型的数据如何定义及呈现的呢,我…

kohya_ss, stable diffusion 显示MaxRetryError: HTTPSConnectionPool()的解决方法

说白了就是访问huggingface.co下载模型、json配置失败,需要挂梯子。 然而有时候明明开了梯子,网页端可以访问google、huggingface.co却依然报上述错。 这时候说明没有开代理,执行的脚本没有连接上梯子对应的端口。 解决办法:手…

【MySQL】数据库中常用的函数

目录 聚合函数COUNT()函数的多种用法COUNT(*)COUNT(主键)COUNT(1)COUNT(常量)COUNT(非主键)COUNT(distinct(字段)) COUNT()函数小结 字符函数length(str)函数:获取参数值的字节个数concat(str1,str2,...)函数:字符串拼接upper(str)、lower(str)函数:大小…

pdf.js使用步骤

使用pdfjs 网页在线预览需要后端服务器支持 1、下载PDF.js 源码包 地址:PDF.js 2、解压源码包,将源码包放置到后端服务器 3、后端部署完成后 访问 viewer.html 类似上图 4、访问在线pdf文件 http://localhost:8081/web/viewer.html?filexxxx.pdf …

[⑥5G NR]: 无线接口协议,信道映射学习

5G系统整体包括核心网、接入网以及终端部分,接入网与终端间通过无线空口协议栈进行连接。无线接口可分为三个协议层:物理层(L1)、数据链路层(L2)和网络层(L3)。 L1:物理…

JVM运行时数据区——虚拟机栈

文章目录 1、虚拟机栈概述1.1、StackOverflowError1.2、OOM异常 2、栈的存储单位3、局部变量表3.1、局部变量表简介3.2、Slot 4、操作数栈5、栈顶缓存技术6、动态链接7、方法的调用7.1、方法调用的分类7.2、虚方法与非虚方法7.3、关于invokedynamic指令7.4、方法重写的本质7.5、…

学习Fiddler抓包

Fiddler通过代理的方式获取程序http通讯的数据,可以用其检测网页和服务器的交互情况,能够记录所有客户端和服务器间的http请求,支持监视、设置端点、以及修改输入输出数据等功能 本质是一个web代理服务器,它的默认工作端口是8888.…

【24年最新版PythonPycharm安装】保姆级别安装教学!附激活码插件分享~

Python 下载安装 Python可以编译成可执行文件(。 py),并通过网络在计算机和其它终端设备上运行。它有内置的数据类型、函数、类和对象,可以将其用于各种目的,例如管理数据和脚本开发。 Python已经成为一种非常流行的…

面试数据库篇(mysql)- 04了解过索引吗?(什么是索引)

索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 B-Tree,B树是…

代码随想录-动态规划

爬楼梯 class Solution {public int climbStairs(int n) {int[] dp new int[n1];dp[0] 1;dp[1] 1;for(int i 2;i < n; i){dp[i] dp[i-1] dp[i-2];}return dp[n];} }746.使用最小花费爬楼梯 class Solution {public int minCostClimbingStairs(int[] cost) {int len …

Verilog语言支持

Verilog语言支持 介绍 本章介绍AMD Vivado™对Verilog硬件描述的合成支持语言 本章包括编码示例。从“coding”下载编码示例文件示例。 Verilog设计 复杂电路的设计通常采用自上而下的方法。 •设计过程的每个阶段都需要不同的规范级别。例如&#xff0c;在体系结构级别&…