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

java团长

共 10908字,需浏览 22分钟

 · 2020-12-11

来源:https://blog.csdn.net/Charzous



开门见山

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

本篇将使用客户端-服务器(C/S架构),结合多线程技术,模拟类似QQ、微信聊天功能,实现一个网络实时聊天室,目前的功能包括:

(1) L(list):查看当前上线用户;

(2) G(group):群聊;

(3) O(one-one):私信;

(4) E(exit):退出当前聊天状态;

(5) bye:离线;

(6) H(help):查看帮助


本篇将详细记录网络实时聊天室的实现步骤

一、数据结构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 ConcurrentHashMapString> 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.EntryString> 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,上面是各个功能具体介绍,下面完整给出代码:

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 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架构),结合多线程技术,模拟类似QQ、微信聊天功能,实现一个网络实时聊天室。

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

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

浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报