跳到主要内容基于 Java Socket 实现多人在线聊天系统 | 极客日志Javajava
基于 Java Socket 实现多人在线聊天系统
介绍使用 Java Socket 构建多人在线聊天系统的方案。系统采用 C/S 架构,服务端监听端口处理多客户端连接,客户端基于 Swing 开发图形界面。核心功能包括用户注册登录、群发消息及在线用户查询。文章涵盖 Socket 通信原理、多线程并发处理、线程安全同步机制及自定义通信协议设计,适合初学者理解网络编程与 GUI 开发。
疯疯癫癫18 浏览 基于 Java Socket 实现多人在线聊天系统
在网络编程学习中,Socket(套接字)是实现 TCP/IP 通信的核心载体。本文将搭建一个支持注册、登录、群发消息、在线用户查询的多人在线聊天系统,涵盖客户端/服务端通信、Swing 图形界面、多线程处理等核心知识点。
一、系统整体架构
本系统采用经典的 C/S(客户端 - 服务端)架构,基于 TCP 协议实现可靠的字节流通信:
- 服务端:单端口监听(9999),为每个客户端连接创建独立线程,维护在线用户列表、处理登录/注册/消息转发逻辑。
- 客户端:提供 Swing 图形界面,支持登录注册、群发消息、查询在线用户,通过多线程处理消息收发(避免 UI 阻塞)。
- 通信协议:自定义指令格式(
指令码:参数),例如 1:账号,密码 代表登录请求,4:消息内容 代表群发消息。
核心技术栈
- 网络通信:Java Socket(ServerSocket/Socket)
- 界面开发:Swing(JFrame/JPanel/JList 等组件)
- 多线程:Thread/Runnable(处理并发连接、异步消息收发)
- 数据存储:内存集合(ArrayList/HashMap)维护用户/连接信息(简易版,实际可替换为数据库)
二、核心代码解析
1. 服务端核心类
(1)消息指令常量类(Message.java)
定义通信指令码,统一客户端和服务端的指令格式,避免硬编码:
package yjq0125.Server;
public class Message {
public static final int login = 1;
public static final int register = 2;
public static final int chatprivate = 3;
public static final ;
;
;
}
int
chatall
=
4
public
static
final
int
logout
=
5
public
static
final
int
search
=
6
(2)服务端主线程(Server.java)
启动 ServerSocket 监听 9999 端口,为每个新客户端连接创建独立的 ServerThread 线程处理通信:
package yjq0125.Server;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws Exception {
Server server = new Server();
server.startServer();
}
public void startServer() throws Exception {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("启动服务器,监听 9999 端口...");
int ID = 1;
while (true) {
Socket socket = serverSocket.accept();
System.out.println("客户端" + ID++ + "已连接,IP:" + socket.getInetAddress());
ServerThread serverThread = new ServerThread(socket);
new Thread(serverThread).start();
}
}
}
(3)客户端连接处理线程(ServerThread.java)
核心逻辑类,处理客户端的登录、注册、群发、查询在线用户等请求:
package yjq0125.Server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class ServerThread implements Runnable {
Socket socket;
OutputStream os;
InputStream is;
public static ArrayList<User> userArrList = new ArrayList<>();
boolean iflogin = false;
String id;
public static Map<String, Socket> socketmap = new HashMap<>();
public static Map<String, Boolean> checklogin = new HashMap<>();
ServerThread(Socket socket) throws IOException {
this.socket = socket;
os = socket.getOutputStream();
is = socket.getInputStream();
}
@Override
public void run() {
while (true) {
try {
String s = readMes();
String[] a = s.split(":");
int choice = Integer.parseInt(a[0]);
switch (choice) {
case Message.login:
handleLogin(a[1]);
break;
case Message.register:
handleRegister(a[1]);
break;
case Message.chatall:
handleChatAll(a[1]);
break;
case Message.logout:
handleLogout();
break;
case Message.search:
handleSearchOnline();
break;
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}
}
private void handleLogin(String params) throws Exception {
String[] mes = params.split(",");
String account = mes[0];
String password = mes[1];
boolean ifnameright = false;
User user1 = new User();
synchronized (userArrList) {
for (User user : userArrList) {
if (user.account.equals(account)) {
ifnameright = true;
user1 = user;
break;
}
}
if (!ifnameright) {
sendMes(socket, "账号错误登录失败");
} else if (user1.password.equals(password)) {
Boolean loginStatus = checklogin.get(account);
if (Boolean.TRUE.equals(loginStatus)) {
sendMes(socket, "账号已登录,登录失败");
} else {
id = user1.idname;
checklogin.put(id, true);
socketmap.put(id, socket);
sendMes(socket, "登录成功");
iflogin = true;
}
} else {
sendMes(socket, "密码错误登录失败");
}
}
}
private void handleRegister(String params) throws Exception {
String[] mes = params.split(",");
String account = mes[0];
String password = mes[1];
String idname = mes[2];
boolean ifsuccess = true;
synchronized (userArrList) {
for (User user : userArrList) {
if (user.account.equals(account)) {
ifsuccess = false;
break;
}
}
if (ifsuccess) {
userArrList.add(new User(account, password, idname));
sendMes(socket, "注册成功");
} else {
sendMes(socket, "账号已存在");
}
}
}
private void handleChatAll(String msg) throws Exception {
if (!iflogin) {
sendMes(socket, "没有登录成功不能发送消息");
return;
}
String sendMsg = "[" + id + "] 群发:" + msg;
synchronized (socketmap) {
for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
Socket socket1 = entry.getValue();
if (!socket.equals(socket1)) {
sendMes(socket1, sendMsg);
}
}
}
}
private void handleLogout() throws Exception {
synchronized (checklogin) {
checklogin.put(id, false);
synchronized (socketmap) {
socketmap.remove(id);
}
sendMes(socket, "退出登录成功");
iflogin = false;
socket.close();
}
}
private void handleSearchOnline() throws Exception {
synchronized (socketmap) {
for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
sendMes(socket, entry.getKey());
}
sendMes(socket, "end");
}
}
public String readMes() throws Exception {
byte[] b = new byte[1024];
int actualLen = is.read(b);
if (actualLen == -1) {
throw new IOException("服务器连接已断开");
}
String mes = new String(b, 0, actualLen, StandardCharsets.UTF_8);
return mes.replace("\r\n", "").trim();
}
public void sendMes(Socket socket, String mes) throws Exception {
OutputStream outputStream = socket.getOutputStream();
String sendStr = mes + "\r\n";
outputStream.write(sendStr.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
}
2. 客户端核心类
(1)客户端 Socket 通信类(MClient.java)
封装 Socket 连接、消息读写、退出等基础通信能力:
package yjq0125.Client;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class MClient {
public OutputStream os;
public InputStream is;
public boolean iflogin = false;
Socket client;
Map<String, String> map = new HashMap<>();
public void startClient() throws Exception {
client = new Socket("127.0.0.1", 9999);
os = client.getOutputStream();
is = client.getInputStream();
}
public String readMes() throws Exception {
byte[] b = new byte[1024];
int actualLen = is.read(b);
if (actualLen == -1) {
throw new IOException("服务器连接已断开");
}
String mes = new String(b, 0, actualLen, StandardCharsets.UTF_8);
return mes.replace("\r\n", "").trim();
}
public void writeMes(String s) throws Exception {
String str = s + "\r\n";
os.write(str.getBytes());
os.flush();
}
public void logout() throws Exception {
if (iflogin) {
writeMes("5:");
iflogin = false;
}
if (os != null) os.close();
if (is != null) is.close();
if (client != null) client.close();
}
}
(2)登录注册界面(GameUI.java)
Swing 实现图形化登录注册界面,通过子线程处理网络请求(避免 UI 阻塞):
package yjq0125.Client;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class GameUI {
JTextField account;
JTextField password;
MClient mClient;
JFrame jFrame;
GameUI() throws Exception {
mClient = new MClient();
mClient.startClient();
}
public static void main(String[] args) throws Exception {
GameUI gameUI = new GameUI();
gameUI.initUI();
}
public void initUI() {
jFrame = new JFrame();
jFrame.setTitle("登录界面");
jFrame.setSize(500, 600);
jFrame.setLocationRelativeTo(null);
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.setLayout(new FlowLayout());
ImageIcon imageIcon = new ImageIcon("photo/01.jpg");
Image target = imageIcon.getImage().getScaledInstance(500, 400, Image.SCALE_SMOOTH);
JLabel jLabel = new JLabel(new ImageIcon(target));
jFrame.add(jLabel);
JLabel usernameLabel = new JLabel("账号:");
jFrame.add(usernameLabel);
account = new JTextField();
account.setPreferredSize(new Dimension(420, 30));
jFrame.add(account);
JLabel pwdLabel = new JLabel("密码:");
jFrame.add(pwdLabel);
password = new JTextField();
password.setPreferredSize(new Dimension(420, 30));
jFrame.add(password);
JLabel idnameLabel = new JLabel("用户名:(登录时不用填)");
jFrame.add(idnameLabel);
JTextField idname = new JTextField();
idname.setPreferredSize(new Dimension(280, 30));
jFrame.add(idname);
JButton login = new JButton("登录");
JButton register = new JButton("注册");
jFrame.add(login);
jFrame.add(register);
login.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String username = account.getText().trim();
String userpassword = password.getText().trim();
if (username.isEmpty() || userpassword.isEmpty()) {
JOptionPane.showMessageDialog(jFrame, "账号和密码不能为空", "提示", JOptionPane.ERROR_MESSAGE);
return;
}
new Thread(() -> {
try {
mClient.writeMes("1:" + username + "," + userpassword);
String mes = mClient.readMes();
SwingUtilities.invokeAndWait(() -> {
account.setText("");
password.setText("");
if (mes.contains("失败")) {
JOptionPane.showMessageDialog(jFrame, mes, "登录失败", JOptionPane.ERROR_MESSAGE);
} else if (mes.equals("登录成功")) {
JOptionPane.showMessageDialog(jFrame, mes, "登录成功", JOptionPane.INFORMATION_MESSAGE);
mClient.iflogin = true;
new ChatUI(username, mClient).initui();
}
});
} catch (Exception ex) {
try {
SwingUtilities.invokeAndWait(() -> {
JOptionPane.showMessageDialog(jFrame, "登录失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
});
} catch (Exception innerEx) {
innerEx.printStackTrace();
}
}
}).start();
}
});
register.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String username = account.getText().trim();
String userpassword = password.getText().trim();
String idnameText = idname.getText().trim();
if (username.isEmpty() || userpassword.isEmpty() || idnameText.isEmpty()) {
JOptionPane.showMessageDialog(jFrame, "账号、密码、用户名不能为空", "提示", JOptionPane.ERROR_MESSAGE);
return;
}
new Thread(() -> {
try {
mClient.writeMes("2:" + username + "," + userpassword + "," + idnameText);
String mes = mClient.readMes();
SwingUtilities.invokeAndWait(() -> {
account.setText("");
password.setText("");
idname.setText("");
if (mes.equals("注册成功")) {
JOptionPane.showMessageDialog(jFrame, mes, "注册成功", JOptionPane.INFORMATION_MESSAGE);
mClient.map.put(username, idnameText);
} else {
JOptionPane.showMessageDialog(jFrame, mes, "注册失败", JOptionPane.ERROR_MESSAGE);
}
});
} catch (Exception ex) {
try {
SwingUtilities.invokeAndWait(() -> {
JOptionPane.showMessageDialog(jFrame, "注册失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
});
} catch (Exception innerEx) {
innerEx.printStackTrace();
}
}
}).start();
}
});
jFrame.setVisible(true);
}
}
(3)聊天界面(ChatUI.java)
支持群发消息、查询在线用户列表,通过多线程实时接收服务端消息:
package yjq0125.Client;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ChatUI {
public String idname;
public MClient mClient;
JTextArea jTextArea;
private DefaultListModel<String> onlineUserModel;
private JList<String> onlineUserList;
boolean ifsearch = false;
ChatUI(String idname, MClient mClient) {
this.idname = idname;
this.mClient = mClient;
onlineUserModel = new DefaultListModel<>();
onlineUserList = new JList<>(onlineUserModel);
onlineUserList.setFont(new Font("楷体", Font.PLAIN, 18));
onlineUserList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
public void initui() {
JFrame jf = new JFrame(idname);
jf.setSize(new Dimension(500, 600));
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setLocationRelativeTo(null);
JPanel centerjPanel = new JPanel();
jf.add(centerjPanel, BorderLayout.CENTER);
JPanel westjPanel = new JPanel();
westjPanel.setLayout(new BoxLayout(westjPanel, BoxLayout.Y_AXIS));
westjPanel.setBackground(Color.GREEN);
westjPanel.setPreferredSize(new Dimension(150, 600));
jf.add(westjPanel, BorderLayout.WEST);
JButton search = new JButton("查找在线列表");
westjPanel.add(search);
JScrollPane listScroll = new JScrollPane(onlineUserList);
listScroll.setPreferredSize(new Dimension(90, 500));
westjPanel.add(listScroll);
jTextArea = new JTextArea();
jTextArea.setFont(new Font("楷体", Font.BOLD, 24));
jTextArea.setPreferredSize(new Dimension(400, 450));
jTextArea.setEditable(false);
centerjPanel.add(jTextArea);
JLabel jLabel = new JLabel("消息栏:");
centerjPanel.add(jLabel);
JTextField jTextField = new JTextField();
jTextField.setPreferredSize(new Dimension(200, 100));
centerjPanel.add(jTextField);
JButton button = new JButton("发送");
centerjPanel.add(button);
new Thread(() -> {
while (!ifsearch) {
try {
String mes = mClient.readMes();
SwingUtilities.invokeLater(() -> {
jTextArea.append("收到:" + mes + "\n");
jTextArea.setCaretPosition(jTextArea.getText().length());
});
} catch (Exception e) {
SwingUtilities.invokeLater(() -> {
jTextArea.append("连接异常:" + e.getMessage() + "\n");
});
break;
}
}
}).start();
search.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ifsearch = true;
onlineUserModel.clear();
new Thread(() -> {
try {
mClient.writeMes("6:");
while (true) {
String mes = mClient.readMes();
if (mes.equals("end")) break;
if (!mes.equals(idname)) {
SwingUtilities.invokeLater(() -> {
onlineUserModel.addElement(mes);
});
}
}
ifsearch = false;
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(jf, "查询在线列表失败:" + ex.getMessage());
});
}
}).start();
}
});
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String mes = jTextField.getText().trim();
if (mes.isEmpty()) return;
try {
mClient.writeMes("4:" + mes);
SwingUtilities.invokeLater(() -> {
jTextArea.append(idname + "群发:" + mes + "\n");
jTextArea.setCaretPosition(jTextArea.getText().length());
jTextField.setText("");
});
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
});
jf.setVisible(true);
}
}
3. 实体类(User.java)
package yjq0125.Server;
public class User {
String account;
String password;
String idname;
public User() {}
public User(String account, String password, String idname) {
this.account = account;
this.password = password;
this.idname = idname;
}
}
三、运行步骤
- 启动服务端:运行 Server.java 的 main 方法,控制台输出'启动服务器,监听 9999 端口...'。
- 启动客户端:运行 GameUI.java 的 main 方法,弹出登录界面。
- 注册账号:输入账号、密码、用户名,点击'注册',提示'注册成功'即可。
- 登录系统:输入注册的账号、密码,点击'登录',成功后打开聊天界面。
- 功能测试:
- 多开客户端,用不同账号登录;
- 点击'查找在线列表',可看到其他在线用户;
- 在消息栏输入内容,点击'发送',其他客户端可收到群发消息。
四、核心知识点总结
- Socket 通信:ServerSocket 监听端口,Socket 建立客户端连接,通过 InputStream/OutputStream 读写字节流。
- 多线程:服务端为每个客户端创建独立线程,避免单连接阻塞;客户端用子线程处理网络请求,避免 Swing UI 卡顿。
- 线程安全:使用
synchronized 关键字保护共享集合(userArrList、socketmap),避免并发修改异常。
- Swing UI 规范:所有 UI 组件更新必须在 EDT(事件调度线程)中执行(
SwingUtilities.invokeLater/invokeAndWait)。
- 自定义通信协议:通过'指令码 + 参数'的格式,让客户端和服务端明确通信意图,解耦逻辑。
五、扩展优化方向
- 功能扩展:实现私聊功能(基于
chatprivate=3 指令)、消息记录持久化(写入文件/数据库)、用户头像等。
- 性能优化:使用线程池替代手动创建线程,避免大量客户端连接导致线程爆炸;设置 Socket 超时时间,避免无限阻塞。
- 异常处理:完善断连重连机制、输入合法性校验(如密码加密)。
- 界面美化:使用 JavaFX 替代 Swing,实现更现代化的 UI。
本系统完整覆盖了 Java Socket 网络编程、多线程、Swing 界面开发的核心知识点,适合入门级学习者理解 C/S 架构的通信原理。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online