杰瑞科技汇

Java Socket多人聊天如何实现?

我们将分步实现,并提供完整的代码。

Java Socket多人聊天如何实现?-图1
(图片来源网络,侵删)

核心设计思路

一个典型的 C/S(客户端/服务器)架构的多人聊天室包含两个主要部分:

  1. 服务器端:

    • 监听一个固定端口,等待客户端的连接请求。
    • 当一个客户端连接成功后,服务器会为该客户端创建一个独立的 Socket 和一个专门的线程(ClientHandler)来处理与该客户端的所有通信。
    • 服务器维护一个所有客户端连接的列表(List<ClientHandler>)。
    • 当服务器从某个客户端收到消息时,它会将这条消息广播给列表中的所有其他客户端。
  2. 客户端:

    • 知道服务器的 IP 地址和端口号。
    • 连接到服务器。
    • 启动两个线程:
      • 输入线程: 持续监听用户在控制台的输入,一旦用户输入消息并按下回车,就将消息发送给服务器。
      • 输出线程: 持续监听从服务器接收到的消息,一旦收到,就打印在控制台上,让所有用户看到。

第一步:服务器端实现

服务器端的核心是 ServerSocket 和一个处理单个客户端的线程类。

Java Socket多人聊天如何实现?-图2
(图片来源网络,侵删)

ClientHandler.java - 处理单个客户端的线程

这个类是每个客户端连接到服务器时,服务器为其创建的一个“管家”,它负责接收该客户端的消息,并转发给其他所有人。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
// 这是一个处理单个客户端连接的线程
public class ClientHandler implements Runnable {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;
    // 使用静态列表来保存所有客户端处理器,这样它们之间可以共享
    private static final List<ClientHandler> clients = new ArrayList<>();
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
        try {
            // 获取输出流,用于向客户端发送消息
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            // 获取输入流,用于接收客户端的消息
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // 将新加入的客户端处理器添加到列表中
            clients.add(this);
            System.out.println("新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        try {
            String inputLine;
            // 循环读取客户端发送的消息
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到来自 " + clientSocket.getInetAddress().getHostAddress() + " 的消息: " + inputLine);
                // 将消息广播给所有其他客户端
                broadcast(inputLine);
            }
        } catch (IOException e) {
            // 如果客户端断开连接,会抛出异常
            System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 断开连接。");
        } finally {
            // 客户端断开连接后,清理资源并从列表中移除
            removeClientHandler();
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    // 广播消息给所有客户端
    private void broadcast(String message) {
        // 遍历所有客户端处理器
        for (ClientHandler client : clients) {
            // 向每个客户端发送消息,但不发送给自己(可选,也可以发送给自己)
            // if (client != this) {
                client.out.println(message);
            // }
        }
    }
    // 从客户端列表中移除自己
    private void removeClientHandler() {
        clients.remove(this);
    }
}

ChatServer.java - 主服务器类

这是服务器的入口,负责启动服务器并接受新的客户端连接。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ChatServer {
    private static final int PORT = 12345; // 服务器监听端口
    public static void main(String[] args) {
        // 使用线程池来管理客户端连接线程,避免为每个连接创建新线程的开销
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("聊天服务器已启动,监听端口: " + PORT);
            // 无限循环,持续接受客户端连接
            while (true) {
                Socket clientSocket = serverSocket.accept(); // 阻塞,等待客户端连接
                System.out.println("已接受来自 " + clientSocket.getInetAddress().getHostAddress() + " 的连接请求。");
                // 为每个新连接创建一个ClientHandler任务,并提交给线程池执行
                threadPool.execute(new ClientHandler(clientSocket));
            }
        } catch (IOException e) {
            System.err.println("服务器启动失败或发生IO异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭线程池
            threadPool.shutdown();
        }
    }
}

第二步:客户端实现

客户端相对简单,需要同时处理用户输入和服务器消息的接收。

ChatClient.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class ChatClient {
    private static final String SERVER_HOST = "127.0.0.1"; // 服务器IP地址,本地回环
    private static final int SERVER_PORT = 12345;       // 服务器端口
    public static void main(String[] args) {
        try (
            // 1. 创建Socket连接到服务器
            Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
            // 2. 获取输出流,用于向服务器发送消息
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 3. 获取输入流,用于接收服务器的消息
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
        ) {
            System.out.println("成功连接到聊天服务器!");
            // 4. 创建一个线程来专门监听服务器发送过来的消息
            Thread receiveThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    String serverMessage;
                    try {
                        // 循环读取服务器发送的消息
                        while ((serverMessage = in.readLine()) != null) {
                            System.out.println(serverMessage);
                        }
                    } catch (IOException e) {
                        // 如果服务器关闭或断开连接,会收到null
                        System.out.println("与服务器断开连接。");
                    }
                }
            });
            receiveThread.start(); // 启动接收线程
            // 5. 使用主线程来读取用户在控制台的输入
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入你的昵称: ");
            String nickname = scanner.nextLine();
            out.println(nickname + " 加入了聊天室。"); // 发送加入消息
            String userInput;
            while (true) {
                System.out.print("> "); // 提示符
                userInput = scanner.nextLine();
                if ("exit".equalsIgnoreCase(userInput)) {
                    out.println(nickname + " 离开了聊天室。"); // 发送离开消息
                    break; // 用户输入exit,则退出循环
                }
                // 将用户输入的消息发送给服务器
                out.println(nickname + ": " + userInput);
            }
        } catch (UnknownHostException e) {
            System.err.println("找不到服务器: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("无法连接到服务器: " + e.getMessage());
        }
    }
}

