杰瑞科技汇

java socket聊天程序

  1. 服务器端:监听客户端连接,并将一个客户端发送的消息广播给所有已连接的客户端。
  2. 客户端:连接到服务器,通过控制台输入消息并发送给服务器,同时也能在控制台看到其他客户端发送的消息。

核心概念

在开始编码前,我们先理解几个核心概念:

java socket聊天程序-图1
(图片来源网络,侵删)
  • Socket (套接字):网络通信的端点,可以把它想象成一个电话,你通过它来拨号(连接)和通话(发送/接收数据)。
  • ServerSocket:服务器端的“总机”,它负责在指定的端口上监听客户端的连接请求,当有客户端请求连接时,它会“接起电话”,并为这个客户端创建一个新的 Socket 来进行一对一的通信。
  • IP 地址:网络中设备的唯一标识,0.0.1(本机地址)。
  • 端口号:设备上应用程序的标识,确保数据被发送到正确的程序,0 到 1023 是系统保留端口,我们应该使用 1024 以上的端口。
  • 输入/输出流Socket 对象包含 InputStreamOutputStream,用于数据的读写,在 Java 中,我们通常会包装成更高级的 BufferedReaderPrintWriter 来方便地处理文本数据。

服务器端代码

服务器端的主要任务是:

  1. 在一个固定端口启动 ServerSocket
  2. 进入一个无限循环,不断等待新的客户端连接。
  3. 当有新客户端连接时,为其创建一个新的线程,专门负责处理与该客户端的通信(接收消息并广播)。
  4. 维护一个所有客户端线程的列表,以便广播消息。

文件名:ChatServer.java

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class ChatServer {
    // 使用线程安全的 List 来存储所有客户端的处理线程
    private static final List<ClientHandler> clients = new CopyOnWriteArrayList<>();
    public static void main(String[] args) {
        // 服务器监听的端口号
        int port = 12345;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("聊天服务器已启动,等待客户端连接...");
            // 主线程负责不断接受新的客户端连接
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
                // 为每个客户端创建一个新的处理线程
                ClientHandler clientHandler = new ClientHandler(clientSocket);
                clients.add(clientHandler);
                // 启动客户端处理线程
                new Thread(clientHandler).start();
            }
        } catch (IOException e) {
            System.err.println("服务器启动或运行时出错: " + e.getMessage());
            e.printStackTrace();
        }
    }
    // 广播消息给所有客户端
    public static void broadcast(String message, ClientHandler excludeClient) {
        for (ClientHandler client : clients) {
            if (client != excludeClient) {
                client.sendMessage(message);
            }
        }
    }
    // 内部类:用于处理单个客户端的通信
    static class ClientHandler implements Runnable {
        private Socket socket;
        private PrintWriter out;
        private BufferedReader in;
        private String username;
        public ClientHandler(Socket socket) {
            this.socket = socket;
        }
        @Override
        public void run() {
            try {
                // 获取输入输出流
                out = new PrintWriter(socket.getOutputStream(), true);
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // 提示用户输入用户名
                out.println("请输入您的用户名:");
                username = in.readLine();
                System.out.println(username + " 已加入聊天室。");
                // 广播新用户加入的消息
                broadcast("[系统] " + username + " 加入了聊天室。", this);
                // 读取客户端发送的消息
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    if ("exit".equalsIgnoreCase(inputLine)) {
                        break; // 客户端退出
                    }
                    // 广播消息给其他客户端
                    broadcast(username + ": " + inputLine, this);
                }
            } catch (IOException e) {
                // 如果客户端异常断开,这里会捕获到异常
                System.out.println("与客户端 " + username + " 的连接中断。");
            } finally {
                // 客户端断开连接后的清理工作
                try {
                    if (socket != null && !socket.isClosed()) {
                        socket.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                // 从客户端列表中移除
                clients.remove(this);
                // 广播用户离开的消息
                if (username != null) {
                    System.out.println(username + " 已离开聊天室。");
                    broadcast("[系统] " + username + " 离开了聊天室。", this);
                }
            }
        }
        // 向此客户端发送消息
        public void sendMessage(String message) {
            out.println(message);
        }
    }
}

客户端代码

客户端的主要任务是:

  1. 创建一个 Socket 对象,连接到服务器的 IP 地址和端口号。
  2. 创建一个线程来持续监听服务器发来的消息,并打印到控制台。
  3. 主线程负责从控制台读取用户输入,并将输入的内容发送给服务器。

文件名:ChatClient.java

