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

在网络编程领域,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客户端消息进行监听

 

java聊天程序源码 2 需求分析 2.1 业务需求 1. 与聊天室成员一起聊天。 2. 可以与聊天室成员私聊。 3. 可以改变聊天内容风格。 4. 用户注册(含头像)、登录。 5. 服务器监控聊天内容。 6. 服务器过滤非法内容。 7. 服务器发送通知。 8. 服务器踢人。 9. 保存服务器日志。 10.保存用户聊天信息。 2.2 系统功能模块 2.2.1 服务器端 1.处理用户注册 2.处理用户登录 3.处理用户发送信息 4.处理用户得到信息 5.处理用户退出 2.2.2 客户端 1.用户注册界面及结果 2.用户登录界面及结果 3.用户发送信息界面及结果 4.用户得到信息界面及结果 5.用户退出界面及结果 2.3 性能需求 运行环境:Windows 9x、2000、xp、2003,Linux 必要环境:JDK 1.5 以上 硬件环境:CPU 400MHz以上,内存64MB以上 3.1.2 客户端结构 ChatClient.java 为客户端程序启动类,负责客户端的启动和退出。 Login.java 为客户端程序登录界面,负责用户帐号信息的验证与反馈。 Register.java 为客户端程序注册界面,负责用户帐号信息的注册验证与反馈。 ChatRoom.java 为客户端程序聊天室主界面,负责接收、发送聊天内容与服务器端的Connection.java 亲密合作。 Windowclose 为ChatRoom.java的内部类,负责监听聊天界面的操作,当用户退出时返回给服务器信息。 Clock.java 为客户端程序的一个小程序,实现的一个石英钟功能。 3. 2 系统实现原理 当用户聊天时,将当前用户名、聊天对象、聊天内容、聊天语气和是否私聊进行封装,然后与服务器建立Socket连接,再用对象输出流包装Socket的输出流将聊天信息对象发送给服务器端 当用户发送聊天信息时,服务端将会收到客户端用Socket传输过来的聊天信息对象,然后将其强制转换为Chat对象,并将本次用户的聊天信息对象添加到聊天对象集Message中,以供所有聊天用户访问。 接收用户的聊天信息是由多线程技术实现的,因为客户端必须时时关注更新服务器上是否有最新消息,在本程序中设定的是3秒刷新服务器一次,如果间隔时间太短将会增加客户端与服务器端的通信负担,而间隔时间长就会让人感觉没有时效性,所以经过权衡后认为3秒最佳,因为每个用户都不可能在3秒内连续发送信息。 当每次用户接收到聊天信息后将会开始分析聊天信息然后将适合自己的信息人性化地显示在聊天信息界面上。 4.1.1 问题陈述 1.接受用户注册信息并保存在一个基于文件的对象型数据库。 2.能够允许注册过的用户登陆聊天界面并可以聊天。 3.能够接受私聊信息并发送给特定的用户。 4.服务器运行在自定义的端口上#1001。 5.服务器监控用户列表和用户聊天信息(私聊除外)。 6.服务器踢人,发送通知。 7.服务器保存日志。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值