vlambda博客
学习文章列表

深入TCP协议底层解读Broken pipe产生的根因

想必你也在服务器上遇到过类似以下的异常:
文件下载失败!org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:356) at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:825)

产生这个异常的表面原因比较简单:客户端主动close连接后,服务端继续往管道中写数据。

前文《》中我们介绍TCP相关知识时,有讲过其头信息格式:

深入TCP协议底层解读Broken pipe产生的根因

其中控制位:

  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1

  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。


前三个标识比较常见了,最后一个 RST 含义是TCP连接中的一端异常中断会立刻发送一个RST标志给对端,该 TCP 连接将跳过四次挥手直接关闭。如果刚好此时对端send buffer中还有数据正在往管道中写,对端就有可能会抛出Broken pipe异常。

我们知道,终止一个TCP连接的正常方式是发送FIN。在发送缓冲区中所有排队数据都已发送之后才发送FIN,正常情况下没有任何数据丢失。但我们发送一个RST报文而不是FIN来中途关闭一个连接,就会导致异常关闭。


模拟出现RST报文的场景,最简单方法就是设置SO_LINGER选项。SO_LINGER是TCP网络编程中的重要选项,在默认情况下,当调用close关闭TCP连接时,close会立即返回,但是如果send buffer中还有数据,系统会试着先把send buffer中的数据发送出去,然后close才返回。


我们使用以下代码进行模拟。

Client端代码:

public class SocketClient {
//client程序 public static void main(String[] args) { try { Socket s = new Socket(); //默认值为-1,这里需要设置为0,表示立即关闭连接 s.setSoLinger(true, 0); s.connect(new InetSocketAddress("localhost", 8888));
OutputStream os = s.getOutputStream(); os.write("hello world".getBytes()); s.close();
LockSupport.park(); }catch (Exception e){ e.printStackTrace(); } }}


Server端代码:

public class SocketServer {
//server程序 public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(8888); Socket s = ss.accept(); InputStream is = s.getInputStream(); byte[] buf =new byte[1024]; int len = is.read(buf); System.out.println("收到数据: " + new String(buf, 0, len));
Thread.sleep(10000);
s.getOutputStream().write("hahaha".getBytes()); System.out.println("发送完成");
LockSupport.park(); }catch (Exception e){ e.printStackTrace(); } }}

分别运行SocketServer和SocketClient,猜猜会发生什么?

不出意外,果然出现了Broken pipe异常。


为了验证上文的结论,我们使用Tcpdump工具来抓包看看整个过程是否如我们预期一样(Tcpdump工具的使用非本文重点,这里就不做过多介绍,读者可自行百度之)。我们重点关注以下flag对照关系即可。

TCP Flag Tcpdump Flag Meaning
SYN [S] Syn packet, a session establishment request.
ACK [A] Ack packet, acknowledge sender’s data.
FIN [F] Finish flag, indication of termination.
RESET [R] Reset, indication of immediate abort of conn.
PUSH [P] Push, immediate push of data from sender.
URGENT [U] Urgent, takes precedence over other data.
NONE [.] Placeholder, usually used for ACK.

我们先开启tcpdump,抓取网卡上8888端口的数据包:

tcpdump -i lo0 port 8888

最终抓到的数据如下:


我们来简单分析下以上报文的含义:

  • 第一行和第二行分别代表TCP三次握手中的前两次client向server发送SYN握手和server向client发送SYN+ACK握手。

  • 第三行应该是TCP握手中的第三次握手

  • 第四行小编没看懂,似乎与理论模型对不上?

  • 第五行中Flags是 [P.],P是push的意思就是发数据,这里代表client向server发送数据,length 11就是client发送的数据hello world的长度

  • 第六行意思是server向client发送ack表示已经接收了数据hello world

  • 第七行重点来了,Flags[R.],R就代表RST报文,client向server发送了RST报文。且后面再无新报文,代表连接已经关闭

怎么样,以上报文是否已经印证了我们前文关于Broken pipe产生的结论?


分析到这里,Broken pipe异常产生的根本原因我们已经很清楚了,根本原因是连接的一端发送了RST报文,但是刚好对端的send buffer中还有数据要写入,就会产生Broken pipe异常了。