Java进阶:基于TCP的网络实时聊天室(socket通信案例)

目录

开门见山

一、数据结构Map

二、保证线程安全

三、群聊核心方法

四、聊天室具体设计

0、用户登录服务器

1、查看当前上线用户

2、群聊

3、私信

4、退出当前聊天状态

5、离线

6、查看帮助

五、聊天室服务完整代码

六、效果演示:基于TCP的网络实时聊天室

结语


开门见山

最近一个月记录了学习Socket网络编程的知识和实战案例,相对来说,比较系统地学习了基于TCP协议实现网络通信,也是计算机网络中重中之重,TCP/IP属于网络层,在java中,对该层的工作实现了封装,在编程中,就更加容易地去实现通信,而不用顾及底层的实现。当然,我们需要熟悉五层协议,在实践中体会其中的原理,理解更加深刻。

所以,系列文章从入门开始,不断完善C/S架构的Socket通信,回忆一下,首先是实现了Server和Client的互相通信,在这个过程发现问题,接着就使用多线程技术解决客户端实时接收信息的问题,后来到了服务器端,发现多用户连接服务器的“先到先得”问题,“后到者”无法正常通信,所以再使用线程池技术解决了多用户服务器的问题。

到此,基本实现了一个简单的客户端-服务器应用,因此,本篇将基于前面全部内容,使用客户端-服务器(C/S架构),结合多线程技术,模拟类似QQ、微信聊天功能,实现一个网络实时聊天室,目前的功能包括:

1(1)L(list):查看当前上线用户; 2(2)G(group):群聊; 3(3)O(one-one):私信; 4(4)E(exit):退出当前聊天状态; 5(5)bye:离线; 6(6)H(help):查看帮助

本篇将详细记录网络实时聊天室的实现步骤,下面的系列文章为前提基础。

Java实现socket通信网络编程系列文章:

  1. 基于UDP协议网络Socket编程(java实现C/S通信案例)https://blog.csdn.net/Charzous/article/details/109016215
  2. 基于TCP协议网络socket编程(java实现C/S通信)https://blog.csdn.net/Charzous/article/details/109016215
  3. Java多线程实现TCP网络Socket编程(C/S通信)https://blog.csdn.net/Charzous/article/details/109283697
  4. Java多线程实现多用户与服务端Socket通信https://blog.csdn.net/Charzous/article/details/109440277

一、数据结构Map

前两篇的TCPClientThreadFX和TCPThreadServer实现了多线程的通信,但也只是客户端和服务器的聊天,如何做到群组的聊天?想法就是客户A的聊天信息通过服务器转发到同时在线的所有客户。

具体做法是需要在服务器端新增记录登陆客户信息的功能,每个用户都有自己的标识。本篇将使用简单的“在线方式”记录客户套接字,即采用集合来保存用户登陆的套接字信息,来跟踪用户连接。

所以,我们需要选择一种合适的数据结构来保存用户的Socket和用户名信息,那在java中,提供了哪些数据结构呢?

Java常用的集合类型有:Map、List和Set。Map是保存Key-Value对,List类似数组,可保存可重复的值,而Set只保存不重复的值,相当于是只保存key,不保存value的Map。

如果是有用户名、学号登录的操作,就可以采用Map类型的集合来存储,例如可使用key记录用户名+学号,value保存套接字。对于**本篇的网络聊天室的需求,需要采用Map,用来保存不同用户的socket和登录名。**用户套接字socket作为key来标识一个在线用户是比较方便的选择,因为每一个客户端的IP地址+端口组合是不一样的。

二、保证线程安全

很明显,我们需要使用到多线程技术,而在多线程环境中,对共享资源的读写存在线程并发安全的问题,例如HashMap、HaspSet等都不是线程安全的,可以通过synchronized关键字进行加锁,但还有更方便的方案:可以直接使用Java标准库的java.util.concurrent包提供的线程安全的集合。例如HashMap的线程安全是 ConcurrentHashMap,HashSet的线程安全Set是CopyOnWriteArraySet

如图,Map继承体系:

 在JDK1.8中,对HashMap进行了改进,当结点数量超过**TREEIFY_THRESHOLD** 则要转换为红黑树,这样很大优化了查询的效率,但仍然不是线程安全的。

这里简单了解一下,具体学习可以查询相关资料。有了以上的基本知识,下面开始进入网络实时聊天室的具体实现。

三、群聊核心方法

基于前面这样的想法:实现群聊就是客户A的聊天信息通过服务器转发到同时在线的所有客户,服务器端根据HashMap记录登陆用户的socket,向所有用户转发信息。

核心的群组发送方法sendToAllMembers,用于给所有在线客服发送信息。

1private void sendToMembers(String msg,String hostAddress,Socket mySocket) throws IOException{ 2 3 PrintWriter pw; 4 OutputStream out; 5 Iterator iterator=users.entrySet().iterator(); 6 while (iterator.hasNext()){ 7 Map.Entry entry=(Map.Entry) iterator.next(); 8 Socket tempSocket = (Socket) entry.getKey(); 9 String name = (String) entry.getValue(); 10 if (!tempSocket.equals(mySocket)){ 11 out=tempSocket.getOutputStream(); 12 pw=new PrintWriter(new OutputStreamWriter(out,"utf-8"),true); 13 pw.println(hostAddress+":"+msg); 14 } 15 } 16 17}

 使用到了Map的遍历,对其他所有用户发送信息。

相同的原理,我们实现私聊的功能,转化为实现的思想,也就是当前用户和指定用户Socket之间的通信,所以我写了一个sendToOne的方法。

1private void sendToOne(String msg,String hostAddress,Socket another) throws IOException{ 2 3 PrintWriter pw; 4 OutputStream out; 5 6 Iterator iterator=users.entrySet().iterator(); 7 while (iterator.hasNext()){ 8 9 Map.Entry entry=(Map.Entry) iterator.next(); 10 Socket tempSocket = (Socket) entry.getKey(); 11 String name = (String) entry.getValue(); 12 13 if (tempSocket.equals(another)){ 14 out=tempSocket.getOutputStream(); 15 pw=new PrintWriter(new OutputStreamWriter(out,"utf-8"),true); 16 pw.println(hostAddress+"私信了你:"+msg); 17 } 18 } 19}

 以上两个方法是本网络聊天室的关键,后面实现的功能将是对这两个方法的灵活运用。

四、聊天室具体设计

目前聊天室的功能定位包括:1)**查看当前上线用户;**2):**群聊;**3)**私信;**4)**退出当前聊天状态;**5)**离线;**6)查看帮助。

首先,初始化最关键的数据结构,作为类成员变量,HashMap用来保存Socket和用户名:

private ConcurrentHashMap<Socket,String> users=new ConcurrentHashMap();

每个功能具体实现如下:

0、用户登录服务器

这里是最开始的服务器端的信息处理,需要记录每个用户的登录信息,包括连接的套接字和自定义昵称,方便后续使用。我采用的方法是当用户连接服务器时候,提醒用户输入用户名来进一步操作,也实现了不重名的判断逻辑。代码如下:

1pw.println("From 服务器:欢迎使用服务!"); 2pw.println("请输入用户名:"); 3String localName = null; 4while ((hostName=br.readLine())!=null){ 5 users.forEach((k,v)->{ 6 if (v.equals(hostName)) 7 flag=true;//线程修改了全局变量 8 }); 9 10 if (!flag){ 11 localName=hostName; 12 users.put(socket,hostName); 13 flag=false; 14 break; 15 } 16 else{ 17 flag=false; 18 pw.println("该用户名已存在,请修改!"); 19 } 20}

 登录成功之后会向所有在线用户告知上线信息。

1、查看当前上线用户

其实就是将服务器端记录在HashMap中的信息返回给请求用户,通过约定的命令L来查看:

1if (msg.trim().equalsIgnoreCase("L")){ 2 users.forEach((k,v)->{ 3 pw.println("用户:"+v); 4 }); 5 continue; 6}

2、群聊

1else if (msg.trim().equals("G")){ 2 pw.println("您已进入群聊。"); 3 while ((msg=br.readLine())!=null){ 4 if (!msg.equals("E")&&users.size()!=1) 5 sendToMembers(msg,localName,socket); 6 else if (users.size()==1){ 7 pw.println("当前群聊无其他用户在线,已自动退出!"); 8 break; 9 } 10 else { 11 pw.println("您已退出群组聊天室!"); 12 break; 13 } 14 } 15 16}

3、私信

同理,处理逻辑变为一对一的通信,与之前服务器-客户端一对一类似,但是这里需要更多的处理,保证逻辑正确,包括被私聊人的在线状态,被私聊人用户名是否正确等。

1//一对一私聊 2else if (msg.trim().equalsIgnoreCase("O")){ 3 pw.println("请输入私信人的用户名:"); 4 String name=br.readLine(); 5 6 //查找map中匹配的socket,与之建立通信 7 //有待改进,处理输入的用户名不存在的情况 8 users.forEach((k, v)->{ 9 if (v.equals(name)) { 10 isExist=true;//全局变量与线程修改问题 11 } 12 13 }); 14 //已修复用户不存在的处理逻辑 15 Socket temp=null; 16 for(Map.Entry<Socket,String> mapEntry : users.entrySet()){ 17 if(mapEntry.getValue().equals(name)) 18 temp = mapEntry.getKey(); 19// System.out.println(mapEntry.getKey()+":"+mapEntry.getValue()+'\n'); 20 } 21 if (isExist){ 22 isExist=false; 23 //私信后有一方用户离开,另一方未知,仍然发信息而未收到回复,未处理这种情况 24 while ((msg=br.readLine())!=null){ 25 if (!msg.equals("E")&&!isLeaved(temp)) 26 sendToOne(msg,localName,temp); 27 else if (isLeaved(temp)){ 28 pw.println("对方已经离开,已断开连接!"); 29 break; 30 } 31 else{ 32 pw.println("您已退出私信模式!"); 33 break; 34 } 35 } 36 } 37 else 38 pw.println("用户不存在!"); 39}

**4、**退出当前聊天状态

这个功能主要融入到群聊和私聊中,可见上面两个功能实现的内部调用,定义了一个方法isLeaved,判断用户是否已经下线。

1//判断用户是否已经下线 2private Boolean isLeaved(Socket temp){ 3 Boolean leave=true; 4 for(Map.Entry<Socket,String> mapEntry : users.entrySet()) { 5 if (mapEntry.getKey().equals(temp)) 6 leave = false; 7 } 8 return leave; 9}

5、离线

这个功能比较简单,通过约定的命令执行。

1if (msg.trim().equalsIgnoreCase("bye")) { 2 pw.println("From 服务器:服务器已断开连接,结束服务!"); 3 4 users.remove(socket,localName); 5 6 sendToMembers("我下线了",localName,socket); 7 System.out.println("客户端离开。");//加当前用户名 8 break; 9}

**6、**查看帮助

通过命令H请求服务器的帮助,是指用户查看哪些命令对应的功能,来进行选择。

1else if (msg.trim().equalsIgnoreCase("H")){ 2 pw.println("输入命令功能:(1)L(list):查看当前上线用户;(2)G(group):进入群聊;(3)O(one-one):私信;(4)E(exit):退出当前聊天状态;(5)bye:离线;(6)H(help):帮助"); 3 continue;//返回循环 4}

五、聊天室服务完整代码

聊天室实现主要工作在于服务端,聚焦于服务器线程处理的内部类Hanler,上面是各个功能具体介绍,下面完整给出代码,只需要在前面文章的基础上,见Java多线程实现多用户与服务端Socket通信

修改服务器端线程处理代码:

1class Handler implements Runnable { 2 private Socket socket; 3 4 public Handler(Socket socket) { 5 this.socket = socket; 6 } 7 8 public void run() { 9 //本地服务器控制台显示客户端连接的用户信息 10 System.out.println("New connection accept:" + socket.getInetAddress().getHostAddress()); 11 try { 12 BufferedReader br = getReader(socket); 13 PrintWriter pw = getWriter(socket); 14 15 pw.println("From 服务器:欢迎使用服务!"); 16 pw.println("请输入用户名:"); 17 String localName = null; 18 while ((hostName=br.readLine())!=null){ 19 users.forEach((k,v)->{ 20 if (v.equals(hostName)) 21 flag=true;//线程修改了全局变量 22 }); 23 24 if (!flag){ 25 localName=hostName; 26 users.put(socket,hostName); 27 flag=false;//可能找出不一致问题 28 break; 29 } 30 else{ 31 flag=false; 32 pw.println("该用户名已存在,请修改!"); 33 } 34 } 35 36// System.out.println(hostName+": "+socket); 37 sendToMembers("我已上线",localName,socket); 38 pw.println("输入命令功能:(1)L(list):查看当前上线用户;(2)G(group):进入群聊;(3)O(one-one):私信;(4)E(exit):退出当前聊天状态;(5)bye:离线;(6)H(help):帮助"); 39 40 String msg = null; 41 //用户连接服务器上线,进入聊天选择状态 42 while ((msg = br.readLine()) != null) { 43 if (msg.trim().equalsIgnoreCase("bye")) { 44 pw.println("From 服务器:服务器已断开连接,结束服务!"); 45 46 users.remove(socket,localName); 47 48 sendToMembers("我下线了",localName,socket); 49 System.out.println("客户端离开。");//加当前用户名 50 break; 51 } 52 else if (msg.trim().equalsIgnoreCase("H")){ 53 pw.println("输入命令功能:(1)L(list):查看当前上线用户;(2)G(group):进入群聊;(3)O(one-one):私信;(4)E(exit):退出当前聊天状态;(5)bye:离线;(6)H(help):帮助"); 54 continue;//返回循环 55 } 56 else if (msg.trim().equalsIgnoreCase("L")){ 57 users.forEach((k,v)->{ 58 pw.println("用户:"+v); 59 }); 60 continue; 61 } 62 //一对一私聊 63 else if (msg.trim().equalsIgnoreCase("O")){ 64 pw.println("请输入私信人的用户名:"); 65 String name=br.readLine(); 66 67 //查找map中匹配的socket,与之建立通信 68 users.forEach((k, v)->{ 69 if (v.equals(name)) { 70 isExist=true;//全局变量与线程修改问题 71 } 72 73 }); 74 //已修复用户不存在的处理逻辑 75 Socket temp=null; 76 for(Map.Entry<Socket,String> mapEntry : users.entrySet()){ 77 if(mapEntry.getValue().equals(name)) 78 temp = mapEntry.getKey(); 79 } 80 if (isExist){ 81 isExist=false; 82 //私信后有一方用户离开,另一方未知,仍然发信息而未收到回复,未处理这种情况 83 while ((msg=br.readLine())!=null){ 84 if (!msg.equals("E")&&!isLeaved(temp)) 85 sendToOne(msg,localName,temp); 86 else if (isLeaved(temp)){ 87 pw.println("对方已经离开,已断开连接!"); 88 break; 89 } 90 else{ 91 pw.println("您已退出私信模式!"); 92 break; 93 } 94 } 95 } 96 else 97 pw.println("用户不存在!"); 98 } 99 //选择群聊 100 else if (msg.trim().equals("G")){ 101 pw.println("您已进入群聊。"); 102 while ((msg=br.readLine())!=null){ 103 if (!msg.equals("E")&&users.size()!=1) 104 sendToMembers(msg,localName,socket); 105 else if (users.size()==1){ 106 pw.println("当前群聊无其他用户在线,已自动退出!"); 107 break; 108 } 109 else { 110 pw.println("您已退出群组聊天室!"); 111 break; 112 } 113 } 114 115 } 116 else 117 pw.println("请选择聊天状态!"); 118 } 119 } catch (IOException e) { 120 e.printStackTrace(); 121 } finally { 122 try { 123 if (socket != null) 124 socket.close(); 125 } catch (IOException e) { 126 e.printStackTrace(); 127 } 128 } 129 } 130}

六、效果演示:基于TCP的网络实时聊天室

首先,开启多个客户端,连接服务器开始进入通信状态。

下面动图演示了几个基本功能,可以看到三个用户实现了实时通信聊天,包括群聊和私聊功能。其他功能就留给大家去探索。

结语

系列文章从入门开始,不断完善C/S架构的Socket通信,回忆一下,首先是实现了Server和Client的互相通信,在这个过程发现问题,接着就使用多线程技术解决客户端实时接收信息的问题,后来到了服务器端,发现多用户连接服务器的“先到先得”问题,“后到者”无法正常通信,所以再使用线程池技术解决了多用户服务器的问题。

本篇基本实现了一个简单的客户端-服务器应用,使用客户端-服务器(C/S架构),结合多线程技术,模拟类似QQ、微信聊天功能,实现一个网络实时聊天室。

学习到的知识有:多线程、线程池、Socket通信、TCP协议、HashMap、JavaFX等,所有知识的结合运用,并通过实战练习,一步步理解知识!

如果觉得不错欢迎“一键三连”哦,点赞收藏关注,有问题直接评论,交流学习!

Java实现socket通信网络编程系列文章:

  1. 基于UDP协议网络Socket编程(java实现C/S通信案例)https://blog.csdn.net/Charzous/article/details/109016215
  2. 基于TCP协议网络socket编程(java实现C/S通信)https://blog.csdn.net/Charzous/article/details/109016215
  3. Java多线程实现TCP网络Socket编程(C/S通信)https://blog.csdn.net/Charzous/article/details/109283697
  4. Java多线程实现多用户与服务端Socket通信https://blog.csdn.net/Charzous/article/details/109440277

我的CSDN博客:https://blog.csdn.net/Charzous/article/details/109540279

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Java进阶:基于TCP的网络实时聊天室(socket通信案例) - HelloWorld