socket伺服器開發中的SO_REUSEADDR選項與讓人心煩的TIME_WAIT
我在開發一個socket伺服器程式並反覆除錯的時候,發現了一個讓人無比心煩的情況:每次kill掉該伺服器程序並重新啟動的時候,都會出現bind錯誤:error:98,Address already in use。然而再kill掉該程序,再次重新啟動的時候,就bind成功了。真讓人摸不著頭腦。難道一定要嘗試兩次才顯得真誠?這不科學!
我的第一反應是kill程序的時候,並沒有完全釋放掉socket資源,倒致第二次啟動的時候,bind失敗。那麼第三次怎麼又成功了呢?
查資料:有人說是TIME_WAIT在搗鬼。
回想一下,Linux下的TIME_WAIT大概是2分鐘,這樣也合情合理。那麼沒有釋放掉的資源是什麼呢,是埠嗎?機智的我立刻決定做實驗找出答案。啟動伺服器程式,在與客戶建立連線之後,kill掉伺服器。飛快地在terminal裡輸入命令:netstat -an|grep 9877。這裡9877是我伺服器打算繫結的埠。果然:
結果顯示9877埠正在被使用,並處於TCP中的TIME_WAIT狀態。再過兩分鐘,我再執行命令netstat -an|grep 9877,世界清靜了,什麼都沒有。
終於找到了答案:果然是TIME_WAIT在搗鬼。
2 解決問題
問題找到了,可是怎麼解決問題呢。如何才能結束掉這個TIME_WAIT狀態呢?否則每次除錯之後,都要巴巴地等上兩分鐘,再進行下次除錯。這太蠢了!想了好久,也沒想出解決辦法。那TCP中有沒有能關閉掉TIME_WAIT的選項呢?翻書!UNP中第7章就是講socket選項的。還真沒有找到。但是,我找到了SO_REUSEADDR選項。關於此選項,書上說可以起到以下4個不同的功用:
(1)SO_REUSEADDR允許啟動一個監聽伺服器並捆綁其眾所周知的埠,即使以前建立的將該埠用作他們的本地埠的連線仍存在。
(2)允許在同一埠上啟動同一伺服器的多個例項,只要每個例項捆綁一個不同的本地IP地址即可。
(3)SO_REUSEADDR 允許單個程序捆綁同一埠到多個套接字上,只要每次捆綁指定不同的本地IP地址即可。
(4)SO_REUSEADDR允許完全重複的捆綁:當一個IP地址和埠號已繫結到某個套接字上時,如果傳輸協議支援,同樣的IP地址和埠還可以捆綁到另一個套接字上。一般來說本特性僅支援UDP套接字。
我遇到的情況正好符合情況1,並且書上說了:“所有TCP伺服器都應該指定本套接字選項,以允許伺服器在這種情形下被重新啟動。”那麼試試看嘍。
上面兩行程式碼,把此套接字listenFd設定為允許地址重用(on=1,如果on=0就是不允許重用了)。這樣每次bind的時候,如果此埠正在使用的話,bind就會把埠“搶”過來。就不會報錯了。完美解決問題。
3 察漏補缺
既然TIME_WAIT這麼討厭,那它的存在有什麼意義呢?畢竟伺服器端已經中斷掉連線了呀。記得之前在看UNP的時候,上面好像有提到過,繼續翻書:
書上說,TIME_WAIT狀態有兩個存在的理由:
(1)可靠地實現TCP全雙工連線的終止;
(2)允許老的重複分節在網路中消逝。
原來如此,解釋一下,上個圖:
(1)如果伺服器最後傳送的ACK因為某種原因丟失了,那麼客戶一定會重新發送FIN,這樣因為有TIME_WAIT的存在,伺服器會重新發送ACK給客戶,如果沒有TIME_WAIT,那麼無論客戶有沒有收到ACK,伺服器都已經關掉連線了,此時客戶重新發送FIN,伺服器將不會發送ACK,而是RST,從而使客戶端報錯。也就是說,TIME_WAIT有助於可靠地實現TCP全雙工連線的終止。
(2)如果沒有TIME_WAIT,我們可以在最後一個ACK還未到達客戶的時候,就建立一個新的連線。那麼此時,如果客戶收到了這個ACK的話,就亂套了,必須保證這個ACK完全死掉之後,才能建立新的連線。也就是說,TIME_WAIT允許老的重複分節在網路中消逝。
回到我們的問題,由於我並不是正常地經過四次斷開的方式中斷連線,所以並不會存在最後一個ACK的問題。所以,這樣是安全的。不過,最終的伺服器版本,還是不要設定為埠可複用的。切記。
加油加油。好好學習,天天向上。
另外:UNP是本好書,要好好看呀。