深入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)
其中控制位:
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异常了。