從新分析connection reset by peer, socket write error錯誤緣由

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

這個週末它又在腦海中浮現,此次總算理解了這個問題,答案就在一本買了好久的新書《HTTP權威指南》中。若是懶得看下面的囉嗦,能夠去直接看書中的《4.7.4 正常關閉鏈接》章節。實際上,我也只是爲了找答案直接經過目錄翻到了這一章,之後再找時間完整看一遍吧。java

問題現象

再從新描述一下這個問題的現象和原由。
問題來源於一個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錯誤的產生

能夠看到不管是對於客戶端仍是服務端,發送數據(輸出信道)老是主動的,而接受數據(輸入信道)老是被動的。socket

  1. 當主動發送數據的一方完成數據發送,進行shutdownOutput以後,另外一方的接受端在從緩衝區讀出全部數據後會收到一條通知,說明數據流結束了,這樣接受端就知道鏈接關閉了。
  2. 可是反過來,若是被動接收數據的一方想要中止接收數據,也就是shutdownInput時,它並不知道數據發送方是否還要發送數據;
    當接收端直接shutdownInput時,數據發送方卻可能還在往緩衝區寫數據呢,若是這個時候對方關閉鏈接的通知尚未到達這邊,那麼數據依然會被傳送到已經shutdownInput另外一端,這個時候另外一端的操做系統會回覆一條「鏈接被對方重置」的報文過去。
    當數據發送方出現這種狀況時,大多數操做系統都會做爲很嚴重的錯誤來處理,會刪除掉對端還未讀取的全部緩存數據。

因此咱們能夠看到關於鏈接關閉存在3種狀況(從某一端的角度):搜索引擎

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

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

正常關閉

HTTP規範只是建議了在要關閉一條鏈接時應該正常的關閉傳輸鏈接,可是沒有說明具體該如何去作。
因爲只有輸出端是本身能夠掌握主動權的,因此要想正常的關閉鏈接首先是各自關閉本身的輸出信道,同時等對方關閉輸出信道,這樣鏈接就徹底關閉了,這樣就不會出現「connecton reset」錯誤了。
可是,理想是美好的,現實中可能會比較無奈,沒法確保雙方都按照這個約定來操做。
因此除了作好本身這一方的關閉輸出信道外,還須要週期性檢查一下輸入信道(對應於對方的輸出)狀態(是否還有數據,是否到了流的末尾),若是通過必定時間對方沒有關閉仍是須要強制結束以節省時間。操作系統

解決問題

問題的緣由清楚了。回頭看看文件上傳接口的場景,就是服務端數據接收的一方在客戶端方處於發送數據的時候強制關閉了鏈接,也就形成了客戶端「connection reset」的錯誤。
那爲何小文件在一樣的場景下沒問題呢?由於小文件數據量小,在服務端關閉鏈接時就已經傳輸完成了。
那怎麼解決大文件狀況下的問題呢?貌似這個場景下沒辦法!由於服務端不該該在參數校驗不經過的狀況下等着客戶端的數據流發送完,不然(實際上一開始說的臨時解決辦法skip真個content-length長度)就可能遇到可能安全問題(若是接口部署在局域網關係倒不大;若是部署在開放的互聯網環境下,那就危險了,也就是若是不懷好意的人拿幾個超大的文件少許的併發調用接口就能夠把寶貴的帶寬給佔據了)。

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

觸類旁通

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

其餘

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

相關文章
相關標籤/搜索