java socket聊天程序-图2
(图片来源网络,侵删)
import java.io.*;
import java.net.*;
public class ChatClient {
    private static final String SERVER_ADDRESS = "127.0.0.1"; // 服务器地址
    private static final int SERVER_PORT = 12345;         // 服务器端口
    public static void main(String[] args) {
        try (
            // 1. 创建 Socket 连接到服务器
            Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
            // 2. 获取输入输出流
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 3. 获取控制台输入流
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            System.out.println("已连接到服务器。");
            // 4. 创建一个新线程来接收服务器消息
            Thread receiverThread = new Thread(() -> {
                try {
                    String serverMessage;
                    while ((serverMessage = in.readLine()) != null) {
                        System.out.println(serverMessage);
                    }
                } catch (IOException e) {
                    System.out.println("与服务器的连接已断开。");
                }
            });
            receiverThread.start();
            // 5. 主线程负责从控制台读取用户输入并发送给服务器
            String userInput;
            while (true) {
                userInput = stdIn.readLine();
                if ("exit".equalsIgnoreCase(userInput)) {
                    break; // 输入 exit 退出
                }
                out.println(userInput);
            }
        } catch (UnknownHostException e) {
            System.err.println("无法找到服务器: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("I/O Error: " + e.getMessage());
        }
        System.out.println("客户端已关闭。");
    }
}

如何运行和测试

  1. 保存文件:将上面的两段代码分别保存为 ChatServer.javaChatClient.java,并放在同一个目录下。

  2. 编译代码:打开终端或命令提示符,进入文件所在目录,运行以下命令编译 Java 文件:

    javac ChatServer.java ChatClient.java
  3. 启动服务器:在同一个终端中,先运行服务器程序:

    java ChatServer

    你会看到输出:

    聊天服务器已启动,等待客户端连接...
  4. 启动客户端再打开一个新的终端(不要关闭服务器的终端),在新的终端中运行客户端程序:

    java ChatClient

    你会看到:

    已连接到服务器。
    请输入您的用户名:

    输入一个用户名,Alice,然后按回车。

  5. 开始聊天

    • 在第一个客户端(Alice)的终端,输入任何消息,"大家好!",然后按回车。
    • 在第二个客户端(Bob)的终端,你也会看到 "Alice: 大家好!"。
    • 再打开第三个终端,启动另一个 java ChatClient,输入用户名 Bob
    • 当 Alice 或 Bob 说话时,另一方和服务器控制台都能看到消息。
  6. 退出

    • 在任意一个客户端的终端输入 exit 并按回车,该客户端会退出。
    • 其他客户端和服务器会收到该用户离开的系统消息。
    • 服务器会一直运行,等待新的客户端连接。

代码讲解与改进点

服务器端 (ChatServer.java)

  • CopyOnWriteArrayList<ClientHandler>:这是一个线程安全的 List,在多线程环境下(服务器主线程添加,客户端线程删除),使用它比 ArrayList 更安全,可以避免 ConcurrentModificationException
  • ServerSocket serverSocket = new ServerSocket(port):在指定端口创建服务器套接字。
  • serverSocket.accept():这是一个阻塞方法,程序会在这里暂停,直到有新的客户端连接,一旦有连接,它会返回一个代表该客户端连接的 Socket 对象。
  • new Thread(clientHandler).start():为每个客户端创建一个独立的线程,这是处理多个客户端连接的关键,这样服务器就不会因为处理一个客户端而阻塞,从而无法响应其他客户端。
  • broadcast() 方法:遍历所有客户端列表,向除了发送者之外的所有客户端发送消息。

客户端 (ChatClient.java)

  • new Socket(SERVER_ADDRESS, SERVER_PORT):创建一个 Socket 并尝试连接到指定的服务器地址和端口,这是一个阻塞方法,连接成功后才会继续执行。
  • 为什么需要接收线程?:网络通信是异步的,我们不能在主线程中一边等待用户输入(stdIn.readLine()),一边又等待服务器消息(in.readLine()),因为 readLine() 都是阻塞的,我们创建一个专门的线程来监听服务器消息,而主线程则专注于处理用户输入。
  • out.println():将用户输入的一行文本发送到服务器。
  • in.readLine():读取服务器传来的一行文本。

潜在的改进方向

  1. 图形用户界面:目前是基于命令行的,可以扩展为使用 Java Swing 或 JavaFX 来创建有聊天窗口、输入框和按钮的 GUI 应用。
  2. 私聊功能:可以在消息前加上特定格式(如 @用户名 消息内容),服务器解析后只将消息发送给指定的用户。
  3. 文件传输:可以通过定义新的消息协议(以 FILE: 开头)来实现文件传输功能。
  4. 更健壮的错误处理:增加对网络中断、异常输入等情况的更细致处理。
  5. 使用 NIO (New I/O):对于更高性能、更高并发的场景,可以使用 Java NIO 中的 SelectorChannel 来实现非阻塞 I/O,这能更高效地管理大量连接。
分享:
扫描分享到社交APP
上一篇
下一篇