本文有更新,請移步個人我的博客:https://blog.andyqiao.top/article/15/安全
我在開發一個socket服務器程序並反覆調試的時候,發現了一個讓人無比心煩的狀況:每次kill掉該服務器進程並從新啓動的時候,都會出現bind錯誤:error:98,Address already in use。然而再kill掉該進程,再次從新啓動的時候,就bind成功了。真讓人摸不着頭腦。難道必定要嘗試兩次才顯得真誠?這不科學!服務器
個人第一反應是kill進程的時候,並無徹底釋放掉socket資源,倒致第二次啓動的時候,bind失敗。那麼第三次怎麼又成功了呢?網絡
查資料:有人說是TIME_WAIT在搗鬼。socket
回想一下,Linux下的TIME_WAIT大概是2分鐘,這樣也合情合理。那麼沒有釋放掉的資源是什麼呢,是端口嗎?機智的我馬上決定作實驗找出答案。啓動服務器程序,在與客戶創建鏈接以後,kill掉服務器。飛快地在terminal裏輸入命令:netstat -an|grep 9877。這裏9877是我服務器打算綁定的端口。果真:學習
結果顯示9877端口正在被使用,並處於TCP中的TIME_WAIT狀態。再過兩分鐘,我再執行命令netstat -an|grep 9877,世界清靜了,什麼都沒有。spa
終於找到了答案:果真是TIME_WAIT在搗鬼。調試
問題找到了,但是怎麼解決問題呢。如何才能結束掉這個TIME_WAIT狀態呢?不然每次調試以後,都要巴巴地等上兩分鐘,再進行下次調試。這太蠢了!想了很久,也沒想出解決辦法。那TCP中有沒有能關閉掉TIME_WAIT的選項呢?翻書!UNP中第7章就是講socket選項的。還真沒有找到。可是,我找到了SO_REUSEADDR選項。關於此選項,書上說能夠起到如下4個不一樣的功用:blog
(1)SO_REUSEADDR容許啓動一個監聽服務器並捆綁其衆所周知的端口,即便之前創建的將該端口用做他們的本地端口的鏈接仍存在。進程
(2)容許在同一端口上啓動同一服務器的多個實例,只要每一個實例捆綁一個不一樣的本地IP地址便可。資源
(3)SO_REUSEADDR 容許單個進程捆綁同一端口到多個套接字上,只要每次捆綁指定不一樣的本地IP地址便可。
(4)SO_REUSEADDR容許徹底重複的捆綁:當一個IP地址和端口號已綁定到某個套接字上時,若是傳輸協議支持,一樣的IP地址和端口還能夠捆綁到另外一個套接字上。通常來講本特性僅支持UDP套接字。
我遇到的狀況正好符合狀況1,而且書上說了:「全部TCP服務器都應該指定本套接字選項,以容許服務器在這種情形下被從新啓動。」那麼試試看嘍。
上面兩行代碼,把此套接字listenFd設置爲容許地址重用(on=1,若是on=0就是不容許重用了)。這樣每次bind的時候,若是此端口正在使用的話,bind就會把端口「搶」過來。就不會報錯了。完美解決問題。
既然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是本好書,要好好看呀。