Socket网络编程(六)——简易聊天室案例

6 篇文章 0 订阅

聊天室数据传输设计

  • 必要条件:客户端、服务器
  • 必要约束:数据传输协议
  • 原理:服务器监听消息来源、客户端链接服务器并发送消息到服务器

客户端、服务器数据交互

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接口。并实现两个方法:

    @Override
    public synchronized void onSelfClosed(ClientHandler handler) {
    }
 
    @Override
    public void onNewMessageArrived(ClientHandler handler, String msg) {
    }

并将 客户端构建溢出线程的remove操作迁移到 onSelfClosed() 方法实现内:

    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public synchronized void onSelfClosed(ClientHandler handler) {
        clientHandlerList.remove(handler);
    }
 
    /**
     * 转发消息给其他客户端
     * @param handler
     * @param msg
     */
    @Override
    public 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
     */
    @Override
    public 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

  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Socket是一种网络编程的通信协议,它能够实现计算机之间的通信。要实现一个聊天室,可以使用Socket来建立一个服务器和多个客户端之间的连接。 首先,我们需要创建一个服务器端的Socket,让它监听一个特定的端口。这样当客户端尝试连接到这个端口时,服务器就能够接收到连接请求。一旦连接成功,服务器和客户端之间就可以进行通信了。 服务器端可以使用多线程来处理多个客户端的连接请求。每当有新的客户端连接到服务器时,就创建一个新的线程来处理与这个客户端的通信。服务器可以接收来自客户端的消息,并将这些消息广播给所有其他客户端,从而实现群聊功能。 而客户端需要创建一个Socket来连接到服务器。客户端可以输入消息并通过Socket发送给服务器,然后等待服务器的广播消息。客户端也可以接收服务器传递过来的其他客户端发送的消息,从而实现与其他人的聊天功能。 在聊天室中,还可以添加一些额外的功能,比如私聊、发送文件等。私聊功能可以通过在消息中添加目标用户的标识来实现,使得只有目标用户能够接收到该条消息。发送文件功能可以通过将文件内容进行分割,并通过Socket逐个发送分割后的数据包。 总之,通过使用Socket协议,我们可以很方便地实现一个聊天室。服务器端和多个客户端之间的通信通过Socket建立连接,实现了消息的传递和广播,从而实现了聊天功能。同时,我们还可以扩展聊天室的功能,使得用户能够进行私聊、发送文件等操作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不愿意做鱼的小鲸鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值