如何运行

  1. 编译代码: 将所有 .java 文件放在同一个目录下,然后使用 javac 编译:

    Java Socket多人聊天如何实现?-图3
    (图片来源网络,侵删)
    javac *.java
  2. 启动服务器: 在一个终端窗口中运行服务器:

    java ChatServer

    你会看到输出:聊天服务器已启动,监听端口: 12345

  3. 启动客户端: 打开两个或更多新的终端窗口,分别运行客户端:

    java ChatClient

    在每个客户端中输入你的昵称,然后就可以开始聊天了。

运行效果示例

终端1 (服务器):

聊天服务器已启动,监听端口: 12345
已接受来自 127.0.0.1 的连接请求。
新客户端连接: 127.0.0.1
已接受来自 127.0.0.1 的连接请求。
新客户端连接: 127.0.0.1
收到来自 127.0.0.1 的消息: Alice: 大家好!
收到来自 127.0.0.1 的消息: Bob: 你好,Alice!

终端2 (客户端1, Alice):

成功连接到聊天服务器!
请输入你的昵称: Alice
> 大家好!
Bob: 你好,Alice!

终端3 (客户端2, Bob):

成功连接到聊天服务器!
请输入你的昵称: Bob
Alice 加入了聊天室。
Alice: 大家好!
> 你好,Alice!

进阶与优化

  1. GUI 图形界面: 目前的版本是基于控制台的,你可以使用 Java Swing 或 JavaFX 来创建图形界面,让聊天体验更友好。
  2. 私聊功能: 可以在消息中添加特殊格式,@Bob 你好,服务器解析后只将消息发送给名为 "Bob" 的客户端。
  3. 用户列表: 服务器可以维护一个在线用户列表,并在有用户加入或离开时广播给所有客户端。
  4. 处理异常和心跳包: 目前的代码在客户端突然断开时(如直接关闭窗口),服务器可能无法立即感知,可以引入心跳机制,客户端定期向服务器发送“我还活着”的信号,如果服务器长时间没收到,就认为客户端已断开并清理资源。
  5. 使用 NIO (New I/O): 对于高并发的场景,传统的 BIO (Blocking I/O) 模式(每个连接一个线程)效率不高,可以使用 Java NIO 的 Selector 机制,用一个线程或少量线程来管理成千上万的连接,性能更高。
分享:
扫描分享到社交APP
上一篇
下一篇