手写一个Java聊天室·自定义协议与多线程实践

📅 2026/7/26 6:46:56 👁️ 阅读次数 📝 编程学习
手写一个Java聊天室·自定义协议与多线程实践

一.项目概述

这是一个基于Java Socket自定义通信协议的简易聊天室。服务端负责管理客户端连接和消息的接收与转发,客户端通过控制台输入消息并实时接收消息。

✅️完整代码已嵌入笔记,可自行运行尝试~

二.服务端核心模块

  • 定义成员变量

    • ss、sockerList 实例变量
      • 特点:属于对象(也就是new Server()创建出来的那个具体实例)
      • 调用方式:必须通过对象名.变量名访问(eg:server.socketList)
      • socketList用于存储所有socket
    • nickMap 静态变量
      • 特点:属于类本身,而不属于某个具体的对象
      • 调用方式:可以直接通过类名.变量名访问(eg:Server.nickMap)
      • 通过哈希表存储每个客户对应的昵称
        ServerSocket ss=null; ArrayList<Socket> socketList=new ArrayList<>(); //存储每个Socket 对应的昵称 static HashMap<Socket,String> nickMap=new HashMap<>();
  • init()方法——初始化服务端

    • ss=new ServerSocket(9527);会在本机的9527端口上创建一个服务端套接字,开始监听客户端连接需求
      public void init(){ try { ss=new ServerSocket(9527); } catch (IOException e) { throw new RuntimeException(e); } }
  • listen()方法——等待客户端连接

    • ss.accept()是一个阻塞方法,没有客户接入时就会卡在这里。当有客户端连接进来时,accept会返回一个Socket对象,代表与这个客户的通信通道
      public Socket listen(){ try { return ss.accept(); } catch (IOException e) { throw new RuntimeException(e); } }
  • send()方法——发送消息

    • 先将字符串消息转换为字节数组
    • 读取消息的长度
    • 利用write方法发送长度
    • 利用write方法发送消息内容这是自定义协议的核心:先发长度,再发内容
    • 调用flush() 相当于点击发送按钮
      public void send(String message, OutputStream os){ try { //用DataOutputStream 包装,方便写int DataOutputStream dos=new DataOutputStream(os); byte[] msgs=message.getBytes("UTF-8"); int len=msgs.length; dos.writeInt(len);//发送长度 dos.write(msgs); dos.flush(); } catch (IOException e) { throw new RuntimeException(e); } }
  • receive()方法——接收消息

    • 先读取消息长度
    • 创建一个长度为len的字节数组,从输入流中读取len个字节填充进去,得到消息完整的字节数据
    • 将字节数组转换回字符串,得到原始消息内容
    • 获取发送消息的客户端昵称,通过循环遍历所有客户端找到与当前连接相同的那个socket,打印消息
    • 返回消息内容
      public String receive(InputStream in,Socket socket){ try { //用DataInputStream 包装,方便读int DataInputStream dis=new DataInputStream(in); int len=dis.readInt(); byte[] msgs=new byte[len]; dis.readFully(msgs); String msg=new String(msgs,"UTF-8"); String senderName=nickMap.get(socket); for (int i = 0; i < socketList.size(); i++) { Socket socket2=socketList.get(i); if (socket2==socket){ System.out.println("收到用户"+senderName+"消息:"+msg); } } return msg; } catch (IOException e) { throw new RuntimeException(e); } }
  • proClientMsg()方法——读取客户端消息

    • 新建线程,在线程内完成以下内容
      • 创建输入流对象
      • 调用receive方法来接收昵称,并将昵称存入nickMap中,输出用户上线消息
      • 循环内转发消息给客户端:遍历所有客户端,如果不是发送者本人就转发消息
    • public void proClientMsg(Socket socket){ new Thread(()->{ InputStream in= null; try { in = socket.getInputStream(); //接收昵称 String nickName=receive(in,socket); nickMap.put(socket,nickName); System.out.println("用户"+nickName+"(端口"+socket.getPort()+")上线"); } catch (IOException e) { throw new RuntimeException(e); } while (true){ try { String msg=receive(in,socket); String senderName=nickMap.get(socket); //转发消息给客户端 for (int i = 0; i < socketList.size(); i++) { Socket socket1=socketList.get(i); if (socket1!=socket){ send("收到用户"+senderName+"的消息:"+msg,socket1.getOutputStream()); } } } catch (IOException e) { throw new RuntimeException(e); } } }).start(); }
  • 主函数——程序入口

    • 创建对象
    • 调用初始化方法
    • 新建线程,在线程内完成以下内容
      • 调用监听方法
      • 将每个socket存入数组中
      • 调用读取消息方法
    • static void main() { Server server=new Server(); //初始化 server.init(); //监听连接部分 new Thread(()->{ while (true) { System.out.println("监听连接中……"); Socket socket = server.listen(); System.out.println(socket.getRemoteSocketAddress()+" 连接成功"); server.socketList.add(socket); server.proClientMsg(socket);//处理客户端消息,每个客户端对应各自的线程 } }).start();

三.客户端核心模块

  • 定义成员变量

    • socket、nickName
  • init()方法——初始化客户端

    • new Socket("127.0.0.1",9527); 创建一个Socket连接,连接到本机的服务端监听端口
      public void init(){ try { socket=new Socket("127.0.0.1",9527); } catch (IOException e) { throw new RuntimeException(e); } }
  • send()方法——发送消息

    • 与服务端send方法完全一致
      public void send(String message){ try { OutputStream os=socket.getOutputStream(); DataOutputStream dos=new DataOutputStream(os); byte[] msgs=message.getBytes("UTF-8"); int len=msgs.length; dos.writeInt(len); dos.write(msgs);//编写 dos.flush();//发送 } catch (IOException e) { throw new RuntimeException(e); } }
  • readMsg()方法——接收消息

    • 与服务端receive方法完全一致
      public String readMsg(){ try { InputStream ins=socket.getInputStream(); DataInputStream dis=new DataInputStream(ins); int len=dis.readInt(); byte[] msgs=new byte[len]; dis.readFully(msgs); String msg=new String(msgs,"UTF-8"); return msg; } catch (IOException e) { throw new RuntimeException(e); } }
  • 主函数——程序入口

    • 创建对象
    • 调用初始化方法
    • 输入昵称并发送
    • 开启一个线程,开启循环发送消息
    • 开启循环接收消息
      static void main() { Client client=new Client(); //初始化 client.init(); //输入昵称 Scanner scanner1=new Scanner(System.in); System.out.println("请输入你的昵称:"); client.nickName=scanner1.nextLine(); client.send(client.nickName); //接收消息 new Thread(()->{ while (true){ String msg= client.readMsg(); System.out.println(msg); System.out.println("请输入消息:"); } }).start(); //发送消息 Scanner scanner=new Scanner(System.in); while (true){ System.out.println("请输入消息:"); String message=scanner.nextLine(); client.send(message); } }