为何Socket的InputStream仅在Socket关闭时才返回-1?
问题背景
我们通常使用(c = inputStream.read()) == -1的条件来检测是否到达"流末尾",但Socket的OutputStream仅在Socket关闭时才会发送流末尾(EOF)信号。
这导致以下服务端代码与客户端代码无法正常工作:
服务端代码
public class Server { public static int port = 27018; public static void main(String[] args) throws IOException, InterruptedException { char c = ' '; ServerSocket serverSocket = new ServerSocket(port); System.out.println("Server started"); Socket connectionSocket = serverSocket.accept(); serverSocket.close(); String bufferOuputString = ""; InputStream in = connectionSocket.getInputStream(); OutputStream out = connectionSocket.getOutputStream(); System.out.println("Connection established on port " + port); out.write("Welcome to my server !".getBytes()); System.out.println("Waiting for client messages"); while(c != '/'){ c = (char) in.read(); if(c == '*'){ System.out.println(bufferOuputString); bufferOuputString = ""; } else { bufferOuputString += c; } } connectionSocket.close(); } }
客户端代码
public class Client { public static int port = 27018; public static void main(String[] args) throws IOException { try (Socket socket = new Socket("127.0.0.1", port)) { int c; Scanner sc = new Scanner(System.in); String bufferInputString = ""; String bufferString = ""; OutputStream out = socket.getOutputStream(); InputStream in = socket.getInputStream(); while((c = in.read()) != -1){ bufferInputString += (char) c; } System.out.println(bufferInputString); while(!bufferString.contains("/")){ System.out.println("You can write messages to server:"); bufferString = sc.nextLine() + "*"; out.write(bufferString.getBytes()); } } } }
客户端会一直被InputStream.read()调用阻塞,因为它在等待永远不会到来的流末尾信号——服务端发送欢迎消息后,还在等待客户端输入,并没有关闭Socket。
为验证这一规则,我测试了另一版服务端代码:
public class Server { public static int port = 27018; public static void main(String[] args) throws IOException, InterruptedException { char c = ' '; ServerSocket serverSocket = new ServerSocket(port); System.out.println("Server started"); Socket connectionSocket = serverSocket.accept(); serverSocket.close(); String bufferOuputString = ""; InputStream in = connectionSocket.getInputStream(); OutputStream out = connectionSocket.getOutputStream(); System.out.println("Connection established on port " + port); out.write("Welcome to my server !".getBytes()); Thread.sleep(3000); connectionSocket.close(); } }
3秒后客户端成功打印出欢迎信息,随后进入无限循环,这证实了Socket的OutputStream仅在Socket.close()时才发送流末尾信号。
我的问题是:为何要做出这样的设计选择?
解答
1. 底层TCP协议的全双工特性决定
Socket基于TCP协议实现,而TCP是全双工通信协议——同一连接上,通信双方可以同时发送和接收数据,两个方向的传输通道是独立的。
流末尾(EOF)对应的是TCP协议中的FIN报文,一旦某一端发送FIN,就意味着这一端的发送通道被关闭,之后再也不能向对方发送数据,但仍然可以接收对方发来的数据。如果在还需要持续发送数据的场景下就触发EOF,等于提前关闭了自己的发送通道,直接破坏了TCP全双工的核心特性。
2. 适配持续交互的通信场景
绝大多数Socket通信是持续交互型的:比如客户端发请求→服务器回响应→客户端再发新请求→服务器再回响应,这个过程中连接需要保持打开状态。如果每次发送完一段数据就触发EOF,意味着每次交互都要重新建立TCP连接——而TCP连接建立的三次握手、关闭的四次挥手都有额外开销,会极大降低通信效率。
只有当通信双方明确知道再也不需要进行任何数据传输时,关闭Socket发送FIN(也就是触发InputStream.read()返回-1)才是合理的,这相当于明确告知对方:"我这边的所有数据都发送完毕,不会再发新数据了"。
3. 回到你的代码问题
你的客户端代码一开始就死等EOF,但服务端发送欢迎消息后仍在等待客户端输入,并未关闭Socket,所以客户端永远等不到-1而被阻塞。解决这类问题的正确思路是约定自定义通信协议:比如用固定长度的字节标记消息结束、用特定字符(如换行符)作为消息分隔符,或者在消息开头携带长度字段,而不是依赖流末尾信号。
内容的提问来源于stack exchange,提问作者MirageHF

