网络基础知识-面试
常用的三個(gè)狀態(tài)是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主動(dòng)關(guān)閉,CLOSE_WAIT 表示被動(dòng)關(guān)閉。
主動(dòng)關(guān)閉的一方在發(fā)送最后一個(gè) ack 后,就會(huì)進(jìn)入 TIME_WAIT 狀態(tài) 停留2MSL(max segment lifetime)時(shí)間,這個(gè)是TCP/IP必不可少的,也就是“解決”不了的。
為什么要TIME_WAIT等待呢?
1. 如果 【最后一步】A 響應(yīng)ACK 包丟失,B 會(huì)以為 A 沒有收到自己的關(guān)閉請(qǐng)求,然后B會(huì)重試向 A 再發(fā) FIN 包。如果沒有 TIME_WAIT 狀態(tài),A 不再保存這個(gè)連接的信息,收到一個(gè)不存在的連接的包,A 會(huì)響應(yīng) RST 包,導(dǎo)致 B 端異常響應(yīng)。
此時(shí), TIME_WAIT 是為了保證 TCP 連接正常終止。
2. TCP 下的 IP 層協(xié)議是無法保證包傳輸?shù)南群箜樞虻摹H绻p方揮手之后,一個(gè)網(wǎng)絡(luò)四元組(src/dst ip/port)被回收,而此時(shí)網(wǎng)絡(luò)中還有一個(gè)【遲到的數(shù)據(jù)包】沒有被 B 接收,A 應(yīng)用程序又立刻使用了同樣的四元組再創(chuàng)建了一個(gè)新的連接后,這個(gè)遲到的數(shù)據(jù)包才到達(dá) B,那么這個(gè)數(shù)據(jù)包就會(huì)讓 B 以為是 A 剛發(fā)過來的。
此時(shí), TIME_WAIT 是為了保證迷失的數(shù)據(jù)包正常過期。
對(duì)于基于TCP的HTTP協(xié)議,關(guān)閉TCP連接的是Server端,這樣,Server端會(huì)進(jìn)入TIME_WAIT狀態(tài)。
TIME_WAIT場景
1.客戶端連接服務(wù)器的80服務(wù),這時(shí)客戶端會(huì)啟用一個(gè)本地的端口訪問服務(wù)器的80,訪問完成后關(guān)閉此連接,立刻再次訪問服務(wù)器的80,
? 這時(shí)客戶端會(huì)啟用另一個(gè)本地的端口,而不是剛才使用的那個(gè)本地端口。原因就是剛才的那個(gè)連接【四元組決定】還處于TIME_WAIT狀態(tài)。
2.客戶端連接服務(wù)器的80服務(wù),這時(shí)服務(wù)器關(guān)閉80端口,立即再次重啟80端口的服務(wù),這時(shí)可能不會(huì)成功啟動(dòng),原因也是服務(wù)器的連接
? 還處于TIME_WAIT狀態(tài)。(如:nginx服務(wù)器)
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Q:問一下TIME_WAIT有什么問題,是閑置而且內(nèi)存不回收嗎?
A:是的,這樣的現(xiàn)象實(shí)際是正常的,有時(shí)和訪問量大有關(guān),設(shè)置 /etc/sysctl.conf 這兩個(gè)參數(shù):
net.ipv4.tcp_tw_reuse? ?是表示是否允許處于TIME-WAIT狀態(tài)的socket重新應(yīng)用于新的TCP連接;
net.ipv4.tcp_tw_recycle 是加速TIME-WAIT sockets回收
Q: 我正在寫一個(gè)unix server程序,不是daemon,經(jīng)常需要在命令行上重啟它,絕大多數(shù)時(shí)候工作正常,但是某些時(shí)候會(huì)報(bào)告"bind: address in use",于是重啟失敗。
A: server程序總是應(yīng)該在調(diào)用 bind()之前 設(shè)置SO_REUSEADDR套接字選項(xiàng)。至于TIME_WAIT狀態(tài),你無法避免,那是TCP協(xié)議的一部分。
Q: 如何避免等待60秒之后才能重啟服務(wù)
A: 使用setsockopt,比如 ?
int option = 1;
if ( setsockopt ( masterSocket, SOL_SOCKET, SO_REUSEADDR, &option,sizeof( option ) ) < 0 )
{
die( "setsockopt" );
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TIME_WAIT在web server中的場景
再引用網(wǎng)絡(luò)資源的一段話:
? 1.值得一說的是,對(duì)于基于TCP的HTTP協(xié)議,關(guān)閉TCP連接的是Server端,這樣,Server端會(huì)進(jìn)入TIME_WAIT狀態(tài),可想而知,對(duì)于訪問量大的Web Server,會(huì)存在大量的TIME_WAIT狀態(tài),
? 假如server一秒鐘接收1000個(gè)請(qǐng)求,那么就會(huì)積壓 240*1000=240,000個(gè) TIME_WAIT的記錄,維護(hù)這些狀態(tài)給Server帶來負(fù)擔(dān)。當(dāng)然現(xiàn)代操作系統(tǒng)都會(huì)用快速的查找算法來管理這些
? TIME_WAIT,所以對(duì)于新的TCP連接請(qǐng)求,判斷是否hit中一個(gè)TIME_WAIT不會(huì)太費(fèi)時(shí)間,但是有這么多狀態(tài)要維護(hù)總是不好。 ?
? 2.HTTP協(xié)議1.1版規(guī)定default行為是Keep-Alive,也就是會(huì)重用TCP連接傳輸多個(gè) request/response,一個(gè)主要原因就是發(fā)現(xiàn)了這個(gè)問題。 ?
也就是說HTTP的交互跟上面畫的那個(gè)圖是不一樣的,關(guān)閉連接的不是客戶端,而是服務(wù)器,所以web服務(wù)器也是會(huì)出現(xiàn)大量的TIME_WAIT的情況的。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
方法一
解決思路很簡單,就是讓服務(wù)器能夠快速回收和重用那些TIME_WAIT的資源。
/etc/sysctl.conf文件的修改:
#表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認(rèn)為0,表示關(guān)閉 ?
net.ipv4.tcp_tw_reuse = 1 ?
#表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0,表示關(guān)閉 ?
net.ipv4.tcp_tw_recycle = 1 ?
修改完之后執(zhí)行/sbin/sysctl -p讓參數(shù)生效。
方法二
在高并發(fā)短連接的server端,當(dāng)server處理完client的請(qǐng)求后立刻closesocket此時(shí)會(huì)出現(xiàn)大量time_wait狀態(tài),導(dǎo)致連接不上,
用linger強(qiáng)制關(guān)閉可以解決此問題,但是linger可能會(huì)導(dǎo)致數(shù)據(jù)丟失,linger值為0時(shí)是強(qiáng)制關(guān)閉,無論并發(fā)多少都能正常連接上,如果非0會(huì)發(fā)生部分連接不上的情況!
(可調(diào)用setsockopt設(shè)置套接字的linger延時(shí)標(biāo)志,同時(shí)將延時(shí)時(shí)間設(shè)置為0。)
這里有一個(gè)基本原則:
設(shè)置SO_LINGER選項(xiàng)后,close的成功返回只是告訴我們先前發(fā)送的數(shù)據(jù)和FIN已由對(duì)端TCP確認(rèn),而不能告訴我們對(duì)端應(yīng)用程序是否已成功接收數(shù)據(jù)。
但是如果我們不設(shè)置這個(gè)選項(xiàng),我們連TCP是否確認(rèn)了數(shù)據(jù)都不知道。
int option=0;
if ( setsockopt ( mastersocket, SOL_SOCKET, SO_LINGER, &option,sizeof(option)) < 0)
{
??? die("setsockopt");
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
必看:待處理錯(cuò)誤? 保活探測分節(jié)、http://blog.csdn.net/u012062760/article/details/45173351
?默認(rèn)情況下,服務(wù)器通過socket、bind和listen重新啟動(dòng)時(shí),由于它試圖捆綁一個(gè)現(xiàn)有連接(即正由早先派生的那個(gè)子進(jìn)程處理著連接)的端口,bind會(huì)失敗。但是如果在socket和bind之間調(diào)用SO_REUSEADDR選項(xiàng),那么bind會(huì)成功
?以下為使用SO_REUSEADDR建議:
?????????? 1、在所有TCP服務(wù)器中,在調(diào)用bind之前設(shè)置SO_REUSEADDR選項(xiàng)
?????????? 2、當(dāng)編寫一個(gè)可在同一時(shí)刻在同一主機(jī)上運(yùn)行多次的多播應(yīng)用程序時(shí),設(shè)置SO_REUSEADDR選項(xiàng),并將所參加多播組的地址作為本地IP地址捆綁
以下我們對(duì)幾種close返回做一個(gè)總結(jié):
1、 close立即返回,根本不等待
2、 close拖延到接收到對(duì)端對(duì)FIN的ACK才返回
3、 后跟一個(gè)read調(diào)用的shutdown一直等到接收對(duì)端的FIN才返回
另一種可以讓客戶知道服務(wù)器應(yīng)用已經(jīng)接受數(shù)據(jù)的方法是應(yīng)用級(jí)ACK。
客戶端在發(fā)送完數(shù)據(jù)后調(diào)用read讀取一個(gè)字節(jié)的數(shù)據(jù):
char ack; ?
write();??? ??? ???? //客戶端寫數(shù)據(jù)給服務(wù)器 ?
read();???????????? //準(zhǔn)備接收一個(gè)結(jié)束數(shù)據(jù)
而服務(wù)器在接受完數(shù)據(jù)后發(fā)送一個(gè)字節(jié)的應(yīng)用級(jí)ACK:
nbytes = read();??? //接收完客戶端的數(shù)據(jù) ?
write();??????????? //向客戶端寫一個(gè)應(yīng)用級(jí)ACK ?
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
recv和send的阻塞和非阻塞
這里只描述同步Socket的send函數(shù)的執(zhí)行流程。當(dāng)調(diào)用該函數(shù)時(shí):
(1)send先比較待發(fā)送數(shù)據(jù)的長度len和套接字socket發(fā)送緩沖區(qū)的長度, 如果len大于socket的發(fā)送緩沖區(qū)的長度,該函數(shù)返回SOCKET_ERROR;
(2)如果len小于或者等于socket的發(fā)送緩沖區(qū)的長度,那么send先檢查協(xié)議是否正在發(fā)送socket的發(fā)送緩沖區(qū)中的數(shù)據(jù)【鎖住發(fā)送緩沖區(qū)】,如果是就等待協(xié)議把數(shù)據(jù)發(fā)送完,
如果協(xié)議 還沒有開始發(fā)送socket的發(fā)送緩沖中的數(shù)據(jù)或者s的發(fā)送緩沖中沒有數(shù)據(jù),那么send就比較socket發(fā)送緩沖區(qū)的剩余空間和len
(3)如果len大于剩余空間大小,send就一直等待協(xié)議把socket的發(fā)送緩沖中的數(shù)據(jù)發(fā)送完
(4)如果len小于剩余 空間大小,send就僅僅把buf中的數(shù)據(jù)copy到剩余空間里(注意并不是send把socket的發(fā)送緩沖中的數(shù)據(jù)傳到連接的另一端的,
而是協(xié)議傳的,send僅僅是把buf中的數(shù)據(jù)copy到socket的發(fā)送緩沖區(qū)的剩余空間里)。
這里只描述同步Socket的recv函數(shù)的執(zhí)行流程。當(dāng)應(yīng)用程序調(diào)用recv函數(shù)時(shí):
(1)recv先等待socket發(fā)送緩沖中的數(shù)據(jù)被協(xié)議傳送完畢,如果協(xié)議在傳送socket發(fā)送緩沖中的數(shù)據(jù)時(shí)出現(xiàn)網(wǎng)絡(luò)錯(cuò)誤,那么recv函數(shù)返回SOCKET_ERROR,
(2)如果socket發(fā)送緩沖中沒有數(shù)據(jù)或者數(shù)據(jù)被協(xié)議成功發(fā)送完畢后,recv先檢查套接字socket接收緩沖區(qū),如果socket接收緩沖區(qū)中沒有數(shù)據(jù)或者協(xié)議正在接收數(shù)據(jù),
那么recv就一直等待,直到協(xié)議把數(shù)據(jù)接收完畢。當(dāng)協(xié)議把數(shù)據(jù)接收完畢,recv函數(shù)就把socket接收緩沖中的數(shù)據(jù)copy到buf中(注意協(xié)議接收到的數(shù)據(jù)可能大于buf的長度,
所以 在這種情況下要調(diào)用幾次recv函數(shù)才能把socket接收緩沖中的數(shù)據(jù)copy完。recv函數(shù)僅僅是copy數(shù)據(jù),真正的接收數(shù)據(jù)是協(xié)議來完成的),
recv函數(shù)返回其實(shí)際copy的字節(jié)數(shù)。如果recv在copy時(shí)出錯(cuò),那么它返回SOCKET_ERROR;如果recv函數(shù)在等待協(xié)議接收數(shù)據(jù)時(shí)網(wǎng)絡(luò)中斷了,那么它返回0。
注意:在Unix系統(tǒng)下,如果recv函數(shù)在等待協(xié)議接收數(shù)據(jù)時(shí)網(wǎng)絡(luò)斷開了,那么調(diào)用recv的進(jìn)程會(huì)接收到一個(gè)SIGPIPE信號(hào),進(jìn)程對(duì)該信號(hào)的默認(rèn)處理是進(jìn)程終止。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select的幾大缺點(diǎn):
(1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在fd很多時(shí)會(huì)很大
(2)同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個(gè)開銷在fd很多時(shí)也很大
(3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
poll的實(shí)現(xiàn)和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),其他的都差不多。
? ? ? ?對(duì)于第一個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中。每次注冊(cè)新的事件到epoll句柄中時(shí)(在epoll_ctl中指定EPOLL_CTL_ADD),
會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝。epoll保證了每個(gè)fd在整個(gè)過程中只會(huì)拷貝一次。
對(duì)于第二個(gè)缺點(diǎn),epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對(duì)應(yīng)的設(shè)備等待隊(duì)列中,而只在epoll_ctl時(shí)把current掛一遍(這一遍必不可少)
并為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào)函數(shù),而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)。
epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實(shí)現(xiàn)睡一會(huì),判斷一會(huì)的效果,和select實(shí)現(xiàn)中的第7步是類似的)。
對(duì)于第三個(gè)缺點(diǎn),epoll沒有這個(gè)限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,
具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
select和epoll 原理概述&優(yōu)缺點(diǎn)比較
就像收本子的班長,以前得一個(gè)個(gè)學(xué)生地去問有沒有本子,如果沒有,它還得等待一段時(shí)間而后又繼續(xù)問,現(xiàn)在好了,只走一次,如果沒有本子,班長就告訴大家去那里交本子,當(dāng)班長想起要取本子,就去那里看看或者等待一定時(shí)間后離開,有本子到了就叫醒他,然后取走。
也許在細(xì)節(jié)方面不是特別恰當(dāng),但是總的來說,比較形象地說出了select和epoll的區(qū)別。
下面我將簡單明了地概述下兩者的原理,并概況兩者的優(yōu)缺點(diǎn)。
select原理概述
調(diào)用select時(shí),會(huì)發(fā)生以下事情:
從用戶空間拷貝fd_set到內(nèi)核空間;
注冊(cè)回調(diào)函數(shù)__pollwait;
遍歷所有fd,對(duì)全部指定設(shè)備做一次poll(這里的poll是一個(gè)文件操作,它有兩個(gè)參數(shù),一個(gè)是文件fd本身,一個(gè)是當(dāng)設(shè)備尚未就緒時(shí)調(diào)用的回調(diào)函數(shù)__pollwait,這個(gè)函數(shù)把設(shè)備自己特有的等待隊(duì)列傳給內(nèi)核,讓內(nèi)核把當(dāng)前的進(jìn)程掛載到其中);
當(dāng)設(shè)備就緒時(shí),設(shè)備就會(huì)喚醒在自己特有等待隊(duì)列中的【所有】節(jié)點(diǎn),于是當(dāng)前進(jìn)程就獲取到了完成的信號(hào)。poll文件操作返回的是一組標(biāo)準(zhǔn)的掩碼,其中的各個(gè)位指示當(dāng)前的不同的就緒狀態(tài)(全0為沒有任何事件觸發(fā)),根據(jù)mask可對(duì)fd_set賦值;
如果所有設(shè)備返回的掩碼都沒有顯示任何的事件觸發(fā),就去掉回調(diào)函數(shù)的函數(shù)指針,進(jìn)入有限時(shí)的睡眠狀態(tài),再恢復(fù)和不斷做poll,再作有限時(shí)的睡眠,直到其中一個(gè)設(shè)備有事件觸發(fā)為止。
只要有事件觸發(fā),系統(tǒng)調(diào)用返回,將fd_set從內(nèi)核空間拷貝到用戶空間,回到用戶態(tài),用戶就可以對(duì)相關(guān)的fd作進(jìn)一步的讀或者寫操作了。
epoll原理概述
調(diào)用epoll_create時(shí),做了以下事情:
內(nèi)核幫我們?cè)趀poll文件系統(tǒng)里建了個(gè)file結(jié)點(diǎn);
在內(nèi)核cache里建了個(gè)紅黑樹用于存儲(chǔ)以后epoll_ctl傳來的socket;
建立一個(gè)list鏈表,用于存儲(chǔ)準(zhǔn)備就緒的事件。
調(diào)用epoll_ctl時(shí),做了以下事情:
把socket放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹上;
給內(nèi)核中斷處理程序注冊(cè)一個(gè)回調(diào)函數(shù),告訴內(nèi)核,如果這個(gè)句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。
調(diào)用epoll_wait時(shí),做了以下事情:
觀察list鏈表里有沒有數(shù)據(jù)。有數(shù)據(jù)就返回,沒有數(shù)據(jù)就sleep,等到timeout時(shí)間到后即使鏈表沒數(shù)據(jù)也返回。而且,通常情況下即使我們要監(jiān)控百萬計(jì)的句柄,大多一次也只返回很少量的準(zhǔn)備就緒句柄而已,所以,epoll_wait僅需要從內(nèi)核態(tài)copy少量的句柄到用戶態(tài)而已。
總結(jié)如下:
一顆紅黑樹,一張準(zhǔn)備就緒句柄鏈表,少量的內(nèi)核cache,解決了大并發(fā)下的socket處理問題。
執(zhí)行epoll_create時(shí),創(chuàng)建了紅黑樹和就緒鏈表;?
執(zhí)行epoll_ctl時(shí),如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內(nèi)核注冊(cè)回調(diào)函數(shù),用于當(dāng)中斷事件來臨時(shí)向準(zhǔn)備就緒鏈表中插入數(shù)據(jù);?
執(zhí)行epoll_wait時(shí)立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。
兩種模式的區(qū)別:
LT模式下,只要一個(gè)句柄上的事件一次沒有處理完,會(huì)在以后調(diào)用epoll_wait時(shí)重復(fù)返回這個(gè)句柄,而ET模式僅在第一次返回。
兩種模式的實(shí)現(xiàn):
當(dāng)一個(gè)socket句柄上有事件時(shí),內(nèi)核會(huì)把該句柄插入上面所說的準(zhǔn)備就緒list鏈表,這時(shí)我們調(diào)用epoll_wait,會(huì)把準(zhǔn)備就緒的socket拷貝到用戶態(tài)內(nèi)存,然后清空準(zhǔn)備就緒list鏈表,最后,epoll_wait檢查這些socket,如果是LT模式,并且這些socket上確實(shí)有未處理的事件時(shí),又把該句柄放回到剛剛清空的準(zhǔn)備就緒鏈表。所以,LT模式的句柄,只要它上面還有事件,epoll_wait每次都會(huì)返回。
對(duì)比
select缺點(diǎn):
最大并發(fā)數(shù)限制:使用32個(gè)整數(shù)的32位,即32*32=1024來標(biāo)識(shí)fd,雖然可修改,但是有以下第二點(diǎn)的瓶頸;
效率低:每次都會(huì)線性掃描整個(gè)fd_set,集合越大速度越慢;
內(nèi)核/用戶空間內(nèi)存拷貝問題。
epoll的提升:
本身沒有最大并發(fā)連接的限制,僅受系統(tǒng)中進(jìn)程能打開的最大文件數(shù)目限制;
效率提升:只有活躍的socket才會(huì)主動(dòng)的去調(diào)用callback函數(shù);
省去不必要的內(nèi)存拷貝:epoll通過內(nèi)核與用戶空間mmap同一塊內(nèi)存實(shí)現(xiàn)。
當(dāng)然,以上的優(yōu)缺點(diǎn)僅僅是特定場景下的情況:高并發(fā),且任一時(shí)間只有少數(shù)socket是活躍的。
如果在并發(fā)量低,socket都比較活躍的情況下,select就不見得比epoll慢了(就像我們常常說快排比插入排序快,但是在特定情況下這并不成立)。
?
總結(jié)
- 上一篇: mysql一个用户SQL慢查询分析,原因
- 下一篇: go语言总结