基于本文回答
0
评论

在 Java 中如何基于传统 IO 实现一个简单的 TCP 客户端与服务端通信?

在 Java 中,基于传统阻塞型 IO(BIO,Blocking IO)实现 TCP 通信,主要依赖 java.net 包中的两个核心类:

  1. ServerSocket:用于服务端,监听指定端口并接收客户端连接。
  2. Socket:用于客户端(以及服务端接受连接后),负责具体的读写操作。

下面是一个最简单的、单对单通信的完整实现步骤和代码。


1. 服务端实现 (TCPServer.java)

服务端的工作流程是:创建 ServerSocket -> 等待客户端连接 (accept) -> 获取输入/输出流 -> 进行读写 -> 关闭资源。

java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) {
        int port = 8888; // 监听端口

        // 使用 try-with-resources 自动关闭资源
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("【服务器】启动,正在监听端口: " + port + "...");

            // accept() 是阻塞方法,直到有客户端连接才会继续执行
            try (Socket socket = serverSocket.accept()) {
                System.out.println("【服务器】客户端已连接,IP: " + socket.getInetAddress().getHostAddress());

                // 获取输入流,用于读取客户端发送的数据
                BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // 获取输出流,用于向客户端发送数据(设置 autoFlush 为 true)
                PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);

                String clientMessage;
                // 循环读取客户端消息,直到客户端关闭连接
                while ((clientMessage = reader.readLine()) != null) {
                    System.out.println("【服务器】收到客户端消息: " + clientMessage);
                    
                    // 向客户端回执消息
                    writer.println("服务器已收到: '" + clientMessage + "'");
                    
                    // 如果收到 "bye",则退出循环,准备断开连接
                    if ("bye".equalsIgnoreCase(clientMessage.trim())) {
                        System.out.println("【服务器】客户端请求断开连接。");
                        break;
                    }
                }
            } // 自动关闭 socket
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("【服务器】已关闭。");
    }
}

2. 客户端实现 (TCPClient.java)

客户端的工作流程是:创建 Socket 建立连接 -> 获取输入/输出流 -> 发送数据并接收服务端回执 -> 关闭资源。

java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TCPClient {
    public static void main(String[] args) {
        String host = "127.0.0.1"; // 服务器IP
        int port = 8888;           // 服务器端口

        // 使用 try-with-resources 自动关闭资源
        try (Socket socket = new Socket(host, port)) {
            System.out.println("【客户端】成功连接到服务器 " + host + ":" + port);

            // 获取输入流(读取服务器响应)
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 获取输出流(向服务器发送数据)
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            
            // 获取控制台输入
            Scanner scanner = new Scanner(System.in);
            String userInput;

            System.out.println("请输入要发送给服务器的消息(输入 'bye' 退出):");
            while (true) {
                System.out.print("> ");
                userInput = scanner.nextLine();
                
                // 发送数据到服务器
                writer.println(userInput);

                // 读取服务器的响应
                String response = reader.readLine();
                System.out.println("【客户端】收到服务器回复: " + response);

                if ("bye".equalsIgnoreCase(userInput.trim())) {
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("【客户端】连接或通信异常: " + e.getMessage());
        }
        System.out.println("【客户端】连接已关闭。");
    }
}

3. 如何运行

  1. 先启动服务端:运行 TCPServer。控制台会显示 【服务器】启动,正在监听端口...,此时程序会阻塞在 accept() 处。
  2. 再启动客户端:运行 TCPClient。控制台会连接成功并提示输入。
  3. 开始通信:在客户端控制台输入任意字符(例如 Hello),回车。你会看到:
    • 服务端收到并打印。
    • 客户端收到服务端的 Echo 回复。
  4. 断开连接:在客户端输入 bye,两端都会优雅地关闭连接并退出。

4. 关键点解析与传统 IO 的局限性

  1. 阻塞式 (Blocking)
    • serverSocket.accept() 是阻塞的,没有客户端连接时,线程会一直卡在这里。
    • reader.readLine() 也是阻塞的,如果客户端不发送数据(或者没有发送换行符 \n),服务端线程就会一直等待。
  2. 一麦单传(无法处理并发)
    • 上述服务端代码一次只能处理一个客户端连接。如果第一个客户端不退出(不输入 bye),第二个客户端即使连上来也无法得到响应(它们会被放入操作系统的 TCP 连接队列中,等待第一个处理完)。

5. 进阶:如何处理多个客户端连接?

在传统 IO 中,要同时处理多个客户端,通常使用 “一连接一线程”(Thread-per-Connection) 模式。可以使用线程池来优化。

多线程服务端改造示例:

java
import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class TCPMultiThreadServer {
    public static void main(String[] args) {
        int port = 8888;
        // 创建线程池处理客户端任务
        ExecutorService executorService = Executors.newCachedThreadPool();

        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("【多线程服务器】启动,监听端口: " + port);

            while (true) {
                // 阻塞等待新客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("【多线程服务器】新客户端已连接: " + socket.getRemoteSocketAddress());
                
                // 提交给线程池处理,主线程立即回去继续 accept()
                executorService.execute(new ClientHandler(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 内部类:处理单个客户端连接的任务
    private static class ClientHandler implements Runnable {
        private final Socket socket;

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

        @Override
        public void run() {
            try (Socket s = this.socket;
                 BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
                 PrintWriter writer = new PrintWriter(s.getOutputStream(), true)) {

                String msg;
                while ((msg = reader.readLine()) != null) {
                    System.out.println("[" + Thread.currentThread().getName() + "] 收到: " + msg);
                    writer.println("Echo: " + msg);
                    if ("bye".equalsIgnoreCase(msg.trim())) break;
                }
            } catch (IOException e) {
                System.err.println("处理客户端异常: " + e.getMessage());
            }
            System.out.println("【连接断开】");
        }
    }
}

通过这种多线程方式,服务端就可以同时和多个客户端进行通信了。这也是 Java 在 NIO(Non-blocking IO)出现之前,解决并发网络通信的标准做法。

右滑查看面试常问