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