
本文深入探讨java服务器-客户端应用在循环读取utf数据时可能遇到的阻塞问题。通过分析一个具体的代码案例,揭示了将system.in的scanner与网络输入流混用是导致程序意外挂起的主要原因。文章将提供详细的解决方案和避免此类问题的最佳实践,以确保网络通信的顺畅进行。
1. 问题描述:服务器循环阻塞现象
在构建Java服务器-客户端应用程序时,开发者常会遇到各种I/O和并发问题。一个常见且容易被忽视的问题是,服务器在与客户端进行网络通信的循环过程中意外阻塞,导致程序挂起。具体表现为,服务器在从客户端接收一系列任务的描述和状态时,在处理完第一个或第二个任务后,不再继续接收后续任务,而是陷入无限等待状态。
这种现象通常发生在服务器端期望通过网络套接字读取客户端发送的数据,但实际上却在等待本地标准输入(如控制台输入)时。
2. 代码分析:定位阻塞根源
为了深入理解问题,我们分析一个典型的服务器-客户端任务管理应用。服务器(Server.java)旨在接收客户端(Client.java)指定数量的任务,并为每项任务收集描述和状态。
服务器端关键代码片段:
立即学习“Java免费学习笔记(深入)”;
// Server.java
public class Server {
// ... 其他成员变量
ArrayList tarea = new ArrayList();
Scanner sc = new Scanner(System.in); // 问题点1:在服务器代码中创建了System.in的Scanner
public void iniciarServidor() throws IOException {
while (true) {
// ... 接受客户端连接,获取输入输出流
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
DataInputStream input = new DataInputStream(socket.getInputStream());
// ... 与客户端交互,获取任务数量
output.writeUTF("Muy buenas " + nombre + " especifique numero de tareas:");
int numeroTareas = input.readInt();
for(int i = 0; i < numeroTareas; i++){
output.writeUTF("Especifique descripcion para la tarea "+i+" :");
String descripcion = input.readUTF(); // 服务器等待客户端发送描述
output.writeUTF("Especifique el estado 'true o false' para la tarea "+i+" :");
boolean estado = input.readBoolean(); // 服务器等待客户端发送状态
Tarea t = new Tarea(descripcion, estado);
tarea.add(t);
}
sc.nextLine(); // 问题点2:服务器在此等待本地控制台输入,导致阻塞
// ... 后续发送任务列表给客户端的代码
}
}
} 问题分析:
- Scanner sc = new Scanner(System.in);:在服务器的类成员中初始化了一个Scanner对象,它绑定到标准输入流System.in。这意味着这个Scanner是用来读取服务器运行环境的控制台输入的。
- sc.nextLine();:在服务器的for循环(用于从客户端接收任务描述和状态)结束后,调用了sc.nextLine()。此时,服务器本应继续处理网络通信或等待客户端的下一个网络请求。然而,由于sc是绑定到System.in的,sc.nextLine()会使服务器进程挂起,等待用户在服务器的控制台上手动输入一行文本并按回车。
这正是导致服务器在完成前几次网络数据接收后,突然“挂起”的根本原因。服务器错误地将网络通信流(DataInputStream)与本地控制台输入流(System.in)混淆,导致在不恰当的时机等待本地输入。
3. 客户端代码审视:通信模式的不匹配
除了服务器端的阻塞问题,客户端的通信逻辑也存在与服务器期望不匹配的情况。
客户端关键代码片段:
// Client.java
public class Client {
// ... 其他成员变量
public void iniciarCliente() throws IOException {
// ... 获取输入输出流
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
Scanner sc = new Scanner(System.in); // 客户端使用System.in读取本地输入
// ... 客户端发送姓名
output.writeUTF(sc.nextLine());
// 客户端发送任务数量
System.out.println(input.readUTF()); // 服务器提示
output.writeInt(sc.nextInt());
sc.nextLine(); // 消耗nextInt()留下的换行符,这是正确的做法
// 客户端只发送了一次任务的描述和状态
System.out.println(input.readUTF()); // 服务器请求描述
descripcion = sc.next();
output.writeUTF(descripcion);
System.out.println(input.readUTF()); // 服务器请求状态
estado = sc.nextBoolean();
output.writeBoolean(estado);
// ... 后续接收服务器确认信息,但只接收了固定的几行
}
}问题分析:
- 循环不匹配: 服务器端在接收到任务数量numeroTareas后,会进入一个for循环,期望接收numeroTareas次任务的描述和状态。然而,客户端代码中,在发送完任务数量后,只发送了一次任务的描述和状态。
- 数据接收不足: 客户端在接收服务器发送的所有任务信息时,也只使用了固定数量的System.out.println(input.readUTF());语句,而不是根据任务数量进行循环接收。
这种服务器期望循环接收而客户端只发送/接收一次的通信模式不匹配,会导致服务器在第一次任务数据接收后,第二次循环中无法收到预期数据(因为客户端没发),从而在等待input.readUTF()或input.readBoolean()时发生阻塞(如果sc.nextLine()的阻塞问题被解决的话)。
4. 解决方案与代码重构
解决上述问题需要遵循两个核心原则:输入流的隔离和通信协议的同步。
4.1 服务器端 (Server.java) 修改
服务器端应严格区分网络输入和本地输入。在处理客户端请求时,只使用通过Socket.getInputStream()获取的网络输入流。
// Server.java (修正版片段) package server; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket










