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

osc_qcm2mqmy 2020-11-10 10:51:54
java jdk JavaFx


目录

开门见山

一、数据结构Map

二、保证线程安全

三、群聊核心方法

四、聊天室具体设计

0、用户登录服务器

1、查看当前上线用户

2、群聊

3、私信

4、退出当前聊天状态

5、离线

6、查看帮助

五、聊天室服务完整代码

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

结语


开门见山

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

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

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

(1)L(list):查看当前上线用户;
(2)G(group):群聊;
(3)O(one-one):私信;
(4)E(exit):退出当前聊天状态;
(5)bye:离线;
(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,用于给所有在线客服发送信息。

private void sendToMembers(String msg,String hostAddress,Socket mySocket) throws IOException{
PrintWriter pw;
OutputStream out;
Iterator iterator=users.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry=(Map.Entry) iterator.next();
Socket tempSocket = (Socket) entry.getKey();
String name = (String) entry.getValue();
if (!tempSocket.equals(mySocket)){
out=tempSocket.getOutputStream();
pw=new PrintWriter(new OutputStreamWriter(out,"utf-8"),true);
pw.println(hostAddress+":"+msg);
}
}
}

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

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

private void sendToOne(String msg,String hostAddress,Socket another) throws IOException{
PrintWriter pw;
OutputStream out;
Iterator iterator=users.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry=(Map.Entry) iterator.next();
Socket tempSocket = (Socket) entry.getKey();
String name = (String) entry.getValue();
if (tempSocket.equals(another)){
out=tempSocket.getOutputStream();
pw=new PrintWriter(new OutputStreamWriter(out,"utf-8"),true);
pw.println(hostAddress+"私信了你:"+msg);
}
}
}

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

四、聊天室具体设计

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

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

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

每个功能具体实现如下:

0、用户登录服务器

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

pw.println("From 服务器:欢迎使用服务!");
pw.println("请输入用户名:");
String localName = null;
while ((hostName=br.readLine())!=null){
users.forEach((k,v)->{
if (v.equals(hostName))
flag=true;//线程修改了全局变量
});
if (!flag){
localName=hostName;
users.put(socket,hostName);
flag=false;
break;
}
else{
flag=false;
pw.println("该用户名已存在,请修改!");
}
}

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

1、查看当前上线用户

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

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

2、群聊

else if (msg.trim().equals("G")){
pw.println("您已进入群聊。");
while ((msg=br.readLine())!=null){
if (!msg.equals("E")&&users.size()!=1)
sendToMembers(msg,localName,socket);
else if (users.size()==1){
pw.println("当前群聊无其他用户在线,已自动退出!");
break;
}
else {
pw.println("您已退出群组聊天室!");
break;
}
}
}

3、私信

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

//一对一私聊
else if (msg.trim().equalsIgnoreCase("O")){
pw.println("请输入私信人的用户名:");
String name=br.readLine();
//查找map中匹配的socket,与之建立通信
//有待改进,处理输入的用户名不存在的情况
users.forEach((k, v)->{
if (v.equals(name)) {
isExist=true;//全局变量与线程修改问题
}
});
//已修复用户不存在的处理逻辑
Socket temp=null;
for(Map.Entry<Socket,String> mapEntry : users.entrySet()){
if(mapEntry.getValue().equals(name))
temp = mapEntry.getKey();
// System.out.println(mapEntry.getKey()+":"+mapEntry.getValue()+'\n');
}
if (isExist){
isExist=false;
//私信后有一方用户离开,另一方未知,仍然发信息而未收到回复,未处理这种情况
while ((msg=br.readLine())!=null){
if (!msg.equals("E")&&!isLeaved(temp))
sendToOne(msg,localName,temp);
else if (isLeaved(temp)){
pw.println("对方已经离开,已断开连接!");
break;
}
else{
pw.println("您已退出私信模式!");
break;
}
}
}
else
pw.println("用户不存在!");
}

4、退出当前聊天状态

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

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

5、离线

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

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

6、查看帮助

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

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

五、聊天室服务完整代码

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

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

class Handler implements Runnable {
private Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
public void run() {
//本地服务器控制台显示客户端连接的用户信息
System.out.println("New connection accept:" + socket.getInetAddress().getHostAddress());
try {
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
pw.println("From 服务器:欢迎使用服务!");
pw.println("请输入用户名:");
String localName = null;
while ((hostName=br.readLine())!=null){
users.forEach((k,v)->{
if (v.equals(hostName))
flag=true;//线程修改了全局变量
});
if (!flag){
localName=hostName;
users.put(socket,hostName);
flag=false;//可能找出不一致问题
break;
}
else{
flag=false;
pw.println("该用户名已存在,请修改!");
}
}
// System.out.println(hostName+": "+socket);
sendToMembers("我已上线",localName,socket);
pw.println("输入命令功能:(1)L(list):查看当前上线用户;(2)G(group):进入群聊;(3)O(one-one):私信;(4)E(exit):退出当前聊天状态;(5)bye:离线;(6)H(help):帮助");
String msg = null;
//用户连接服务器上线,进入聊天选择状态
while ((msg = br.readLine()) != null) {
if (msg.trim().equalsIgnoreCase("bye")) {
pw.println("From 服务器:服务器已断开连接,结束服务!");
users.remove(socket,localName);
sendToMembers("我下线了",localName,socket);
System.out.println("客户端离开。");//加当前用户名
break;
}
else if (msg.trim().equalsIgnoreCase("H")){
pw.println("输入命令功能:(1)L(list):查看当前上线用户;(2)G(group):进入群聊;(3)O(one-one):私信;(4)E(exit):退出当前聊天状态;(5)bye:离线;(6)H(help):帮助");
continue;//返回循环
}
else if (msg.trim().equalsIgnoreCase("L")){
users.forEach((k,v)->{
pw.println("用户:"+v);
});
continue;
}
//一对一私聊
else if (msg.trim().equalsIgnoreCase("O")){
pw.println("请输入私信人的用户名:");
String name=br.readLine();
//查找map中匹配的socket,与之建立通信
users.forEach((k, v)->{
if (v.equals(name)) {
isExist=true;//全局变量与线程修改问题
}
});
//已修复用户不存在的处理逻辑
Socket temp=null;
for(Map.Entry<Socket,String> mapEntry : users.entrySet()){
if(mapEntry.getValue().equals(name))
temp = mapEntry.getKey();
}
if (isExist){
isExist=false;
//私信后有一方用户离开,另一方未知,仍然发信息而未收到回复,未处理这种情况
while ((msg=br.readLine())!=null){
if (!msg.equals("E")&&!isLeaved(temp))
sendToOne(msg,localName,temp);
else if (isLeaved(temp)){
pw.println("对方已经离开,已断开连接!");
break;
}
else{
pw.println("您已退出私信模式!");
break;
}
}
}
else
pw.println("用户不存在!");
}
//选择群聊
else if (msg.trim().equals("G")){
pw.println("您已进入群聊。");
while ((msg=br.readLine())!=null){
if (!msg.equals("E")&&users.size()!=1)
sendToMembers(msg,localName,socket);
else if (users.size()==1){
pw.println("当前群聊无其他用户在线,已自动退出!");
break;
}
else {
pw.println("您已退出群组聊天室!");
break;
}
}
}
else
pw.println("请选择聊天状态!");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

六、效果演示:基于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

版权声明
本文为[osc_qcm2mqmy]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4274724/blog/4710729

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云