前言
昨天在處理post
請求的時候,遇到了socket
的inputstream
被阻塞的情況,前前后后查了很多資料,試了很多方法,但是問題依然無法解決。
我甚至還去看了Tomcat
的源碼,也沒發現好的解決思路,但是在查資料的過程中,我已經找到問題的原因了,所以今天主要是來梳理下問題的原因,總結下經驗,讓各位小伙伴在以后遇到類似問題的時候,可以少踩坑,少走彎路。
問題的原因
問題的原因很簡單,就是因為我們socket
實現用的是傳統的I/O
流,這種實現有一個顯著的缺點——阻塞,因為本身它就是阻塞是通信。
這種實現方式下的socket
通信處理過程是這樣的:客戶端像服務器端發起訪問請求(假設服務器已經啟動),服務器端接受訪問請求后,開始建立通信(InputStream
和OutputStream
),但是線路只有一條,發送消息的時候不能接收消息,接收消息的時候也是不能發送消息的。所以,在實際數據傳輸的時候,服務端必須接受完客戶端的所有消息后,才能返回響應消息。
在我們的項目中,阻塞就發生在接收消息階段,服務器端一直在接收客戶端消息,消息沒有接受完成,通信管路被占用,所以客戶端無法接收到響應。
這里還不得不說更詳細的原因,也就是消息接收一直不完成的原因。我們都知道,socket
通信是依賴InputStream
和OutputStream
流的,我們這里阻塞就是因為InputStream
,在處理客戶端請求的時候,我們需要不斷地從InputSteam
中讀取數據,但是InputStream
的read
方法都是阻塞的,也就是說它讀不到數據的時候,就一直等在那里等待客戶端的數據,因為它不知道客戶端的什么時候可以傳完,這樣線程就被阻塞了,后續業務也就無法處理。
read
方法,按照官方給的文檔,讀取完成后應該返回null
或者-1
,在讀取文件的的時候,是可以正常終止的,但是由於socket
本身傳輸數據形式的特殊性,這個返回結果基本上是不會出現的。也就是說,如果已經沒有數據了,你還在讀取,這時候只會讓線程阻塞。
如果這個時候,客戶端終止請求,服務端就知道數據傳輸完了,線程就不再阻塞了,但是由於連接斷開了,服務器的響應數據也就無法傳給客戶端了。
針對這個問題,大致有兩種解決方案,一種就是客戶端在發送數據最開始就告訴服務器,他要發送多少數據,然后服務器按照數據傳輸大小,終止讀取。
我們之前有做過一個和銀行對接的項目,用的就是socket
,當時的約定的規范就是先在消息最開始發送本次消息大小,然后再拼接上消息內容,這樣服務器可以先接收一個int
的消息大小,然后后續數據再根據這個大小去讀取,也就不會出現阻塞的情況。
另一種解決方案是在消息傳輸結束加一個結束符號。我們在最開始的get
請求處理實現中,就是根據消息最后的空行(\r\n
),如果讀到這個內容,后續就不再讀取在,這樣就不會發生阻塞。
不知道大家還記不記得我們昨天說的請求頭的知識點:
在post
請求中,請求頭與請求體之間是有一個空行的,但是在請求體結束后,並沒有結束標識,所以無法確定啥時候請求體完成,這就導致了我們前面說的阻塞。
也有資料說,在post
請求頭中增加content-length
,可以解決這個問題,從理論上講應該沒啥問題,但是我在實際測試的時候,發現也有問題,有時間再研究下。
總結
任何技術的誕生和出現,都是因為其他技術的缺陷或者局限性。Nio
就是在這樣的背景之下出現的,Nio
全稱就是new I/O
,表面它是一種新的實現方式的I/O
,它是一種非阻塞式流,它可以讓你在讀的同時,實現的操作,而且目前NIO
的應用特別廣泛,netty
就是基於Nio
構建的一個web
框架。
說了這么多,其實就想說一句,如果某種實現方式滿足不了當下的業務需求,那就換種實現方式繼續戰斗。只要思想不滑坡,辦法總比問題多😹
另外需要補充說明下,后面我會再試下content-length
的方式,如果還是解決不了這個問題,就打算直接上NIO
。