在 Java 中如何基于传统 IO 实现一个简单的 TCP 客户端与服务端通信?
在 Java 中,基于传统阻塞型 IO(BIO,Blocking IO)实现 TCP 通信,主要依赖 java.net 包中的两个核心类:
ServerSocket:用于服务端,监听指定端口并接收客户端连接。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. 如何运行
- 先启动服务端:运行
TCPServer。控制台会显示【服务器】启动,正在监听端口...,此时程序会阻塞在accept()处。 - 再启动客户端:运行
TCPClient。控制台会连接成功并提示输入。 - 开始通信:在客户端控制台输入任意字符(例如
Hello),回车。你会看到:- 服务端收到并打印。
- 客户端收到服务端的 Echo 回复。
- 断开连接:在客户端输入
bye,两端都会优雅地关闭连接并退出。
4. 关键点解析与传统 IO 的局限性
- 阻塞式 (Blocking):
serverSocket.accept()是阻塞的,没有客户端连接时,线程会一直卡在这里。reader.readLine()也是阻塞的,如果客户端不发送数据(或者没有发送换行符\n),服务端线程就会一直等待。
- 一麦单传(无法处理并发):
- 上述服务端代码一次只能处理一个客户端连接。如果第一个客户端不退出(不输入
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)出现之前,解决并发网络通信的标准做法。