Java ServerSocket客户端接收字节数异常问题咨询
通过Java ServerSocket向网络发送图片字节及其他数据时,代码出现异常:多次运行相同的服务端与客户端代码,有时死锁,有时运行正常。经测试发现,程序运行结果受服务端两条发送指令间的延迟以及发送缓冲区大小影响。
服务端核心代码
ServerSocket serverSocket = new ServerSocket(4999); Socket clientSocket = serverSocket.accept(); OutputStream outStream = clientSocket.getOutputStream(); PrintWriter outWriter = new PrintWriter(outStream, true); byte buf[] = new byte[2000]; // 示例缓冲区大小为2000 System.out.println("sending: " + buf.length + " bytes"); outWriter.println(buf.length); // 如果在这里加延迟(比如1000次System.out.println(1)),客户端能正确接收缓冲区;否则客户端接收0字节 outStream.write(buf); clientSocket.close(); serverSocket.close();
客户端核心代码
Socket clientSocket = new Socket(InetAddress.getByName("<address of the server>"), 4999); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); int len = Integer.valueOf(in.readLine()); System.out.println("expected length: " + len); byte[] receiveBuf = clientSocket.getInputStream().readNBytes(len); System.out.println("received buffer length: " + receiveBuf.length); /* 此时receiveBuf.length应该等于预期大小,但只要服务端发送的缓冲区过大(约大于1443字节) 或者两条发送指令间延迟过小,客户端就会打印出接收的字节数小于预期的结果。 比如缓冲区大小为20000字节时,客户端实际接收11808字节,正好是20000 - 2^13 */ clientSocket.close();
测试现象
按理论,TCP/IP应保证数据正确收发,不受设备速度或指令间延迟影响,但实际出现以下异常:
- 仅在跨设备通信时出现,同设备通信无问题
- 服务端添加
System.out.println()增加延迟时,客户端可正确接收数据 - 不同缓冲区大小的测试结果:
- 服务端发送2000字节:客户端预期2000字节,实际接收0/2000/0字节(三次测试)
- 服务端发送8192字节:客户端预期8192字节,实际接收6/0/8192字节(三次测试)
- 服务端发送20000字节:客户端预期20000字节,实际均接收11808字节(三次测试)
- 服务端发送100000字节:客户端预期100000字节,实际均接收91808字节(三次测试)
- 服务端发送1000000字节:客户端预期1000000字节,实际接收998540/1000000/991808字节(三次测试)
问题:为何TCP传输的结果与完整性会受服务端程序执行速度及发送缓冲区大小影响?
这不是TCP本身的问题,是代码里的流处理逻辑错误导致的,核心问题集中在两个点:
1. 客户端的流复用冲突
客户端同时使用BufferedReader和直接调用Socket.getInputStream()读取数据,而BufferedReader会预读数据到自身内部缓冲区。当你调用in.readLine()读取长度后,BufferedReader已经把后续的部分(甚至全部)字节读到了自己的缓冲区中,这时再用getInputStream().readNBytes(len)读取,自然只能拿到剩下的字节,甚至读不到,导致接收长度不足。
比如你测试中20000字节的情况,BufferedReader默认预读缓冲区大小是8192字节(2^13),所以readNBytes只能读到20000-8192=11808字节,和你看到的结果完全吻合。
2. 服务端的输出流刷新时机
服务端用PrintWriter的println方法时,虽然构造时指定了autoFlush=true,但autoFlush仅在输出换行符或字节数达到缓冲区阈值时触发刷新,再加上TCP的Nagle算法可能延迟发送小数据包,导致outWriter.println(buf.length)的长度数据可能留在服务端发送缓冲区,和后续outStream.write(buf)的字节一起发送。此时客户端的BufferedReader在读readLine()时,会把后面的字节也预读到自己的缓冲区,导致后续readNBytes无法读到完整数据。
而添加延迟(比如多次System.out.println)时,服务端有足够时间将长度数据刷新并发送,客户端的BufferedReader只会读到长度那一行,不会预读后续字节,所以readNBytes能拿到完整数据。
解决办法
方案一:统一使用单个流处理所有数据
客户端不要混用BufferedReader和原始输入流,改用DataInputStream统一读取,避免预读问题:
// 修改后的客户端代码 Socket clientSocket = new Socket(InetAddress.getByName("<server address>"), 4999); DataInputStream in = new DataInputStream(clientSocket.getInputStream()); // 读取长度并转换为整数 int len = Integer.parseInt(in.readLine()); System.out.println("expected length: " + len); byte[] receiveBuf = new byte[len]; in.readFully(receiveBuf); // readFully会阻塞直到读满指定长度 System.out.println("received buffer length: " + receiveBuf.length); clientSocket.close();
方案二:服务端手动刷新输出流
在outWriter.println(buf.length)之后,强制调用flush()确保长度数据立即发送:
// 修改后的服务端代码 outWriter.println(buf.length); outWriter.flush(); // 强制刷新,确保长度数据被立即发送 outStream.write(buf);
更规范的做法:使用二进制协议传输长度
用PrintWriter发送字符串形式的长度容易出现编码和换行符问题,更可靠的方式是直接发送二进制int类型:
// 服务端代码 DataOutputStream out = new DataOutputStream(clientSocket.getOutputStream()); out.writeInt(buf.length); // 直接写入4字节的int类型长度 out.write(buf); // 客户端代码 DataInputStream in = new DataInputStream(clientSocket.getInputStream()); int len = in.readInt(); // 读取4字节的int类型长度 byte[] receiveBuf = new byte[len]; in.readFully(receiveBuf);
这种方式完全避免了字符串解析和缓冲区预读的问题,是TCP传输结构化数据的标准做法。
内容的提问来源于stack exchange,提问作者oswin122

