一、项目整体架构
本项目是基于 BIO 阻塞式 Socket 实现的桌面多人聊天室,支持用户登录、群聊、一对一私聊功能,搭配Swing实现可视化客户端界面。整体分为服务端、客户端、用户实体、UI界面四大核心模块,采用 服务端中转消息 的通信模式(所有客户端消息统一发送至服务端,由服务端分发转发)。
1.1 核心模块分工
- Server(服务端):监听端口、接收客户端连接、处理用户登录、解析消息类型、转发私聊/群聊消息、维护在线用户列表
- Client(客户端):建立服务端连接、用户账号登录、封装私聊/群聊消息、发送消息、接收服务端转发的消息
- ClientUser(用户实体):封装在线用户信息(账号、Socket连接),服务端核心存储对象
- ClientUI(可视化界面):基于Swing实现聊天窗口、输入框、发送按钮,绑定消息收发事件
1.2 通信核心原理
基于TCP长连接通信,客户端与服务端建立永久Socket连接,全程流式传输字节数据;通过 自定义消息协议 区分私聊、群聊消息,服务端统一调度转发,实现多客户端通信。
二、自定义通信协议(核心重点)
项目无第三方通信框架,完全自定义消息格式,通过固定字节顺序解析消息类型、发送目标、消息内容,是实现私聊/群聊区分的核心。
2.1 消息类型定义
- 1:私聊消息(定向发送指定用户)
- 2:群聊消息(广播给所有在线用户,除发送者自身)
2.2 完整消息协议格式
私聊消息格式:消息类型(1字节) + 好友名称长度(1字节) + 好友名称(字节数组) + 消息长度(1字节) + 消息内容(字节数组)
群聊消息格式:消息类型(1字节) + 消息长度(1字节) + 消息内容(字节数组)
登录消息格式:账号长度(1字节) + 账号名称(字节数组)
三、用户实体类(ClientUser)
服务端用于存储所有在线用户的核心实体,绑定用户账号与Socket连接,实现精准消息转发。
java
package V4;
import java.net.Socket;
/**
* 在线用户实体类:封装单个客户端的连接信息与账号
* 服务端集合通过该对象维护所有在线用户
*/
public class ClientUser {
String account; // 客户端登录账号
Socket socket; // 客户端对应的Socket连接对象
// 构造方法:登录成功后创建用户对象
public ClientUser(Socket socket, String account) {
this.account = account;
this.socket = socket;
}
}
四、客户端核心实现(Client)
客户端核心功能:初始化连接、账号登录、封装发送消息、接收服务端返回消息。所有IO操作基于字节流传输,严格遵循自定义协议。
java
package V4;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 聊天室客户端核心类
* 负责连接服务端、登录、消息发送、消息接收
*/
public class Client {
Socket socket = null; // 客户端Socket连接对象
// 初始化:连接服务端 + 执行登录逻辑
public void init(){
try {
// 连接本地服务端,端口与服务端一致:54321
socket = new Socket("127.0.0.1",54321);
login(socket.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 用户登录:向服务端发送账号信息
public void login(OutputStream os){
Scanner sc = new Scanner(System.in);
System.out.println("请输入账号:");
String account = sc.nextLine();
byte[] accounts = account.getBytes();
try {
// 先发送账号长度,再发送账号内容(服务端按长度读取,避免粘包)
os.write(accounts.length);
os.write(accounts);
os.flush(); // 强制刷新流,确保数据立即发送
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 消息发送核心方法:区分私聊/群聊,封装协议数据
* 规则:消息以@开头为私聊,否则为群聊
*/
public void send(String message){
try {
OutputStream os = socket.getOutputStream();
// 判断消息类型:首字符@ 代表私聊
if (message.charAt(0) == '@') {
// 拆分私聊格式:@好友名 消息内容
int index = message.indexOf(' ');
String friend = message.substring(1, index); // 截取目标好友账号
String msg = message.substring(index + 1); // 截取消息内容
// 按照私聊协议封装数据
os.write(1);// 1=私聊类型标识
int fidlen = friend.getBytes().length;
os.write(fidlen); // 好友名称字节长度
os.write(friend.getBytes()); // 好友名称字节数据
int msglen = msg.getBytes().length;
os.write(msglen); // 消息内容字节长度
os.write(msg.getBytes()); // 消息内容字节数据
}else{
// 群聊消息封装
os.write(2);// 2=群聊类型标识
byte[] msgs = message.getBytes();
os.write(msgs.length); // 消息长度
os.write(msgs); // 消息内容
}
os.flush();// 强制刷新,完成消息发送
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 读取服务端转发的消息
public String readMsg(){
try {
InputStream ins = socket.getInputStream();
// 先读长度,再读内容,解决TCP粘包问题
int len = ins.read();
byte[] msgs = new byte[len];
ins.read(msgs);
return new String(msgs);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static void main() {
}
}
4.1 客户端核心关键点备注
- 粘包解决核心:所有消息传输遵循「先传长度、再传内容」,服务端根据长度精准读取字节,避免TCP流式传输导致的粘包、半包问题
- 消息区分规则:通过消息首字符
@判定私聊,空格分割好友名称与消息内容 - 流刷新机制:每次write后必须执行
flush(),否则数据会滞留缓冲区,无法及时发送
五、服务端核心实现(Server)
服务端是整个聊天室的中枢,负责监听连接、处理登录、解析消息、转发消息、维护在线用户列表,通过多线程支持多客户端同时在线。
java
package V4;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
/**
* 聊天室服务端核心类
* 端口监听、客户端连接管理、消息解析、消息转发
*/
public class Server {
ServerSocket ss = null;
// 全局在线用户列表:存储所有已登录的客户端连接
ArrayList<ClientUser> socketlist<>();
// 初始化服务端:绑定固定端口54321
public void init() {
try {
ss = new ServerSocket(54321);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 监听客户端连接:阻塞方法,等待客户端接入
public Socket listen() {
try {
return ss.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 统一消息发送工具:服务端向客户端转发消息
public void send(String message, OutputStream os){
try {
// 遵循长度+内容协议发送消息
int len = message.getBytes().length;
os.write(len);
os.write(message.getBytes());
os.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 消息解析方法:读取客户端发送的原始字节流
* 根据消息类型(1/2)分别解析私聊、群聊数据
* 返回带标识的字符串,用于后续转发逻辑处理
*/
public String receive(InputStream in) {
try {
int type = in.read(); // 读取消息类型
if(type == 1){
// 解析私聊消息
int fidLen = in.read (); // 好友名称长度
byte [] fid = new byte [fidLen];
in.read (fid);
String fidStr = new String(fid); // 目标好友账号
int mlen = in.read (); // 消息长度
byte [] msgs = new byte [mlen];
in.read (msgs);
String msg = new String (msgs); // 消息内容
// 自定义分隔符# 拼接数据,方便后续拆分处理
return "1"+"#"+fidStr+"#"+ msg;
}else {
// 解析群聊消息
int len = in.read();
byte[] msgs = new byte[len];
in.read(msgs);
String msg = new String(msgs);
return "2"+"#"+msg;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
}
/**
* 处理客户端所有消息的核心方法
* 每个客户端连接单独开启一个子线程,实现多客户端并发
*/
public void proClientMsg(Socket socket) {
new Thread(() -> {
// 1. 处理用户登录,获取登录账号
String account = prologin(socket);
// 2. 将在线用户存入集合
ClientUser clientUser = new ClientUser(socket,account);
socketlist.add(clientUser);
// 3. 循环监听当前客户端的所有消息
while (true) {
try {
InputStream in = clientUser.socket.getInputStream();
String msg = receive(in);
String[] datas=msg.split("#"); // 拆分解析后的消息数据
String type = datas[0];
// 私聊消息转发逻辑
if(type.equals("1")){
String fid = datas[1]; // 目标好友账号
String msg1 = datas[2]; // 消息内容
// 遍历在线用户列表,精准匹配目标用户
for (ClientUser otCU : socketlist) {
if(otCU.account.equals(fid)){
// 拼接发送者信息,转发给目标用户
msg1 = clientUser.account+"说:"+msg1;
send(msg1,otCU.socket.getOutputStream());
break;
}
}
}else {
// 群聊消息转发逻辑:广播给所有在线用户(排除自己)
String msg1 = datas[1];
msg1 = clientUser.account+"说:"+msg1;
for (ClientUser otCU : socketlist) {
Socket socket1 = otCU.socket;
if(socket1 != clientUser.socket){
send(msg1,socket1.getOutputStream());
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
// 处理用户登录:读取客户端发送的账号信息
public String prologin(Socket socket) {
try {
InputStream in = socket.getInputStream();
int len = in.read();
byte[] msgs = new byte[len];
in.read(msgs);
String account = new String (msgs);
System.out.println ("用户名:"+account+" 上线了 ");
return account;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 服务端启动入口
static void main() {
Server server = new Server();
server.init();
// 循环监听客户端连接(主线程阻塞,单独开启线程)
new Thread(()->{
while (true){
System.out.println("监听连接中~");
Socket s = server.listen();
System.out.println(s.getInetAddress()+":"+s.getPort()+"连接成功!");
server.proClientMsg(s); // 处理该客户端的所有后续逻辑
}
}).start();
}
}
5.1 服务端核心关键点备注
- 多线程并发:每个客户端连接单独创建子线程处理消息,避免单线程阻塞,支持多用户同时在线
- 消息转发机制:所有消息不直接点对点传输,必须经过服务端中转,实现统一管理
- 私聊精准匹配:通过遍历在线用户集合,匹配账号名称,实现定向消息发送
- 群聊广播过滤:转发群聊消息时排除发送者自身,避免自己收到自己的消息
- 数据分隔处理:使用
#作为自定义分隔符,拆分消息类型、目标账号、消息内容,解析简单高效
六、可视化UI界面实现(ClientUI)
基于Swing实现桌面可视化窗口,包含聊天记录展示区、消息输入框、发送按钮,同时开启子线程实时接收服务端消息,避免UI阻塞。
java
package V4;
import javax.swing.*;
import java.awt.*;
/**
* 聊天室可视化客户端界面
* 基于Swing实现,绑定消息收发事件
*/
public class ClientUI extends JFrame {
Client client = new Client(); // 绑定客户端核心通信对象
public static void main() {
ClientUI clientUI = new ClientUI();
clientUI.showUI();
}
// 初始化展示UI窗口
public void showUI() {
// 启动客户端连接+登录
client.init();
// 窗口基础配置
JFrame jf = new JFrame("小小聊天室");
jf.setSize(500, 550);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setLocationRelativeTo(null); // 窗口居中
// 聊天记录展示区域(带滚动条)
JTextArea msgArea = new JTextArea();
JScrollPane scrollPane = new JScrollPane(msgArea);
scrollPane.setPreferredSize(new Dimension(0, 470));
// 消息输入框、发送按钮
JTextField msgField = new JTextField();
msgField.setBackground(Color.pink);
JButton sendBtn = new JButton("发送");
msgField.setPreferredSize(new Dimension(370, 0));
sendBtn.setPreferredSize(new Dimension(100, 0));
// 组件布局添加
jf.add(scrollPane, BorderLayout.NORTH);
jf.add(msgField, BorderLayout.WEST);
jf.add(sendBtn, BorderLayout.EAST);
jf.setVisible(true);
// 绑定发送按钮点击事件
sendBtn.addActionListener(e -> sendMsg(msgArea,msgField));
// 开启子线程:实时接收服务端消息,刷新UI(防止主线程阻塞)
new Thread(() -> {
while (true) {
String msg = client.readMsg();
msgArea.append(msg+"\n"); // 追加消息到聊天界面
}
}).start();
}
// 发送消息事件:点击按钮触发
public void sendMsg(JTextArea msgArea, JTextField msgField) {
String msg = msgField.getText();
// 本地先展示自己发送的消息
msgArea.append("我说:"+msg+"\n");
// 调用客户端方法发送消息到服务端
client.send(msg);
msgField.setText(""); // 清空输入框
}
}
6.1 UI核心关键点备注
- 子线程接收消息:消息接收是阻塞循环操作,必须放在子线程,否则会卡死Swing主线程(UI刷新线程)
- 本地消息回显:自己发送的消息本地直接展示,服务端只转发他人消息,提升体验
- 滚动文本域:使用
JScrollPane包裹文本域,实现聊天记录滚动查看
七、项目启动流程总结
- 启动 Server服务端,监听54321端口,等待客户端连接
- 启动 ClientUI客户端,输入账号完成登录,服务端记录在线用户
- 客户端输入消息:无@为群聊,@好友+空格+内容为私聊
- 客户端封装协议消息发送至服务端
- 服务端解析消息类型,转发至对应目标用户
- 客户端子线程接收转发消息,刷新UI展示聊天内容