手写一个Java聊天室·自定义协议与多线程实践
📅 2026/7/26 6:46:56
👁️ 阅读次数
📝 编程学习
一.项目概述
这是一个基于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<>();
- ss、sockerList 实例变量
init()方法——初始化服务端
- ss=new ServerSocket(9527);会在本机的9527端口上创建一个服务端套接字,开始监听客户端连接需求
public void init(){ try { ss=new ServerSocket(9527); } catch (IOException e) { throw new RuntimeException(e); } }
- ss=new ServerSocket(9527);会在本机的9527端口上创建一个服务端套接字,开始监听客户端连接需求
listen()方法——等待客户端连接
- ss.accept()是一个阻塞方法,没有客户接入时就会卡在这里。当有客户端连接进来时,accept会返回一个Socket对象,代表与这个客户的通信通道
public Socket listen(){ try { return ss.accept(); } catch (IOException e) { throw new RuntimeException(e); } }
- ss.accept()是一个阻塞方法,没有客户接入时就会卡在这里。当有客户端连接进来时,accept会返回一个Socket对象,代表与这个客户的通信通道
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); } }
- 与服务端send方法完全一致
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); } }
- 与服务端receive方法完全一致
主函数——程序入口
- 创建对象
- 调用初始化方法
- 输入昵称并发送
- 开启一个线程,开启循环发送消息
- 开启循环接收消息
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); } }
编程学习
技术分享
实战经验