TCP三次握手和四次斷連深入分析:連接狀態與關系
說到tcp協議,凡是稍微看過的人都能順口說出三次握手和四次斷連,再牛逼的一點的就能夠把每個狀態(SYNC_SENT、CLOSE_WAIT。。。。。。等)都能背出來,而說道socket編程,基本上寫過網絡編程的人都會熟悉那幾個標準的API:socket、connect、listen、accept。。。。。。等。但是,我敢打賭很少有人明白tcp狀態和socket編程API之間的關系。不信? 看看如下幾個問題你是否知道吧:
1)什么時候客戶端才能夠連接上server端, 是server端調用bind后還是listen后還是accept后 ?
2)什么情況下會出現FIN_WAIT_2狀態
本周打破砂鍋問到底的精神以及實事求是的精神,我用python腳本寫了一些腳本測試這些情況,看完后你一定會大呼過癮,對tcp的協議和socket編程的理解又更上層樓。
注:以下測試是在Linux(2.6.18)平臺上用python(2.7)腳本測試,其它平臺沒有測試,有興趣可以自己測試一下。
連接過程測試和驗證
第1種情況:client調用connect,server只是調用了socket + bind,沒有調用listen
tcpdump抓包如下:
抓包結果顯示:server端直接回復了RST包
此時使用netstat或者ss命令去查看,無論是client還是server,都查不到連接
第2種情況:client調用connect,server調用了socket + bind + listen
tcpdump抓包如下:
抓包結果顯示:三次握手完成
使用ss工具去查看,client和server都顯示ESTAB
- [liyh@localhost ~]$ ss -t -n | grep 50000
- ESTAB 0 0 10.1.73.45:55354 10.1.73.76:50000
第3種情況:client調用connect + send,server調用了socket + bind + listen + accept + recv + send
tcpdump抓包如下:
抓包結果顯示:三次握手完成,并且雙方都可以發送和接受數據了
使用ss工具去查看,client和server都顯示ESTAB
- [liyh@localhost ~]$ ss -t -n | grep 50000
- ESTAB 0 0 10.1.73.45:41363 10.1.73.76:50000
連接過程總結
1)可以看到,只有當server端listen之后,client端調用connect才能成功,否則就會返回RST響應拒絕連接
2)只有當accept后,client和server才能調用recv和send等io操作
3)socket API調用錯誤不會導致client出現SYN_SENT狀態,那么只能是網絡設備丟包(路由器、防火墻)才會導致SYNC_SENT狀態
斷連過程測試和總結
第1種情況:client調用close,但server沒有調用close
tcpdump抓包如下:
抓包結果顯示:client發送了fin包,server端應答了ack包#p#
使用ss工具去查看,client顯示FIN_WAIT_2狀態:
- [liyh@localhost ~]$ ss -t -n | grep "10.1.73.76:50000"
- FIN-WAIT-2 0 0 10.1.73.45:47630 10.1.73.76:50000
server顯示CLOSE-WAIT狀態
- [jws@jae_test ~]$ ss -t -n | grep 50000
- CLOSE-WAIT 1 0 10.1.73.76:50000 10.1.73.45:47630
而且有一個值得注意的現象是:client的連接過一段時間就沒有了,而server的連接一直處于CLOSE_WAIT狀態
原因在于Linux系統內核中有一個參數可以控制FIN_WAIT_2的時間:tcp_fin_timeout
第2種情況:client調用close,server調用close
tcpdump抓包如下:
抓包結果顯示:熟悉的四次斷連來啦
使用ss工具去查看,client顯示TIME-WAIT狀態
- [liyh@localhost ipv4]$ ss -a -n | grep 50000
- TIME-WAIT 0 0 10.1.73.45:39751 10.1.73.76:50000
server端使用ss工具去看已經看不到連接了
第3種情況:server端被kill了
tcpdump抓包如下:
抓包結果顯示:熟悉的四次斷連來啦
咦,怎么會和正常的server close一樣呢?答案就在于操作系統的實現:
close函數其實本身不會導致tcp協議棧立刻發送fin包,而只是將socket文件的引用計數減1,當socket文件的引用計數變為0的時候,操作系統會自動關閉tcp連接,此時才會發送fin包。
這也是多進程編程需要特別注意的一點,父進程中一定要將socket文件描述符close,否則運行一段時間后就可能會出現操作系統提示too many open files
第4種情況:client端調用shutdown操作
shutdown操作有三種關閉方式:SHUT_RD、SHUT_WR、SHUT_RDWR,分別測試后發現有趣的現象。
1)如果是SHUT_RD,則tcpdump抓包發現沒有發送任何包;
2)如果是SHUT_RD或者SHUT_RDWR,則client會發送FIN包給server,
若server收到后執行close操作,則server發送FIN給client,最終連接被關閉。
SHUT_WR或者SHUT_RDWR抓包顯示如下:
使用ss命令查看,client顯示如下:
- [liyh@localhost ipv4]$ ss -a -n | grep 50000
- TIME-WAIT 0 0 10.1.73.45:39751 10.1.73.76:50000
server顯示連接已經被關閉了。
關于shutdown的詳細解釋可以參考:http://www.gnu.org/software/libc/manual/html_node/Closing-a-Socket.html
歸納一下SHUT_RD的處理:
1)client端不再接收數據,如果有新的數據到來,直接丟棄(reject)
2)沒有發送任何tcp包,所以server端并不知道這個狀態,server端可以繼續發送數據,但由于1)的原因,發了也白發
歸納一下SHUT_WR或者SHUT_RDWR的處理:
1)停止傳送數據(不能再調用write操作),丟棄緩沖區中未發送的數據(已調用write但底層tcp協議棧還沒發送的)
2)停止等待已發送數據的確認消息,已發送未確認的數據不再重發
斷連過程總結
1)close只是減少socket文件的引用計數,當計數減為0后,操作系統執行tcp的斷連操作
2)client端close后server端不close,會導致client端連接狀態為FIN_WAIT_2,server端連接狀態為CLOSE_WAIT
正常編程肯定不會這樣處理,一般都是在異常處理跳轉(C++/JAVA等)導致沒有close,或者整個系統異常導致沒有close(例 如JVM內存出現out of memory錯誤)
3)shutdown的處理邏輯比較復雜,非特殊情況不要亂用,很容易出問題
4)進程退出后操作系統會自動回收socket,發起tcp關閉流程操作