1. 程式人生 > >connection reset by peer, socket write error問題分析

connection reset by peer, socket write error問題分析

getc 主動 連接 返回 知識 可能 -- 成功 connect

上次寫《connection reset by peer, socket write error問題排查》已經過去大半年,當時把問題“敷衍”過去了。
但是此後每隔一段時間就會又想起來,baidu、google一番,可能也會再拉周圍的人小討論一下,然後無果而終。淡忘,想起,淡忘,又想起,揮之不去。

這個周末它又在腦海中浮現,這次總算理解了這個問題,答案就在一本買了很久的新書《HTTP權威指南》中。如果懶得看下面的啰嗦,可以去直接看書中的《4.7.4 正常關閉連接》章節。實際上,我也只是為了找答案直接通過目錄翻到了這一章,以後再找時間完整看一遍吧。

問題現象

再重新描述一下這個問題的現象和起因。
問題來源於一個http的文件上傳接口,接口會先對一些參數簽名進行校驗,參數簽名通過之後才會取出InputStream,將文件數據保存起來。如果參數校驗失敗或者檢查到文件已經存在(參數上會帶md5),則直接返回了錯誤信息。

實際上大多數情況挺正常的,但是偶爾在客戶端會出現“connection reset by peer, socket write error”。這個錯誤通過搜索引擎找了答案,都不能解釋遇到的現象,只有嘗試著猜測和重現了。經過嘗試發現,只有比較“大”的文件在參數校驗失敗或者屬於重復上傳的情況才能重現這個錯誤。
所以猜測應該是當客戶端上傳大文件時,服務端接收到了http header就拿到了接口參數,可以開始進行校驗了,不符合條件時就直接返回了Response,關閉OutputStream的同時也把InputStream給close掉了。
基於此猜測,在服務端改動了一下,返回Response之前,先request.getInputStream().skip(request.getContentLength)
。果然。問題不會出現了,雖然接口處理變慢了。
然後,我通過wireshark進行了抓包,實際上也抓到了服務端返回的錯誤碼信息,也就是說服務端在這個情況下,Response已經輸出了,而且很可能客戶端是收到了的。
這個是令人比較矛盾的地方,並不是服務端數據沒有輸出啊,為什麽客戶端接收不到這個響應,而且是直接報了一個奇怪的錯誤呢?

翻了書之後,才弄清楚了其中的細節,細節是魔鬼啊。

關於連接的關閉

TCP連接是雙向的,TCP連接的每一端都有一個輸入隊列和一個輸出隊列,用於數據的讀或者寫。
放入一端輸出隊列的數據會被傳送到另一端的輸入隊列。

Recv-Q 輸入<-------------------------------------------------輸出 Send-Q
Client ------------------------------------------------------- Server
Send-Q 輸出------------------------------------------------->輸入 Recv-Q                                                       

連接的全關閉和半關閉

當應用程序的通過TCP通信時,Client端和Server端都可以關閉輸入和輸出信道中的某一個,或者兩個都關閉。
如果只關閉其中的一個,稱之為“半關閉”,如果兩個都關閉,稱之為“全關閉”。
這兩種操作對應java裏的Socket有相應的方法,shutdownInput()或者shutdownOutput()是半關閉操作,close()是全關閉操作。

connection reset錯誤的產生

可以看到不論是對於客戶端還是服務端,發送數據(輸出信道)總是主動的,而接受數據(輸入信道)總是被動的。

  1. 當主動發送數據的一方完成數據發送,進行shutdownOutput之後,另一方的接受端在從緩沖區讀出所有數據後會收到一條通知,說明數據流結束了,這樣接受端就知道連接關閉了。
  2. 但是反過來,如果被動接收數據的一方想要停止接收數據,也就是shutdownInput時,它並不知道數據發送方是否還要發送數據;
    當接收端直接shutdownInput時,數據發送方卻可能還在往緩沖區寫數據呢,如果這個時候對方關閉連接的通知還沒有到達這邊,那麽數據依然會被傳送到已經shutdownInput另一端,這個時候另一端的操作系統會回復一條“連接被對方重置”的報文過去。
    當數據發送方出現這種情況時,大多數操作系統都會作為很嚴重的錯誤來處理,會刪除掉對端還未讀取的所有緩存數據。

所以我們可以看到關於連接關閉存在3種情況(從某一端的角度):

  1. 完全關閉:直接關閉輸入和輸出
  2. 半關閉(Output):關閉輸出,
  3. 半關閉(Input):關閉輸入

從上面的分析也可以看到,只有關閉輸出是兩端各自可以掌握主動權的,也就是相對安全的。

正常關閉

HTTP規範只是建議了在要關閉一條連接時應該正常的關閉傳輸連接,但是沒有說明具體該如何去做。
由於只有輸出端是自己可以掌握主動權的,所以要想正常的關閉連接首先是各自關閉自己的輸出信道,同時等對方關閉輸出信道,這樣連接就完全關閉了,這樣就不會出現“connecton reset”錯誤了。
但是,理想是美好的,現實中可能會比較無奈,無法確保雙方都按照這個約定來操作。
所以除了做好自己這一方的關閉輸出信道外,還需要周期性檢查一下輸入信道(對應於對方的輸出)狀態(是否還有數據,是否到了流的末尾),如果經過一定時間對方沒有關閉還是需要強制結束以節省時間。

解決問題

問題的原因清楚了。回頭看看文件上傳接口的場景,就是服務端數據接收的一方在客戶端方處於發送數據的時候強制關閉了連接,也就造成了客戶端“connection reset”的錯誤。
那為什麽小文件在同樣的場景下沒問題呢?因為小文件數據量小,在服務端關閉連接時就已經傳輸完成了。
那怎麽解決大文件情況下的問題呢?貌似這個場景下沒辦法!因為服務端不應該在參數校驗不通過的情況下等著客戶端的數據流發送完,否則(實際上一開始說的臨時解決辦法skip真個content-length長度)就可能遇到可能安全問題(如果接口部署在局域網關系倒不大;如果部署在開放的互聯網環境下,那就危險了,也就是如果不懷好意的人拿幾個超大的文件少量的並發調用接口就可以把寶貴的帶寬給占據了)。

既然技術角度無法解決了,只有從業務的角度來解決這個問題了。可以將這個文件上傳接口拆分為兩個接口,一個上傳token生成接口,一個數據上傳接口。token生成接口負責參數校驗,如果校驗成功則返回一個臨時token,客戶端用拿到的token再去上傳數據。這樣對於正常的調用方客戶端應該不會再有問題,而對於非法的token不接收數據就很合理了。

舉一反三

回頭想想之前那篇文章中提到的找到的資料中說的服務端並發連接數達到上限、關掉瀏覽器等,都可以解釋的通了。

其他

這也反映了一個問題,搜索引擎往往只能找到少部分問題的真正答案,要想能夠舉一反三,還是得從書中獲取成體系的知識。只有全面系統的理解了一個知識體系,才能在遇到問題時具備以不變應萬變的能力。
假如之前對HTTP或者TCP有一定的理解,那這個問題應該很容易就想通了。

connection reset by peer, socket write error問題分析