核心思想
在一个典型的客户端-服务器模型中,客户端通常需要同时处理两个任务:

- 发送数据:将用户输入(或其他业务逻辑产生的数据)发送给服务器。
- 接收数据:持续监听并接收服务器发来的消息(如聊天消息、数据更新等)。
如果这两个任务在同一个线程中执行,那么当客户端在等待接收数据时(socket.getInputStream().read() 是一个阻塞方法),它将无法处理用户输入,导致程序“卡死”,反之亦然。
多线程的解决方案
- 主线程:负责启动客户端,并启动一个专门用于发送数据的线程。
- 接收线程:负责接收数据。
这样,发送和接收任务被分离到两个独立的线程中,它们可以并发执行,互不阻塞,从而实现一个功能完善、响应迅速的客户端。
完整代码示例
下面是一个完整的、可运行的 Java Socket 多线程客户端示例,这个客户端可以连接到一个简单的回显服务器,并允许用户同时发送和接收消息。

客户端代码 (MultiThreadClient.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;
public class MultiThreadClient {
private Socket socket;
private BufferedReader in; // 用于接收服务器消息
private PrintWriter out; // 用于向服务器发送消息
private BufferedReader consoleReader; // 用于读取用户控制台输入
public MultiThreadClient(String host, int port) {
try {
// 1. 创建一个 Socket 并连接到指定的服务器地址和端口
socket = new Socket(host, port);
System.out.println("成功连接到服务器 " + host + ":" + port);
// 2. 获取输入流和输出流
// in 用于读取服务器发送来的数据
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// out 用于向服务器发送数据,autoFlush=true 表示 println 后自动刷新缓冲区
out = new PrintWriter(socket.getOutputStream(), true);
// consoleReader 用于读取用户在控制台的输入
consoleReader = new BufferedReader(new InputStreamReader(System.in));
// 3. 启动接收线程
// 这个线程会一直运行,监听并打印来自服务器的消息
new Thread(new Receiver()).start();
// 4. 主线程负责发送用户输入
System.out.println("请输入消息 (输入 'exit' 退出):");
String userInput;
while (true) {
// 读取用户从控制台输入的一行
userInput = consoleReader.readLine();
if ("exit".equalsIgnoreCase(userInput)) {
break; // 如果用户输入 exit,则退出循环
}
// 将用户输入发送给服务器
out.println(userInput);
}
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + host);
} catch (IOException e) {
System.err.println("无法连接到服务器或发生 I/O 错误。");
e.printStackTrace();
} finally {
// 5. 关闭资源
closeResources();
}
}
// 接收服务器消息的线程任务
private class Receiver implements Runnable {
@Override
public void run() {
try {
String serverMessage;
// 循环读取服务器发送来的消息,直到连接关闭或发生异常
while ((serverMessage = in.readLine()) != null) {
System.out.println("服务器回复: " + serverMessage);
}
} catch (IOException e) {
// 如果服务器关闭了连接,readLine() 会返回 null,循环结束,这里捕获异常并打印
System.out.println("与服务器连接已断开。");
} finally {
System.out.println("接收线程已停止。");
}
}
}
// 关闭所有打开的资源
private void closeResources() {
try {
if (consoleReader != null) consoleReader.close();
if (out != null) out.close();
if (in != null) in.close();
if (socket != null && !socket.isClosed()) socket.close();
System.out.println("所有资源已关闭,客户端退出。");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 服务器地址和端口
String serverHost = "localhost"; // 如果服务器在同一台机器上,使用 localhost
int serverPort = 8080; // 确保服务器正在这个端口上监听
// 启动客户端
new MultiThreadClient(serverHost, serverPort);
}
}
一个简单的服务器代码 (SimpleServer.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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleServer {
public static void main(String[] args) {
int port = 8080;
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());
// 为每个客户端连接创建一个任务,并提交到线程池
threadPool.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
} finally {
threadPool.shutdown(); // 关闭线程池
}
}
}
// 处理单个客户端连接的任务
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
) {
String inputLine;
// 读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到来自客户端 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
// 将收到的消息回显给客户端
out.println("服务器: " + inputLine);
}
} catch (IOException e) {
System.out.println("客户端 " + clientSocket.getInetAddress() + " 断开连接或发生错误。");
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码分步解析
客户端主逻辑 (MultiThreadClient.java)
-
Socket socket = new Socket(host, port);- 这是客户端的核心,它尝试在指定的
host和port上建立一个到服务器的 TCP 连接,这是一个阻塞操作,如果服务器未启动或地址/端口错误,会抛出IOException。
- 这是客户端的核心,它尝试在指定的
-
BufferedReader in和PrintWriter outin:包装了socket.getInputStream(),用于读取从服务器流过来的数据。BufferedReader提供了readLine()方法,可以方便地按行读取文本。out:包装了socket.getOutputStream(),用于向服务器写入数据。PrintWriter提供了println()方法,并且我们设置了autoFlush=true,这样每次调用println后都会自动刷新缓冲区,确保数据被立即发送。
-
BufferedReader consoleReader
(图片来源网络,侵删)- 这个
BufferedReader包装的是System.in,用于读取用户在控制台(命令行)的输入。
- 这个
-
new Thread(new Receiver()).start();- 这是多线程的关键,我们创建了一个新的
Thread,并将一个实现了Runnable接口的Receiver对象作为其任务,调用start()方法后,JVM 会启动一个新的执行流,专门用于执行Receiver的run()方法,主线程则继续向下执行。
- 这是多线程的关键,我们创建了一个新的
-
发送循环
- 主线程进入一个
while循环,通过consoleReader.readLine()等待用户输入。 - 当用户输入一行文本并按回车后,
readLine()返回该文本。 out.println(userInput);将文本发送给服务器。- 如果用户输入
exit,循环结束,finally块中的资源关闭代码被执行。
- 主线程进入一个
接收线程 (Receiver 内部类)
implements RunnableReceiver是一个内部类,实现了Runnable接口,这意味着它包含
