基于 Java Socket 的多线程网络聊天程序

发布于:2025-05-16 ⋅ 阅读:(13) ⋅ 点赞:(0)

在网络编程领域,Java Socket 编程是实现客户端与服务器端通信的重要方式。今天,我们来深入探讨一个基于 Java Socket 的多线程聊天程序,剖析其代码实现、核心机制以及测试场景。

一、代码实现详解

(一)服务器端

  1. 整体架构:服务器端代码定义了关键成员变量,如端口号、ServerSocket、用于日志显示的 JTextArea、界面框架 JFrame 以及存储客户端信息的 Map 等。通过一系列方法,包括界面初始化(initUI)、服务器启动(start)、日志记录(log)、服务器关闭(shutdown)等,以及内部类 ClientHandler,构建起完整的服务器功能体系。
  2. 界面初始化:initUI 方法创建 JFrame 窗口,设置标题为 “Socket Chat Server”,关闭操作设为 EXIT_ON_CLOSE ,大小为 700x500 ,布局为 BorderLayout。添加不可编辑的 JTextArea 用于显示日志,并放入 JScrollPane 后添加到窗口中间区域,同时设置窗口关闭监听器。
  3. 服务器启动:start 方法中,先记录服务器启动和等待客户端连接的日志。创建 serverThread 线程,在其中创建 ServerSocket 并绑定到指定端口(12345)。通过 while (true) 循环,利用 serverSocket.accept () 持续监听客户端连接,一旦有连接,记录日志并创建 ClientHandler 线程处理通信。
  4. 日志记录:log 方法借助 SwingUtilities.invokeLater 确保日志在 Swing 事件调度线程中更新,将信息追加到 logTextArea 并定位光标到末尾。
  5. 服务器关闭:shutdown 方法记录关闭日志,中断 serverThread 线程,关闭 ServerSocket ,遍历关闭所有客户端输出流,记录成功关闭日志后延迟退出。
  6. 客户端处理线程(ClientHandler 类):run 方法开始记录线程启动日志。获取客户端连接的输入输出流,读取客户端名称存入 clients 映射表,记录连接日志并广播客户端加入消息。在消息处理中,循环读取消息,若为私信(以 @开头),解析目标用户名并发送私信;若为普通消息则群发。当客户端连接断开时,清理资源,从 clients 移除信息,广播离开消息并关闭相关资源。

(二)客户端

  1. 整体架构:客户端代码定义了服务器地址、端口号、Socket、输入输出流对象、界面框架 JFrame、显示消息的 JTextArea、输入消息的 JTextField 以及客户端名称等成员变量,涵盖客户端初始化、界面初始化、消息发送、消息接收等功能。
  2. 客户端初始化:构造函数中创建 Socket 连接服务器,获取输入输出流,发送客户端名称,调用 initUI 初始化界面,并启动 MessageReceiver 线程接收服务器消息。
  3. 界面初始化:initUI 方法创建 JFrame 窗口,设置标题包含客户端名称,关闭操作设为 EXIT_ON_CLOSE ,大小为 600x400 ,布局为 BorderLayout。添加显示消息的 JTextArea(不可编辑)到 JScrollPane 后放入窗口中间区域,创建输入消息的 JTextField 和 “发送” 按钮,添加事件监听实现消息发送功能,同时在 messageArea 显示欢迎和私信格式提示信息。
  4. 消息发送:sendMessage 方法将输入框消息发送给服务器,若消息为 “exit”,则关闭 Socket 并销毁窗口。
  5. 消息接收线程(MessageReceiver 类):通过循环从输入流读取服务器消息,追加显示到 messageArea,连接断开时记录相关信息。

二、核心机制揭秘

(一)服务器端核心机制

  1. 多线程实现:通过 serverThread 监听客户端连接,ServerSocket.accept () 方法阻塞等待连接,新连接到来时返回 Socket 对象。为每个客户端连接创建独立的 ClientHandler 线程,实现并发处理。利用 HashMap 存储客户端信息,通过线程隔离保证多线程环境下的安全。
  2. 客户端标识:客户端连接后发送名称,服务器以此作为唯一标识,通过 clients 映射表将客户端名称与输出流关联,实现定向消息发送。
  3. 消息处理机制:broadcast 方法实现群发消息,遍历客户端时排除发送者;sendPrivateMessage 方法通过解析 @用户名 消息内容格式,实现私信功能,查找目标输出流发送消息。
  4. 资源管理:客户端断开时自动清理资源并广播通知;服务器关闭时优雅中断线程、关闭连接。

(二)客户端核心机制

  1. 网络连接:通过 Socket 连接服务器,建立输入输出流通道,启动时发送客户端名称标识身份。
  2. 双线程设计:主线程负责 UI 交互和消息发送,MessageReceiver 线程独立监听服务器消息,避免 UI 阻塞。
  3. 消息处理:输入框支持普通消息和私信格式,接收到的消息自动显示在消息区域,通过 “exit” 命令优雅断开连接。

