從tcp原理角度理解Broken pipe和Connection Reset by Peer的區別
以前我們經常會碰到Broken pipe
或者Connection reset by peer
之類的異常,但是tcp實現裡什麼情況下會丟擲這些異常呢,以前我給對方的回答都是模稜兩可的,自己說實話都沒把握,因為自己也沒有驗證過,對它們的認識都是從網上看來的,正確與否也不知道,昨天獨明突然又問到這個問題,前段時間正好對tcp這塊研究了一段時間,有了點理論知識之後再從實踐角度對此問題進行一下分析,下面對我這次的調研過程進行下描述與大家分享,希望大家以後對此類問題都能很自信地應答。
三次握手和四次揮手過程
在講具體的原因之前,我們有必要補充下tcp這塊的一些基礎知識,我們都知道tcp通訊有三次握手和四次揮手,網上介紹的文章也一大堆,圖我也懶得畫了,直接網上找一個圖給大家
三次握手是最前面的三條線表示的過程,四次揮手是最後面的四條線表示的過程,裡面涉及到幾個關鍵詞,SYN,ACK,FIN,MSS,其中SYN是主要用在三次握手過程中的,FIN用在四次揮手過程中,ACK在三次握手和四次揮手過程中的作用就是對收到的SYN和FIN做一個確認,SYN,FIN等存在於TCP頭裡(tcp報文圖也給大家弄了個圖,不用再去找啦),0/1表示有無此標記,在tcp實現裡後面還會跟一個依次遞增的數字,比如上面的J,K等,確認就是遞增這些數字(真正的資料報文的ack除外),MSS是表示每一個tcp報文裡資料欄位的最大長度,不包括tcp頭的大小噢 相信大家看到這兩個圖會對這些概念有了一個清晰的認識了
tcpdump抓包工具
介紹了基礎原理之後,再介紹下抓包工具,tcpdump,這工具對你瞭解tcp的整個過程會非常有幫助,在你無法除錯tcp實現的情況下這個工具自然也是必不可少的,具體用法網上有很多介紹,直接從man page上也可以看到詳細的介紹,我也不多說啦,下面的截圖就是tcpdump根據tcp通訊過程獲取到的
這要稍微提下tcpdump的結果和上面的幾個過程的對應關係 前面三條其實就是我們上面所說的三次握手,四次握手過程上面沒有完全表現出來,只完成了一半的揮手過程(5,8兩條表示的) 裡面有幾個標識S,F,ack,P,其實還有個R,如果有這些標識那麼在tcp頭裡的SYN,FIN,ACK,PSH,RET分別為1,其中PSH表示要求tcp立即將資料傳遞給上層,不要做別的什麼處理,RET這個表示重置連線,也是和我們今天討論的問題有很大關係的FLAG,下面會詳細介紹
reset報文傳送場景
RST的標誌位,這個標識為在如下幾種情況下會被設定,以下是我瞭解的情況,可能還有更多的場景,沒有驗證
- 當嘗試和未開放的伺服器埠建立tcp連線時,伺服器tcp將會直接向客戶端傳送reset報文
- 雙方之前已經正常建立了通訊通道,也可能進行過了互動,當某一方在互動的過程中發生了異常,如崩潰等,異常的一方會向對端傳送reset報文,通知對方將連線關閉
- 當收到TCP報文,但是發現該報文不是已建立的TCP連線列表可處理的,則其直接向對端傳送reset報文
- ack報文丟失,並且超出一定的重傳次數或時間後,會主動向對端傳送reset報文釋放該TCP連線
Broken pipe以及Connection reset by peer
做了這麼些鋪墊之後下面進入正題,那麼Broken pipe
或者Connection reset by peer
分別代表什麼意思呢,下面從glibc的原始碼裡有對此的介紹
#. TRANS Broken pipe; there is no process reading from the other end of a pipe. #. TRANS Every library function that returns this error code also generates a #. TRANS @code{SIGPIPE} signal; this signal terminates the program if not handled #. TRANS or blocked. Thus, your program will never actually see @code{EPIPE} #. TRANS unless it has handled or blocked @code{SIGPIPE}. #: sysdeps/generic/siglist.h:39 sysdeps/gnu/errlist.c:359 #: sysdeps/unix/siglist.c:39 msgid "Broken pipe" msgstr "斷開的管道" #. TRANS A network connection was closed for reasons outside the control of the #. TRANS local host, such as by the remote machine rebooting or an unrecoverable #. TRANS protocol violation. #: sysdeps/gnu/errlist.c:614 msgid "Connection reset by peer" msgstr ""
其實我們java異常裡看到的Broken pipe
或者Connection reset by peer
資訊不是jdk或者jvm裡定義的,我看到這些關鍵字往往會首先搜尋下jdk或者hotspot原始碼找到位置進行上下文分析,但是這次沒找到,後面才想到應該是linux或者glibc裡定義的,果然在glibc離看到了如上的描述和定義
對於Broken pipe
在管道的另外一端沒有程序再讀的時候就會丟擲此異常,Connection reset by peer
的描述其實不是很正確,從我的實踐來看只描述了一方面,其實在某一端正常close之後,也是可能會有此異常的。
異常模擬
從我的測試場景是這樣的, 共同的前提是客戶端向服務端發了資料之後立馬呼叫close關閉socket並進程退出,而服務端在收到客戶端的資料之後sleep一會,保證對方的socket已經關閉,接著分別進行兩種場景測試
場景:
-
服務端往socket裡寫一次資料,返回繼續做select
-
服務端連續寫兩次資料,必須保證兩次的buffer都是有資料的,也就是保證ByteBuffer的pos和limit要不是一個值
結果:
-
會丟擲Connection reset by peer
-
會丟擲Broken pipe
分析:
-
當我們往一個對端已經close的通道寫資料的時候,對方的tcp會收到這個報文,並且反饋一個reset報文,tcpdump的結果如下所示,當收到reset報文的時候,繼續做select讀資料的時候就會丟擲
Connect reset by peer
的異常,從堆疊可以看得出 -
當第一次往一個對端已經close的通道寫資料的時候會和上面的情況一樣,會收到reset報文,當再次往這個socket寫資料的時候,就會丟擲
Broken pipe
了 ,根據tcp的約定,當收到reset包的時候,上層必須要做出處理,呼叫將socket檔案描述符進行關閉,其實也意味著pipe會關閉,因此會丟擲這個顧名思義的異常