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)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通信案例)基于UDP协议网络Socket编程(java实现C/S通信案例)_陆海潘江小C的博客-CSDN博客_基于udp的socket编程。编写一个基于udp协议的socket网络通信应用程序,实现如下功
  2. 基于TCP协议网络socket编程(java实现C/S通信)基于UDP协议网络Socket编程(java实现C/S通信案例)_陆海潘江小C的博客-CSDN博客_基于udp的socket编程。编写一个基于udp协议的socket网络通信应用程序,实现如下功
  3. Java多线程实现TCP网络Socket编程(C/S通信)Java多线程实现TCP网络Socket编程(C/S通信)_陆海潘江小C的博客-CSDN博客_socket tcp 多线程发送
  4. Java多线程实现多用户与服务端Socket通信Java多线程实现多用户与服务端Socket通信_陆海潘江小C的博客-CSDN博客_java socket 多用户

一、数据结构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通信案例)基于UDP协议网络Socket编程(java实现C/S通信案例)_陆海潘江小C的博客-CSDN博客_基于udp的socket编程。编写一个基于udp协议的socket网络通信应用程序,实现如下功
  2. 基于TCP协议网络socket编程(java实现C/S通信)基于UDP协议网络Socket编程(java实现C/S通信案例)_陆海潘江小C的博客-CSDN博客_基于udp的socket编程。编写一个基于udp协议的socket网络通信应用程序,实现如下功
  3. Java多线程实现TCP网络Socket编程(C/S通信)Java多线程实现TCP网络Socket编程(C/S通信)_陆海潘江小C的博客-CSDN博客_socket tcp 多线程发送
  4. Java多线程实现多用户与服务端Socket通信Java多线程实现多用户与服务端Socket通信_陆海潘江小C的博客-CSDN博客_java socket 多用户

我的CSDN博客:Java进阶:基于TCP的网络实时聊天室(socket通信案例)_陆海潘江小C的博客-CSDN博客

  • 83
    点赞
  • 215
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 45
    评论
里面包含聊天室客户端和服务器端的源文件和一份完整的设计报告。 一、 系统概要 本系统能实现基于VC++的网络聊天室系统。有单独的客户端、服务器端。 服务器应用程序能够接受来自客户端的广播,然后向客户端发送本机的IP与服务端口,让客户端接入到服务器进行聊天,检测用户名是否合法(重复),服务器责接收来自客户端聊天信息,并根据用户的需求发送给指定的人或所有人,能够给出上线下线提示。客户端能够发出连接请求,能编辑发送信息,可以指定发给单人或所有人,能显示聊天人数,上线下线用户等。 二、 通信规范的制定 服务请求规范: 服务器端: (1) 创建一个UDP的套接字,接受来自客户端的广播请求,当请求报文内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”时,接受请求,给客户端发送本服务器TCP聊天室的端口号。 (2) 创建一个主要的TCP协议的套接字负责客户端TCP连接 ,处理它的连接请求事件。 (3)在主要的TCP连接协议的套接字里面再创建TCP套接字保存到动态数组里,在主要的套接字接受请求后 ,就用这些套接字和客户端发送和接受数据。 客户端: (1) 当用户按“连接”按钮时,创建UDP协议套接字,给本地计算机发广播,广播内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”。 (2)当收到服务器端的回应,收到服务器发来的端口号后,关闭UDP连接。根据服务器的IP地址和端口号重新创建TCP连接。 故我思考:客户端一定要知道服务器的一个端口,我假设它知道服务器UDP服务的端口,通过发广播给服务器的UDP服务套接字,然后等待该套接字发回服务器TCP聊天室服务的端口号,IP地址用ReceiveForom也苛刻得到。 通信规范 通信规范的制定主要跟老师给出的差不多,并做了一小点增加: (增加验证用户名是否与聊天室已有用户重复,在服务器给客户端的消息中,增加标志0) ① TCP/IP数据通信 --- “聊天”消息传输格式 客户机 - 服务器 (1)传输“用户名” STX+1+用户名+ETX (2) 悄悄话 STX+2+用户名+”,”+内容+ETX (3) 对所有人说 STX+3+内容+ETX 服务器- 客户机 (0)请求用户名与在线用户名重复 //改进 STX+0+用户名+EXT (1)首次传输在线用户名 STX+1+用户名+ETX (2)传输新到用户名 STX+2+用户名+ETX (3)传输离线用户名 STX+3+用户名+ETX (4)传输聊天数据 STX+4+内容+ETX (注:STX为CHR(2),ETX 为CHR(3)) 三、 主要模块的设计分析 四、 系统运行效果 (要求有屏幕截图) 五、 心得与体会
基于JavaSocketTCP网络编程可以用来实现实时聊天互动程序。 首先,需要使用Java中的ServerSocket类来创建一个服务器端程序。服务器端程序监听指定的端口,等待客户端连接。一旦客户端连接成功,服务器端创建一个新的线程来处理该客户端的请求,这样可以实现多个客户端同时连接的功能。 在服务器端程序中,可以使用Socket类的getInputStream和getOutputStream方法来获取客户端的输入流和输出流。通过获取到的输入流和输出流,服务器端和客户端可以实现双向的数据通信。 在客户端程序中,也需要使用Socket类来创建一个客户端实例。客户端通过指定服务器的IP地址和端口号,发起连接请求。连接成功后,客户端也可以使用getInputStream和getOutputStream方法来获取服务器的输入流和输出流。 实时聊天互动可以通过服务器端和客户端互相发送消息来实现。例如,客户端发送消息到服务器端,服务器端将收到的消息发送给其他所有客户端。这样所有连接到服务器的客户端都可以看到其他客户端发送的消息。 此外,可以使用Java的多线程来实现同时接收多个客户端的请求。每当有新的客户端连接成功,服务器端就创建一个新的线程来处理该客户端的消息。 总结来说,基于JavaSocketTCP网络编程可以实现实时聊天互动的功能。通过服务器端和客户端之间的数据交互,可以实现多个客户端之间的消息传递,达到实时聊天的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 45
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陆海潘江小C

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值