当前位置: 首页 > news >正文

手写一个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); } }
http://www.jsqmd.com/news/1265825/

相关文章:

  • C++赋值运算符重载:从深浅拷贝到拷贝交换的完整指南
  • Agent工程师技术栈:从LLM调用到生产部署全解析
  • C++ const关键字深度解析:从语法到设计哲学与实战应用
  • 晶圆机器人大臂疲劳寿命预测:GWO-BP神经网络方法
  • C/C++全排列算法:从蛮力法到回溯框架的深度解析
  • LVDS与CSI-2高速接口实战:寄存器配置、协议解析与调试指南
  • HarmonyOS ArkTS 实战:实现一个房贷计算器
  • SVD分解在SLAM位姿估计中的原理推导与C++实现
  • 如何快速掌握SubtitleEdit:免费开源字幕编辑工具的完整指南
  • Atomic Agent智能体本地部署实战:从GAIA基准优势到生产环境配置
  • 2026 年现阶段,建德正规的调直扁铁批发厂家选哪家,难怪工地师傅效率翻倍,靠的是这玩意儿省了一半工夫还没瞎折腾?-鑫艾腾五金制品 - 企业信息推荐【官方】
  • 飞牛OS笔记本合盖不休眠设置指南
  • 深入解析TI AM261x PKE引擎:从硬件架构到ECC/ECDSA实战避坑指南
  • C++性能优化:深入理解CPU操作成本与实战技巧
  • NLP采样策略详解:温度参数与Top-p采样实战指南
  • SpringBoot+Vue 助农管理系统管理平台源码【适合毕设/课设/学习】Java+MySQL
  • 昇腾AI处理器实时视频分析系统设计与优化
  • OpenClaw智能体:系统级自动化操作的技术解析
  • C++文件操作进阶:从I/O流到二进制序列化实战指南
  • AI高效协作:超越提示词的五大实战方法
  • 智能ERP系统:AI驱动的实时闭环优化实践
  • 图像超分、去模糊轻量化算法,端侧实时推理实现
  • 郑州暴雨季房屋漏水高发如何应对?2026中原城市防水施工时机与团队选择 - 雨婺虹房屋维修
  • C++跨平台实现打开文件所在文件夹并选中文件功能详解
  • LlamaIndex与LangChain文档处理实战指南
  • C++深入浅出讲解函数重载
  • C++线程安全数据结构:从互斥锁到无锁编程的实战指南
  • 智能展会登记系统:OCR与高并发架构实战
  • Python集成测试实战:基于Testcontainers与pytest构建真实数据库测试环境
  • 仅解码器架构在嵌入模型中的高效应用实践