
1. 问题分析:原始TCP通信的局限性
在构建基于TCP/IP协议的客户端-服务器应用程序时,常见的需求是实现持续的数据交换,直到客户端或服务器决定终止连接。然而,初学者在实现这一功能时常遇到困惑,尤其是在使用BufferedReader进行行读取时。
原始的服务器代码结构如下:
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket welcomeSocket = new ServerSocket(6789);
while (true){ // 外层循环
Socket connectionSocket = welcomeSocket.accept(); // 接受新连接
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
clientSentence = inFromClient.readLine(); // 读取一行
capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
}
}
}这段代码的问题在于,while(true)循环的每次迭代都执行welcomeSocket.accept(),这意味着服务器在处理完一个客户端的一行输入后,会立即返回到循环的开始,并尝试接受新的客户端连接。对于同一个客户端的后续输入,服务器将无法接收,因为当前连接的Socket实例已经被“抛弃”,服务器在等待新的连接。这导致了服务器只能读取每个客户端发送的第一行数据。
客户端代码也存在类似的问题,它在一个无限循环中发送数据并尝试读取响应,但没有明确的终止机制。
立即学习“Java免费学习笔记(深入)”;
2. 服务器端解决方案:内层循环与终止指令
为了实现单个客户端连接内的持续数据交换,服务器需要引入一个内层循环来处理来自当前连接的连续输入。同时,需要定义一个特定的指令(例如"stop")来允许客户端通知服务器终止当前连接。
2.1 持续处理单个客户端连接
修改后的服务器代码应包含一个内层while(true)循环,专门用于处理当前connectionSocket的数据。
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket welcomeSocket = new ServerSocket(6789);
System.out.println("Server started, waiting for clients...");
while (true) { // 外层循环:接受新客户端连接
Socket connectionSocket = welcomeSocket.accept();
System.out.println("Client connected: " + connectionSocket.getInetAddress());
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
// 内层循环:处理当前客户端的持续输入
while (true) {
String clientSentence;
try {
clientSentence = inFromClient.readLine();
if (clientSentence == null) { // 客户端关闭连接
System.out.println("Client disconnected unexpectedly.");
break; // 跳出内层循环,等待新客户端
}
} catch (IOException e) {
System.out.println("Error reading from client: " + e.getMessage());
break; // 读取错误,跳出内层循环
}
if (clientSentence.equalsIgnoreCase("stop")) {
System.out.println("Client requested to stop. Closing connection.");
break; // 收到终止指令,跳出内层循环
}
String capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
System.out.println("Received: '" + clientSentence + "', Sent: '" + capitalizedSentence.trim() + "'");
}
connectionSocket.close(); // 关闭当前客户端连接
System.out.println("Connection to client closed.");
}
// welcomeSocket.close(); // 如果服务器需要完全终止,则在此处关闭
}
}代码说明:
- 外层while(true)循环:负责不断接受新的客户端连接。
- 内层while(true)循环:一旦接受到一个客户端连接,就进入此循环,持续读取来自该客户端的数据。
- inFromClient.readLine():尝试读取客户端发送的一行数据。
- clientSentence == null判断:当客户端关闭其输出流或整个Socket连接时,readLine()会返回null。这是一个重要的EOF指示,服务器应据此判断客户端已断开。
- equalsIgnoreCase("stop"):服务器检查收到的消息是否是预定义的终止指令(不区分大小写)。
- break语句:当收到"stop"指令或检测到客户端断开(readLine()返回null)时,跳出内层循环,从而结束当前客户端连接的处理。
- connectionSocket.close():在内层循环结束后,务必关闭当前客户端的Socket连接,释放资源。
2.2 服务器的完全终止策略
如果服务器不仅要终止与当前客户端的连接,而且在处理完一个客户端后希望完全停止运行,那么可以移除外层while(true)循环,并在处理完一个客户端后关闭ServerSocket。
import java.io.*;
import java.net.*;
public class TCPServerSingleClient { // 示例:仅处理一个客户端后终止的服务器
public static void main(String[] args) throws IOException {
ServerSocket welcomeSocket = new ServerSocket(6789);
System.out.println("Server started, waiting for ONE client...");
Socket connectionSocket = welcomeSocket.accept(); // 接受一个客户端连接
System.out.println("Client connected: " + connectionSocket.getInetAddress());
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
while (true) { // 处理当前客户端的持续输入
String clientSentence;
try {
clientSentence = inFromClient.readLine();
if (clientSentence == null) {
System.out.println("Client disconnected unexpectedly.");
break;
}
} catch (IOException e) {
System.out.println("Error reading from client: " + e.getMessage());
break;
}
if (clientSentence.equalsIgnoreCase("stop")) {
System.out.println("Client requested to stop. Closing connection.");
break;
}
String capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
System.out.println("Received: '" + clientSentence + "', Sent: '" + capitalizedSentence.trim() + "'");
}
connectionSocket.close(); // 关闭客户端连接
welcomeSocket.close(); // 关闭服务器Socket,服务器完全终止
System.out.println("Server shut down.");
}
}3. 客户端解决方案:发送终止指令与EOF判断
客户端需要修改以发送"stop"指令,并在收到服务器关闭连接的信号时优雅地终止其自身循环。
import java.io.*;
import java.net.*;
public class Klient {
public static void main(String[] args) throws UnknownHostException, IOException {
String sentence;
String modifiedSentence;
BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
Socket clientSocket = null; // 初始化为null,方便finally块关闭
try {
clientSocket = new Socket("127.0.0.1", 6789);
System.out.println("Connected to server.");
DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream());
BufferedReader inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
System.out.println("Enter text (type 'stop' to terminate):");
while (true) {
sentence = inFromUser.readLine(); // 从用户读取输入
// 发送数据到服务器
outToServer.writeBytes(sentence + '\n');
outToServer.flush(); // 确保数据立即发送
if (sentence.equalsIgnoreCase("stop")) {
System.out.println("Sent 'stop' command. Terminating client.");
break; // 发送"stop"后,客户端也应终止
}
// 读取服务器响应
modifiedSentence = inFromServer.readLine();
if (modifiedSentence == null) { // 服务器关闭连接
System.out.println("Server closed the connection. Terminating client.");
break; // 服务器断开,客户端终止
}
System.out.println("FROM SERVER: " + modifiedSentence);
}
} catch (IOException e) {
System.err.println("Client error: " + e.getMessage());
} finally {
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close(); // 确保客户端Socket关闭
System.out.println("Client socket closed.");
}
}
}
}代码说明:
- inFromUser.readLine():从控制台读取用户输入。
- outToServer.writeBytes(sentence + '\n'):发送用户输入到服务器,并追加换行符以匹配readLine()的期望。
- outToServer.flush():强制将缓冲区中的数据发送出去,避免数据滞留。
- sentence.equalsIgnoreCase("stop"):客户端在发送"stop"指令后,自身也应该终止循环。
- inFromServer.readLine()返回null:这是客户端检测服务器关闭连接的关键。当服务器关闭其输出流或整个Socket时,客户端的readLine()会返回null,表示数据流已结束。
- try-catch-finally块:用于捕获可能发生的IOException并确保在任何情况下clientSocket都能被关闭,实现资源的释放。
4. 总结与注意事项
- 循环结构是关键:在TCP通信中,为了实现持续的数据交换,服务器和客户端都需要使用循环来反复读取和写入数据。服务器通常需要一个外层循环接受新连接,一个内层循环处理单个连接的持续数据。
-
明确的终止机制:
- 指令终止:定义一个特定的字符串(如"stop")作为终止指令,客户端发送后,服务器和客户端都据此终止各自的循环。
- EOF终止:BufferedReader.readLine()返回null是数据流结束(EOF)的指示。这通常发生在远程端关闭了连接或其输出流时。客户端和服务器都应检查此条件以优雅地终止。
- 资源管理:无论通信如何终止,都必须确保Socket和ServerSocket等网络资源被正确关闭,通常在finally块中执行。
- 异常处理:网络编程中I/O操作容易出现异常,应使用try-catch块进行适当的异常处理。
- 线程模型:上述服务器示例是单线程的,一次只能处理一个客户端。在实际应用中,为了并发处理多个客户端,服务器通常会为每个新连接启动一个独立的线程。
- 缓冲区刷新:在DataOutputStream写入数据后,有时需要调用flush()方法以确保数据立即发送,而不是滞留在缓冲区中。
通过遵循这些原则和代码模式,可以构建出健壮且能够优雅终止的Java TCP客户端-服务器应用程序。










