敘述
想必或多或少在Java的服務器都會遇到過這種異常,如下圖
由於Java偏上層,日常開發接觸系統底層的機會偏少,要搞清楚什麼原因導致的這種異常,肯定是先要百度google一番。
網絡解釋雲裏霧裏
百度+google下,巴拉巴拉還真不少介紹這個錯誤的文章。欣喜地翻了一篇又一篇,但好像我依舊不明白具體什麼原因導致的,雲裏霧裏啊。好吧,舉兩個例子:
例子一:
這上邊說的好像有點道理,寫個代碼做個試驗驗證下吧!直接上代碼:
//client程序public static void main(String[] args) { try { Socket s = new Socket(); s.connect(new InetSocketAddress("127.0.0.1",3113)); OutputStream os = s.getOutputStream(); os.write("hello".getBytes()); s.close(); System.in.read();//防止程序退出 }catch (Exception e){ e.printStackTrace(); } }//server程序public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(3113); Socket s = ss.accept(); InputStream is = s.getInputStream(); byte[] buf =new byte[1024]; int len = is.read(buf); System.out.println("recv:"+new String(buf,0,len)); Thread.sleep(10000); s.getOutputStream().write("hello".getBytes()); System.out.println("send over"); System.in.read(); }catch (Exception e){ e.printStackTrace(); } }
代碼邏輯比較簡單吧,client向server發送請求,然後調用close()關閉連接,服務端收到請求打印到控制檯,等待10秒(保證client關閉了連接),然後繼續向client發數據。看一下控制檯的結果:
挺討厭,就是不報Broken pipe異常。上邊的文章,想說相信你真的好難啊!那再看另一篇文章吧
例二:
這篇文章倒列舉了好幾種原因,點擊了stop按鈕?被tomcat停掉?線程機制產生jvm出錯?真不知他媽的在說什麼,難道就不能再具體點嗎?
這樣的文章看不上幾篇就煩了。
意外發現
網上找不到滿意的解釋,那就硬着頭皮翻翻講解底層一點的書籍吧。還真巧,在一本叫《UNIX網絡編程卷1》中獲得了一點靈感。如下截圖:
如下劃線部分所說:向某個已收到RST的連接執行寫操作時,將會返回EPIPE錯誤。EPIPE!PIPE!第一百零一靈感告訴我這與Broken pipe錯誤有關係。好了,有了新的發現就程序驗證吧。
爲了順利實驗,先把實驗用到的兩個知識點說一下吧。
知識準備之RST報文
終止一個TCP連接的正常方式是發送FIN。在發送緩衝區中所有排隊數據都已發送之後才發送FIN,正常情況下沒有任何數據丟失。但我們有時也可能發送一個RST報文段而不是FIN來中途關閉一個連接。這稱爲異常關閉。
現在知道RST報文的作用了,那就在大致列一下出現RST報文的場景吧:
1.connect一個不存在的端口;
2.向一個已經關掉的連接send數據;
3.向一個已經崩潰的對端發送數據(連接之前已經被建立);
4.close(sockfd)時,直接丟棄接收緩衝區未讀取的數據,並給對方發一個RST。這個是由SO_LINGER選項來控制的;
5.a重啓,收到b的保活探針,a發rst,通知b。
模擬出現RST報文的場景,最簡單地方法感覺就是使用SO_LINGER選項來控制,那接下來再瞭解下SO_LINGER選項吧!
知識準備之SO_LINGER參數
SO_LINGER是用來設置函數close()關閉TCP連接時的行爲。缺省close()的行爲是,如果有數據殘留在socket發送緩衝區中則系統將繼續發送這些數據給對方,等待被確認,然後返回。
設置此選項並把超時時間設置爲零,調用close()會立即關閉該連接,通過發送RST分組(而不是用正常的FIN|ACK|FIN|ACK四個分組)來關閉該連接。至於發送緩衝區中如果有未發送完的數據,則丟棄。
知識準備的差不多了,好了,準備開森的實驗了。
實驗驗證
這裏再將實驗代碼貼一份吧,跟上邊的實驗代碼唯一的區別就是這裏設置了SO_LINGER選項。
這下你信了吧。這時你是不是也有點好奇,真的是設置了SO_LINGER產生了RST報文嗎?client和server之間到底進行了怎麼樣的交互呢? 想看清具體client和server期間進行了怎樣的交互,那就只好抓包了。就用tcpdump抓包看吧,不管你會不會用,它都是簡單方便快捷的好工具,絕對是分析TCP的好幫手。
抓包分析
就按照上邊的實驗程序抓個包吧,又大又清晰地截圖^_^
簡單解釋下:localhost.50387是client端,localhost.cs-auth-svr是server端。
第一行:client向server發送SYN請求建立連接
第二行:server向client發送SYN也請求建立連接
第三行:client向server返回ACK表示同意連接
第四行:server向client發送ack?什麼?TCP三步握手建立連接怎麼變成四步了?啥時候的事啊咋沒通知我啊?難道我的mac不在狀態手滑了就發出去了?算了先不care這個問題了,知道的可以告訴下我。
第五行:看到Flags [P.]了嗎,P是push的意思就是發數據,這裏就是client向server發送數據,length 5就是client發送的hello的長度,沒錯吧
第六行:這裏是server向client發送ac表示已經接收了hello
第七行:這是重點,Flags[R.],R就代表RST報文,client向server發送了RST報文。
現在應該一切雲開月明瞭吧。^_^
收到RST包,繼續向對方寫數據就一定會報Broken pipe嗎?還真的被我試出個不會的情況。
特殊情況
這個特殊情況也很好理解,按照上邊說的:向一個已經關掉的連接send數據時會收到對方的RST報文。此時再向其sends數據就不會報Broken pipe。直接上測試程序和抓包吧
//client程序public static void main(String[] args) { try {
Socket s = new Socket();
s.connect(new InetSocketAddress("127.0.0.1",3113));
OutputStream os = s.getOutputStream();
os.write("hello".getBytes());
s.close();
System.in.read();//防止程序退出
}catch (Exception e){
e.printStackTrace();
}
}//server程序public static void main(String[] args) { try {
ServerSocket ss = new ServerSocket(3113);
Socket s = ss.accept();
InputStream is = s.getInputStream(); byte[] buf =new byte[1024]; int len = is.read(buf);
System.out.println("recv:"+new String(buf,0,len));
Thread.sleep(10000);
s.getOutputStream().write("hello".getBytes());
s.getOutputStream().write("hello2".getBytes());
System.out.println("send over");
System.in.read();
}catch (Exception e){
e.printStackTrace();
}
}
client調用close向server發送FIN,server向client發送hello,然後收到client的RST報文,繼續向client發送hello2。
上邊流程可以看到,client向server發送了RST報文,但是服務器繼續寫也不會報錯,畢竟誰讓client之前就向server發送了FIN表示正常關閉呢。