三、测试场景探究

(一)本地测试场景

本地测试时,客户端和服务器运行在同一台计算机,使用本地回环地址 127.0.0.1 连接。从网络通信原理看,此地址对应本地主机,通信通过计算机内部网络协议栈进行,无需经过实际物理网络。

(二)不同主机测试

在不同计算机分别运行服务器和客户端程序,服务器端显示的客户端 IP 地址为客户端所在计算机的实际 IP 地址(如局域网内的 192.168.x.x )。

(三)虚拟机测试

使用虚拟机软件(如 VMware、VirtualBox )创建多个虚拟机,分别部署服务器和客户端。因每个虚拟机相当于独立计算机,通信时服务器端可显示不同 IP 地址。

这个 Java Socket 多线程聊天程序,全面展示了网络通信、多线程并发、客户端 - 服务器架构、图形界面开发以及消息协议设计等多方面的知识与实践。无论是对于初学者深入理解网络编程基础,还是开发者探索更复杂应用的优化方向,都具有重要的参考价值。通过不断优化异常处理、扩展功能、提升性能和安全性,我们可以让这类程序在实际应用中发挥更大作用。

服务器端代码:

package com.example.socketchat;

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class Server {
    private static final int PORT = 12345;
    private ServerSocket serverSocket;
    private JTextArea logTextArea;
    private JFrame frame;
    private Map<String, PrintWriter> clients = new HashMap<>(); // 客户端名称 -> 输出流
    private Thread serverThread; // 服务器监听线程

    public Server() {
        initUI();
    }

    private void initUI() {
        frame = new JFrame("Socket Chat Server");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(700, 500);
        frame.setLayout(new BorderLayout());

        logTextArea = new JTextArea();
        logTextArea.setEditable(false);
        JScrollPane scrollPane = new JScrollPane(logTextArea);
        frame.add(scrollPane, BorderLayout.CENTER);

        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                shutdown();
            }
        });

        frame.setVisible(true);
    }

    public void start() {
        log("服务器已启动,监听端口: " + PORT);
        log("等待客户端连接...");

        // 在单独线程中运行服务器监听逻辑
        serverThread = new Thread(() -> {
            try {
                serverSocket = new ServerSocket(PORT);

                while (true) {
                    Socket clientSocket = serverSocket.accept();
                    log("客户端连接已建立: " + clientSocket.getInetAddress());
                    new ClientHandler(clientSocket).start();
                }
            } catch (IOException e) {
                // 检查是否是服务器关闭导致的异常
                if (serverThread.isInterrupted()) {
                    log("服务器已正常关闭");
                } else {
                    log("服务器异常: " + e.getMessage());
                }
            }
        });

        serverThread.start();
    }

    private void log(String message) {
        // 使用SwingUtilities确保日志更新在事件调度线程中执行
        SwingUtilities.invokeLater(() -> {
            logTextArea.append(message + "\n");
            logTextArea.setCaretPosition(logTextArea.getDocument().getLength());
        });
    }

    private void shutdown() {
        try {
            log("服务器正在关闭...");

            // 中断服务器线程
            if (serverThread != null && serverThread.isAlive()) {
                serverThread.interrupt();
            }

            // 关闭ServerSocket
            if (serverSocket != null && !serverSocket.isClosed()) {
                serverSocket.close();
            }

            // 关闭所有客户端连接
            for (PrintWriter writer : clients.values()) {
                writer.close();
            }

            log("服务器已成功关闭");

            // 延迟退出以确保日志显示
            Thread.sleep(500);
            System.exit(0);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class ClientHandler extends Thread {
        private Socket clientSocket;
        private BufferedReader reader;
        private PrintWriter writer;
        private String clientName;

        public ClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        @Override
        public void run() {
            log("客户端处理线程已启动: " + clientSocket.getInetAddress());
            try {
                reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                writer = new PrintWriter(clientSocket.getOutputStream(), true);

                // 接收客户端名称
                clientName = reader.readLine();
                clients.put(clientName, writer);
                log("客户端 [" + clientName + "] 已连接");
                broadcast("系统消息: [" + clientName + "] 加入了聊天室", null);

                // 处理消息
                String message;
                while ((message = reader.readLine()) != null) {
                    log("来自 [" + clientName + "] 的消息: " + message);

                    // 解析消息格式: @目标用户 消息内容
                    if (message.startsWith("@")) {
                        int spaceIndex = message.indexOf(" ");
                        if (spaceIndex > 0) {
                            String target = message.substring(1, spaceIndex);
                            String content = message.substring(spaceIndex + 1);
                            sendPrivateMessage(clientName, target, content);
                        } else {
                            writer.println("系统提示: 私信格式不正确,应为 '@用户名 消息内容'");
                        }
                    } else {
                        // 群发消息
                        broadcast("[" + clientName + "]: " + message, clientName);
                    }
                }
            } catch (IOException e) {
                log("客户端 [" + clientName + "] 连接断开: " + e.getMessage());
            } finally {
                // 清理资源
                if (clientName != null) {
                    clients.remove(clientName);
                    broadcast("系统消息: [" + clientName + "] 离开了聊天室", null);
                }
                try {
                    if (clientSocket != null) clientSocket.close();
                    if (reader != null) reader.close();
                    if (writer != null) writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        private void sendPrivateMessage(String sender, String target, String message) {
            PrintWriter targetWriter = clients.get(target);
            if (targetWriter != null) {
                targetWriter.println("私信 [" + sender + "]: " + message);
                writer.println("你对 [" + target + "] 说: " + message);
                log("[" + sender + "] 私信给 [" + target + "]: " + message);
            } else {
                writer.println("系统提示: 目标用户 [" + target + "] 不存在");
            }
        }

        private void broadcast(String message, String sender) {
            for (Map.Entry<String, PrintWriter> entry : clients.entrySet()) {
                if (sender == null || !entry.getKey().equals(sender)) {
                    entry.getValue().println(message);
                }
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            Server server = new Server();
            server.start();
        });
    }
}

客户端代码:

package com.example.socketchat;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.net.Socket;

public class Client {
    private static final String SERVER_IP = "localhost";
    private static final int PORT = 12345;
    private Socket socket;
    private BufferedReader reader;
    private PrintWriter writer;
    private JFrame frame;
    private JTextArea messageArea;
    private JTextField inputField;
    private String clientName;

    public Client(String name) {
        this.clientName = name;
        try {
            socket = new Socket(SERVER_IP, PORT);
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            writer = new PrintWriter(socket.getOutputStream(), true);

            // 发送客户端名称
            writer.println(clientName);

            initUI();
            new Thread(new MessageReceiver()).start();
        } catch (IOException e) {
            JOptionPane.showMessageDialog(null, "无法连接到服务器: " + e.getMessage(), "连接错误", JOptionPane.ERROR_MESSAGE);
            System.exit(1);
        }
    }

    private void initUI() {
        frame = new JFrame("Socket Chat - " + clientName);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(600, 400);
        frame.setLayout(new BorderLayout());

        // 消息显示区域
        messageArea = new JTextArea();
        messageArea.setEditable(false);
        messageArea.setLineWrap(true);
        messageArea.setWrapStyleWord(true);
        JScrollPane scrollPane = new JScrollPane(messageArea);
        frame.add(scrollPane, BorderLayout.CENTER);

        // 输入区域
        JPanel inputPanel = new JPanel(new BorderLayout());
        inputField = new JTextField();
        inputField.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String message = inputField.getText().trim();
                if (!message.isEmpty()) {
                    sendMessage(message);
                    inputField.setText("");
                }
            }
        });

        JButton sendButton = new JButton("发送");
        sendButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String message = inputField.getText().trim();
                if (!message.isEmpty()) {
                    sendMessage(message);
                    inputField.setText("");
                }
            }
        });

        inputPanel.add(inputField, BorderLayout.CENTER);
        inputPanel.add(sendButton, BorderLayout.EAST);
        frame.add(inputPanel, BorderLayout.SOUTH);

        frame.setVisible(true);
        inputField.requestFocus();

        // 显示欢迎信息
        messageArea.append("欢迎 [" + clientName + "] 加入聊天室!\n");
        messageArea.append("使用 '@用户名 消息内容' 格式可以发送私信\n");
    }

    private void sendMessage(String message) {
        writer.println(message);
        if ("exit".equalsIgnoreCase(message)) {
            try {
                socket.close();
                frame.dispose();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private class MessageReceiver implements Runnable {
        @Override
        public void run() {
            try {
                String message;
                while ((message = reader.readLine()) != null) {
                    messageArea.append(message + "\n");
                    messageArea.setCaretPosition(messageArea.getDocument().getLength());
                }
            } catch (IOException e) {
                messageArea.append("与服务器的连接已断开\n");
            }
        }
    }

    public static void main(String[] args) {
        // 启动多个客户端
        SwingUtilities.invokeLater(() -> {
            new Client("客户端1");
            new Client("客户端2");
            new Client("客户端3");
            new Client("客户端4");
            new Client("客户端5");
        });
    }
}

运行结果:

1.先运行服务器端,等待客户端连接

2.再运行客户端

 

3.再查看服务器端显示如下

 

4.实现各个客户端之间的通信

1)群发

 

所有客户端都收到此消息

 

2)只给一个客户端发【@客户端3,这里是标识符,@某个用户名发送消失,只有这个客户端能够接受此消息】

 

服务器端也能够对各2客户端消息进行监听

 


网站公告

今日签到

点亮在社区的每一天
去签到