在网络编程学习中,Socket(套接字)是实现 TCP/IP 通信的核心基础。本文将手把手教你搭建一个支持注册、登录、私聊、群发、在线用户查询 的多人聊天室系统,全程基于 Java 原生 Socket 和 Swing 实现,无第三方框架依赖,适合 Java 网络编程入门学习。
一、系统整体设计
1. 核心功能
- 用户注册 / 登录(支持账号唯一性校验、登录状态防重复)
- 私聊功能:指定在线用户发送一对一消息
- 群发功能:向所有在线用户广播消息
- 在线用户查询:展示当前登录的所有用户列表
- 安全退出:注销登录状态,释放连接资源
2. 技术栈
- 网络通信:Java Socket(TCP 协议)
- 多线程:Thread/Runnable 处理多客户端并发
- UI 界面:Swing 实现图形化交互
- 数据存储:集合(ArrayList/HashMap)存储用户、连接信息
3. 架构设计
采用经典的C/S(客户端 - 服务器)架构:
- 服务端(Server):监听 9999 端口,为每个客户端创建独立线程(SeverThread),处理登录、注册、消息转发等核心逻辑
- 客户端(Client):分为登录 UI(GameUI)、聊天 UI(chatUI),负责用户交互和消息收发
二、核心代码拆解
1. 公共常量定义(Message.java)
首先定义指令常量,统一客户端和服务端的指令交互格式,避免硬编码:
java
package yjq0125.Sever;
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. 用户实体类(User.java)
封装用户核心信息,包含账号、密码、用户名(展示用):
java
package yjq0125.Sever;
public class User {
String account; //账号
String password; //密码
int id; //预留ID
String idname; //用户名(展示名)
//空构造
public User(){}
//带参构造(注册时使用)
public User(String account,String password,String idname) {
this.account=account;
this.password = password;
this.idname=idname;
}
}
3. 服务端核心实现
(1)服务端启动类(Msever.java)
创建 ServerSocket 监听端口,接收客户端连接并创建独立线程处理:
java
package yjq0125.Sever;
import java.net.ServerSocket;
import java.net.Socket;
public class Msever {
public static void main(String[] args) throws Exception {
Msever msever=new Msever();
msever.startSever();
}
public void startSever() throws Exception {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("启动服务器,监听9999端口...");
int ID = 1;
while (true) {
// 阻塞等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端"+ID+++"已连接");
// 为每个客户端创建独立线程,避免单线程阻塞
SeverThread severThread=new SeverThread(socket);
new Thread(severThread).start();
}
}
}
(2)服务端线程类(SeverThread.java)
核心逻辑类,处理客户端的所有指令请求,关键要点:
- 用
ArrayList<User>存储所有注册用户 - 用
HashMap<String, Socket>维护在线用户的 Socket 连接 - 用
HashMap<String, Boolean>标记用户登录状态,防止重复登录 - 所有共享集合操作加
synchronized保证线程安全
java
package yjq0125.Sever;
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 SeverThread 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<>();//在线用户-Socket映射
public static Map<String, Boolean> checklogin = new HashMap<>();//用户-登录状态映射
// 构造方法:初始化Socket和IO流
SeverThread(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) {
// 1. 登录逻辑
case Message.login: {
String[] mes = a[1].split(",");
String account = mes[0];
String password = mes[1];
boolean ifnameright = false;
boolean ifpasswordright = 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)) {
ifpasswordright = true;
}
if (ifpasswordright) {
// 校验是否已登录
Boolean loginSatus = checklogin.get(account);
if (Boolean.TRUE.equals(loginSatus)) {
sendMes(socket, "账号已登录,登录失败");
} else {
id = user1.idname;
checklogin.put(id, true);
socketmap.put(id, socket);
sendMes(socket, "登录成功");
iflogin = true;
}
} else {
sendMes(socket, "密码错误登录失败");
}
}
}
break;
}
// 2. 注册逻辑
case Message.register: {
String[] mes = a[1].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, "账号已存在");
}
}
break;
}
// 3. 私聊逻辑
case Message.chatprivate: {
if (!iflogin) {
sendMes(socket, "没有登录成功不能发送消息");
} else {
String[] mes = a[1].split(",");
String idname = mes[0];
String message = "["+id+"]"+"私聊:"+mes[1];
Socket socket1 = null;
// 查找目标用户的Socket
for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
if (idname.equals(entry.getKey())) {
socket1 = entry.getValue();
break;
}
}
if(socket1 != null) sendMes(socket1, message);
}
break;
}
// 4. 群发逻辑
case Message.chatall: {
if (!iflogin) {
sendMes(socket, "没有登录成功不能发送消息");
} else {
String mes = "[" + id + "]群发:" + a[1];
synchronized (socketmap) {
// 遍历所有在线用户,排除自己
for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
Socket socket1 = entry.getValue();
if (!socket.equals(socket1)) {
sendMes(socket1, mes);
}
}
}
}
break;
}
// 5. 退出登录
case Message.logout: {
synchronized (checklogin) {
if (!iflogin) {
sendMes(socket, "未登录,无需退出");
break;
}
if (checklogin.get(id)) {
checklogin.put(id, false);
socketmap.remove(id);
sendMes(socket, "退出登录成功");
iflogin = false;
}
}
break;
}
// 6. 查询在线用户
case Message.search: {
String idlist = "";
synchronized (socketmap) {
for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
idlist += (entry.getKey() + ",");
}
sendMes(socket, idlist);
}
break;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// 读取客户端消息(处理粘包/空字符问题)
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();
}
}
4. 客户端实现
(1)客户端通信类(MClient.java)
封装 Socket 连接、消息读写、退出逻辑,作为客户端和服务端的通信桥梁:
java
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,核心要点:
- 子线程处理网络请求,避免 UI 阻塞
- Swing 组件更新必须在 EDT 线程(
SwingUtilities.invokeAndWait) - 输入校验(非空判断)
java
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;
JTextField idname ;
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(3);
FlowLayout flow = new FlowLayout();
jFrame.setLayout(flow);
// 背景图片(可自行替换路径)
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);
idname = new JTextField();
idname.setPreferredSize(new Dimension(280, 30));
jFrame.add(idname);
// 登录按钮
JButton login = new JButton("登录");
jFrame.add(login);
// 注册按钮
JButton register = new JButton("注册");
jFrame.add(register);
// 登录按钮事件
login.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String username = account.getText();
String userpassword = password.getText();
if (username.trim().equals("") || userpassword.trim().equals("")) {
JOptionPane.showMessageDialog(jFrame, "账号和密码不能为空", "提示", JOptionPane.ERROR_MESSAGE);
return;
}
// 子线程处理网络请求
new Thread(() -> {
try {
mClient.writemes("1:" + username + "," + userpassword);
String mes = mClient.readmes();
// EDT线程更新UI
SwingUtilities.invokeAndWait(() -> {
account.setText("");
password.setText("");
if (mes.equals("登录成功")) {
JOptionPane.showMessageDialog(jFrame, mes, "登录成功", JOptionPane.INFORMATION_MESSAGE);
mClient.iflogin = true;
// 打开聊天界面
new chatUI(username, mClient).initui();
} 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();
}
ex.printStackTrace();
}
}).start();
}
});
// 注册按钮事件
register.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String username = account.getText();
String userpassword = password.getText();
String idnameText = idname.getText();
if (username.trim().equals("") || userpassword.trim().equals("") || idnameText.trim().equals("")) {
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();
}
ex.printStackTrace();
}
}).start();
}
});
jFrame.setVisible(true);
}
}
(3)聊天界面(chatUI.java)
实现消息收发、在线用户查询、私聊 / 群发切换:
java
package yjq0125.Client;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.concurrent.atomic.AtomicBoolean;
public class chatUI {
public String idname;
public MClient mClient;
JTextArea jTextArea;
private DefaultListModel<String> onlineUserModel;
private JList<String> onlineUserList;
private AtomicBoolean isSearching = new AtomicBoolean(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(600, 600));
jf.setDefaultCloseOperation(3);
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(new Runnable() {
@Override
public void run() {
while (true) {
try {
String mes = mClient.readmes();
// 处理在线用户列表响应
if (isSearching.get()) {
String[] idlist=mes.split(",");
for(String s:idlist) {
if(!s.equals(idname)&&!s.equals("")) {
SwingUtilities.invokeLater(() -> onlineUserModel.addElement(s));
}
}
isSearching.set(false);
} else {
// 处理聊天消息
SwingUtilities.invokeLater(() -> {
jTextArea.append(idname+"收到:" + mes + "\n");
jTextArea.setCaretPosition(jTextArea.getText().length());
});
}
} catch (Exception e) {
SwingUtilities.invokeLater(() -> {
jTextArea.append("连接异常:" + e.getMessage() + "\n");
});
e.printStackTrace();
break;
}
}
}
}).start();
// 在线用户查询按钮事件
search.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (isSearching.get()) {
JOptionPane.showMessageDialog(jf, "正在查询在线列表,请稍等");
return;
}
isSearching.set(true);
onlineUserModel.clear();
try {
mClient.writemes("6:");
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
});
// 发送按钮事件(私聊/群发切换)
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String mes = jTextField.getText().trim();
if(mes.isEmpty()) return;
String target=onlineUserList.getSelectedValue();
try {
if(target==null) {
// 群发
mClient.writemes("4:" + mes);
SwingUtilities.invokeLater(() -> {
jTextArea.append(idname + "群发:" + mes + "\n");
jTextArea.setCaretPosition(jTextArea.getText().length());
jTextField.setText("");
});
} else {
// 私聊
mClient.writemes("3:"+target+","+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);
}
}
三、运行步骤
- 启动服务端:运行Msever.java的 main 方法,控制台输出 "启动服务器,监听 9999 端口..."
- 启动客户端:运行GameUI.java的 main 方法,弹出登录界面
- 注册账号:输入账号、密码、用户名,点击 "注册",提示 "注册成功"
- 登录:输入注册的账号、密码,点击 "登录",提示 "登录成功" 后弹出聊天界面
- 多客户端测试:重复步骤 2-4,启动多个客户端,实现私聊 / 群发 / 在线查询
四、核心知识点总结
- Socket 通信:ServerSocket 监听端口,Socket 建立客户端 - 服务端连接,通过 IO 流收发数据
- 多线程并发 :每个客户端对应一个线程,避免单线程阻塞;共享集合加
synchronized保证线程安全 - Swing UI 编程 :EDT 线程更新组件,避免 UI 卡顿;使用
DefaultListModel动态更新列表 - 协议设计 :自定义指令格式(
指令码:参数),实现客户端和服务端的标准化交互
五、优化方向
- 密码加密:当前密码明文传输,可使用 MD5/SHA 加密
- 断线重连:客户端检测连接断开后自动重连
- 历史消息:增加文件 / 数据库存储聊天记录
- 异常处理:完善 try-catch,避免单个客户端异常导致服务端崩溃
- UI 美化:使用更现代的 UI 框架(如 JavaFX)替代 Swing
本教程完整实现了一个基础的多人聊天室系统,覆盖了 Java Socket、多线程、Swing 核心知识点,适合入门学习和二次开发。如果有问题,欢迎在评论区交流~