
在java中构建tcp客户端-服务器应用程序时,实现连续的数据交换直至特定条件满足(例如收到“stop”指令或连接关闭)是一个常见的需求。原始代码中存在一个常见误区,即服务器在处理完一个客户端请求后,外层循环会立即尝试接受新的客户端连接,而非持续与当前客户端通信。这导致服务器无法处理同一客户端的后续请求,而客户端则会因服务器未响应而挂起。
服务器端逻辑优化
原服务器代码的 while(true) 循环用于不断接受新的客户端连接。一旦 welcomeSocket.accept() 返回一个 Socket 对象,服务器会读取一行数据并发送响应,然后立即回到 accept() 状态,等待下一个连接。这意味着每个客户端只能发送一条消息。为了实现与单个客户端的连续通信,我们需要在接受连接之后,为该连接引入一个内部循环。
修正后的服务器代码示例:
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) throws IOException {
String clientSentence;
String capitalizedSentence;
ServerSocket welcomeSocket = new ServerSocket(6789);
System.out.println("服务器已启动,等待客户端连接...");
// 外层循环:持续接受新的客户端连接
while (true) {
Socket connectionSocket = welcomeSocket.accept();
System.out.println("新客户端已连接:" + connectionSocket.getInetAddress());
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
// 内层循环:与当前客户端进行持续通信
while (true) {
try {
clientSentence = inFromClient.readLine();
if (clientSentence == null) { // 客户端关闭连接
System.out.println("客户端断开连接:" + connectionSocket.getInetAddress());
break; // 跳出内层循环,等待新客户端
}
System.out.println("收到来自客户端[" + connectionSocket.getInetAddress() + "]的消息: " + clientSentence);
if (clientSentence.equalsIgnoreCase("stop")) {
System.out.println("收到'stop'指令,关闭当前客户端连接。");
outToClient.writeBytes("Server received 'stop'. Connection closing.\n"); // 可选:通知客户端
break; // 收到"stop"指令,跳出内层循环,关闭当前连接
}
capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
} catch (SocketException e) {
System.out.println("客户端连接异常断开:" + connectionSocket.getInetAddress() + " - " + e.getMessage());
break; // 捕获连接异常,跳出内层循环
} catch (IOException e) {
System.err.println("读取或写入客户端数据时发生错误:" + e.getMessage());
break; // 其他IO错误,跳出内层循环
}
}
// 关闭当前客户端连接的资源
connectionSocket.close();
System.out.println("客户端连接已关闭。");
}
// 注意:如果服务器需要完全终止,则 welcomeSocket 也需要关闭,但通常服务器会一直运行。
// welcomeSocket.close();
}
}服务器端逻辑说明:
- 外层循环 (while(true) for welcomeSocket.accept()): 负责接受新的客户端连接。
- 内层循环 (while(true) for inFromClient.readLine()): 负责与当前已连接的客户端进行持续的数据交换。
- “stop”指令处理: 在内层循环中,如果 clientSentence 等于(忽略大小写)"stop",服务器会发送一个可选的确认消息给客户端,然后 break 出内层循环,从而关闭当前的 connectionSocket。外层循环会继续执行,等待下一个客户端连接。
- 客户端断开检测: inFromClient.readLine() 返回 null 表示客户端已关闭其输出流(即连接已关闭)。此时服务器也应 break 内层循环并关闭 connectionSocket。
- 异常处理: 增加了 try-catch 块来处理 SocketException 和其他 IOException,确保在客户端异常断开时服务器能优雅地处理,避免程序崩溃。
客户端逻辑优化
客户端需要持续发送用户输入,并接收服务器的响应。关键在于如何检测服务器何时关闭了连接,以便客户端也能优雅地终止。当服务器关闭其输出流时,客户端的 inFromServer.readLine() 方法将返回 null。
立即学习“Java免费学习笔记(深入)”;
修正后的客户端代码示例:
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;
DataOutputStream outToServer = null;
BufferedReader inFromServer = null;
try {
clientSocket = new Socket("127.0.0.1", 6789);
outToServer = new DataOutputStream(clientSocket.getOutputStream());
inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
System.out.println("已连接到服务器。输入消息,输入'stop'结束会话。");
// 客户端持续发送和接收数据
while (true) {
System.out.print("请输入消息: ");
sentence = inFromUser.readLine();
if (sentence == null) { // 用户输入流关闭(如Ctrl+D),终止客户端
System.out.println("用户输入流已关闭,客户端即将退出。");
break;
}
outToServer.writeBytes(sentence + '\n'); // 发送数据到服务器
// 接收服务器响应
modifiedSentence = inFromServer.readLine();
if (modifiedSentence == null) { // 服务器关闭了连接
System.out.println("服务器已关闭连接。客户端即将退出。");
break;
}
System.out.println("来自服务器的响应: " + modifiedSentence);
if (sentence.equalsIgnoreCase("stop")) {
System.out.println("已发送'stop'指令,等待服务器响应并关闭连接。");
// 理论上,服务器收到'stop'后会关闭连接,导致下一次readLine返回null
// 这里的break是多余的,但为了清晰性可以保留,或者依赖modifiedSentence == null来退出
break;
}
}
} catch (ConnectException e) {
System.err.println("无法连接到服务器: " + e.getMessage());
} catch (SocketException e) {
System.err.println("网络连接异常: " + e.getMessage());
} catch (IOException e) {
System.err.println("客户端IO操作发生错误: " + e.getMessage());
} finally {
// 确保关闭所有资源
try {
if (inFromUser != null) inFromUser.close();
if (outToServer != null) outToServer.close();
if (inFromServer != null) inFromServer.close();
if (clientSocket != null) clientSocket.close();
System.out.println("客户端资源已释放。");
} catch (IOException e) {
System.err.println("关闭客户端资源时发生错误: " + e.getMessage());
}
}
}
}客户端逻辑说明:
- 持续通信循环: while(true) 循环负责从用户读取输入,发送到服务器,并接收服务器的响应。
- 检测服务器关闭: inFromServer.readLine() 返回 null 是服务器关闭连接的明确信号。客户端检测到此情况后,应 break 出循环并关闭自身资源。
- 发送“stop”指令: 客户端发送“stop”指令后,通常会等待服务器响应(可能是一个确认消息),然后服务器会关闭连接。客户端会通过 inFromServer.readLine() == null 检测到服务器的关闭,从而退出循环。
- 资源管理: 使用 try-catch-finally 结构确保无论发生何种异常或正常退出,所有的输入/输出流和 Socket 资源都能被正确关闭,防止资源泄露。
服务器完全终止的场景
如果服务器不仅要关闭当前客户端连接,而且在收到“stop”指令后要完全终止运行(即不再接受任何新的连接),则需要修改服务器的结构,移除外层 while(true) 循环,并在处理完一个客户端后关闭 ServerSocket。
服务器完全终止代码示例:
import java.io.*;
import java.net.*;
public class SingleClientTerminatingTCPServer {
public static void main(String[] args) throws IOException {
String clientSentence;
String capitalizedSentence;
ServerSocket welcomeSocket = null;
Socket connectionSocket = null;
try {
welcomeSocket = new ServerSocket(6789);
System.out.println("服务器已启动,等待单个客户端连接...");
// 接受一个客户端连接
connectionSocket = welcomeSocket.accept();
System.out.println("客户端已连接:" + connectionSocket.getInetAddress());
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
// 与当前客户端进行持续通信
while (true) {
try {
clientSentence = inFromClient.readLine();
if (clientSentence == null) {
System.out.println("客户端断开连接:" + connectionSocket.getInetAddress());
break;
}
System.out.println("收到来自客户端[" + connectionSocket.getInetAddress() + "]的消息: " + clientSentence);
if (clientSentence.equalsIgnoreCase("stop")) {
System.out.println("收到'stop'指令,服务器即将完全终止。");
outToClient.writeBytes("Server received 'stop'. Server terminating.\n");
break; // 收到"stop"指令,跳出内层循环
}
capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
} catch (SocketException e) {
System.out.println("客户端连接异常断开:" + connectionSocket.getInetAddress() + " - " + e.getMessage());
break;
} catch (IOException e) {
System.err.println("读取或写入客户端数据时发生错误:" + e.getMessage());
break;
}
}
} catch (IOException e) {
System.err.println("服务器启动或运行过程中发生错误: " + e.getMessage());
} finally {
// 确保关闭所有资源
try {
if (connectionSocket != null) connectionSocket.close();
if (welcomeSocket != null) welcomeSocket.close();
System.out.println("服务器资源已释放,服务器已关闭。");
} catch (IOException e) {
System.err.println("关闭服务器资源时发生错误: " + e.getMessage());
}
}
}
}总结与注意事项
- 循环结构是关键: 服务器端需要一个外层循环来接受新连接,一个内层循环来处理与单个客户端的持续通信。如果服务器只处理一个客户端并随后终止,则可以省略外层循环。
- 优雅终止: 客户端通过 readLine() 返回 null 来检测服务器的关闭,服务器通过检查特定指令(如“stop”)或 readLine() 返回 null 来检测客户端的意图或断开。
- 资源管理: 始终使用 try-catch-finally 结构来确保所有 Socket 和流资源在不再需要时被正确关闭,这是避免资源泄露和程序不稳定的最佳实践。
- 异常处理: 网络通信中,各种异常(如 ConnectException, SocketException, IOException)很常见。妥善的异常处理能够提高程序的健壮性。
- 协议设计: 在实际应用中,通信双方需要约定一个明确的协议,包括如何发送数据、如何识别命令、如何处理错误以及如何终止连接。本教程中的“stop”指令即是一个简单的协议示例。
- 并发考虑: 上述服务器示例是单线程的,一次只能处理一个客户端。对于需要同时处理多个客户端的场景,需要引入多线程或线程池机制来为每个 connectionSocket 分配一个独立的线程进行处理。










