使用 Java 写了个简单的 TCP 服务端和客户端,然后使用 Wireshark 进行抓包。
测试1:服务端不关闭 socket
服务端:
- 服务端监听 8888 端口,然后 accept
- 当有客户端接入时,获取 InputStream,然后循环打印
- 当退出循环时,不关闭 client,直接回到 accept
客户端:
- 连接服务端后,等待从控制台输入,让后将数据同通过 OutputStream 发给服务端
- 当输入 “exit”,退出循环,关闭 socket
抓包:
首先前三个报文为 TCP 三次握手报文,可以看到双方发送的第一个报文都会带上 “选项” 字段,比如 MSS、WS 和 SACK_PERM。
接着的四个报文是客户端分别往服务端发送字符串 “1\r\n” 和 “2\r\n”,可以看到使用了 PSH 标识,是因为输出的时候我每次都调用了 flush(),以及每次发送消息,对端都会返回一个空的 ACK 报文来确认。
再接着的两个报文,是客户端主动关闭 socket。此时连接处于半关闭状态,客户端到服务端的连接已释放,而服务端到客户端的连接还保持着,所以客户端不能再发送数据给服务端,而服务端可以发送数据给客户端。
最后,等待 120s 后,由于客户端处于 FIN-WAIT-2(终止等待2)状态,等待了一定时间后并没有收到服务端的后续报文,所以发送了一个 RST 重置报文给服务端。
所以可见,如果只是一端关闭了 socket,那么连接只能关闭一半,另外一半将一直保持着。
上述问题就是由于服务端在客户端关闭 socket 后,并没有相应的关闭自己这边的 socket,这会导致服务端堆积大量处于 CLOSE-WAIT(关闭等待)状态的连接,直至客户端发送 RST 重置报文,或者一直保留。
测试2:客户端关闭 socket,连接进入半关闭状态下,服务端发送数据,再关闭 socket
我们改变方案,在客户端关闭 socket 后,连接处于半关闭时,服务端继续给客户端发送数据,然后关闭对应的 socket:
此时看最后两个报文,服务端发送一个 PSH 的数据报文,但是客户端由于 socket 已关闭,所以响应了一个 RST 重置报文,并且后续的两个挥手报文服务端也没有发出了。
上面两个例子可以看见,RST 报文可以重置一个 TCP 连接,即通知对端此次连接直接结束。
测试3:客户端关闭输出流,连接进入半关闭状态下,服务端发送数据,客户端从输入流接收,服务端再关闭 socket,客户端不关闭 socket。
最后一个方案,客户端不直接关闭 socket,而是先关闭输入流 socket.shutdownOutput(),然后从 InputStream 等待并输出数据,最后再关闭 socket。
服务端在接受完客户端发送的数据后,返回一个 “After half-close” 给客户端,然后把对应的 socket 关闭。
此时可以看到,最后六个报文中,前面两个是客户端的连接释放报文,中间两个是半关闭状态下服务端发送的数据报文,最后两个是服务端的连接释放报文。
总结:
- socket.shutdownOutput() 可以发起连接释放报文,当对端确认后,TCP 连接就会处于半关闭状态。如果应用程序想在半关闭状态下接受对端发送的数据,要使用它。
- 当处于半关闭时,只要对端关闭 socket,就会完成四次挥手,即使本端的 socket 不关闭。
进入半关闭状态两种方法:
- 关闭本端 socket,而对端不关闭
- 这种方法会导致没有办法继续接收对端的数据,此时如果对端发来了数据,那么会自动返回 RST 报文
- 如果对端没有继续发送数据,而是也选择关闭 socket,那么四次挥手完成
- 关闭本端的 Output,socket.shutdownOutput()
主动关闭请求也能由服务端发起,所以编码时要注意一下。
其他:
- 当本端无论是 shutdownInput() 还是 shutdownOutput(),都不会影响到对端的 Stream 的状态,因为 Stream 是对于一个 JVM 来说的。
问题:
Q:如果先调用 socket.shutdownInput() 会发生半关闭吗?还是全关闭?
A:不会发生半关闭,但是对端使用 OutputStream 发送数据的时候,本端会响应 RST 重置报文。对端在代码层面不会抛出异常,因为连接还没有关闭。