少點代碼,多點頭髮
本文已經收錄至我的GitHub,歡迎大家踊躍star 和 issues。
https://github.com/midou-tech/articles
三次握手建立鏈接,四次揮手斷開鏈接。這個問題算非常經典的問題,也是面試官非常喜歡問的問題。
不誇張的說,龍叔在校招面試的時候每一家公司都問到過關於三次握手和四次揮手相關的問題,相信大家也都差不多被面試官各種懟。
這個問題的重要性,已經意識到。不說廢話了,接下來就是聽龍叔給你安排的明明白白。
先畫個圖,看下TCP的建立連接 和 斷開連接的整體過程。
看完這個圖相信聰明的你在整體對三次握手和四次揮手有了一些基本把控。但是,裏面的細節肯定是會有些生疏或者模糊的,接下來就一個一個問題的揭露本質。
在解釋之前先看點基礎知識做做鋪墊。
TCP狀態轉移解釋
狀態 | 描述 |
---|---|
CLOSED | 阻塞或關閉狀態,表示主機當前沒有正在傳輸或者建立的鏈接 |
LISTEN | 監聽狀態,表示服務器做好準備,等待建立傳輸鏈接 |
SYN RECV | 收到第一次的傳輸請求,還未進行確認 |
SYN SENT | 發送完第一個SYN報文,等待收到確認 |
ESTABLISHED | 鏈接正常建立之後進入數據傳輸階段 |
FIN WAIT1 | 主動發送第一個FIN報文之後進入該狀態 |
FIN WAIT2 | 已經收到第一個FIN的確認信號,等待對方發送關閉請求 |
TIMED WAIT | 完成雙向鏈接關閉,等待分組消失 |
CLOSING | 雙方同時關閉請求,等待對方確認時 |
CLOSE WAIT | 收到對方的關閉請求並進行確認進入該狀態 |
LAST ACK | 等待最後一次確認關閉的報文 |
再看下TCP的報文格式
首部有20字節的固定長度,含義如下:
源端口和目的端口
各佔2字節,就是存儲源端口號和目的端口的
序號seq
佔4字節,表示的範圍就是整形的範圍[0~2^32]。序號使用在給數據部分每個字節進行編號的,編號方式是mod 2^32 。
確認號ack
佔4字節,範圍也是無符號整數的範圍。使用在對端傳輸給我的數據最後一個字節序號,例如A傳輸給B 101—500,此時B返回的確認號一定是小於等於501的。當B段正確接收數據之後纔會返回確認號,換句話說確認號之前的數據已經全部接收。
數據偏移
佔4bit,數據偏移很多人很容易想到是不是表示數據的長度,那就錯了。偏移嘛,指的是TCP起始位置到數據部分的起始位置的偏移,也就是TCP首部的長度。
保留
佔6bit,保留字段顧名思義,就是爲今後使用,默認置爲0。
緊急URG控制位
佔用1bit,URG=1,表示緊急指針有效,此時tcp數據優先傳輸。相當於生活中的緊急通道,特殊情況時使用。
在網絡中也會有特殊情況,例如,發送一個很長的程序在遠程服務器上運行,此時發現程序有bug,需要中斷運行,因此我們從鍵盤輸入Ctrl c,假如不使用緊急數據,需要在緩衝區裏排隊,都知道是bug了,還要排隊,這怕是要出鍋啊。
此時使用緊急數據傳輸,不需要排隊,直接中斷程序是不是更符合我們的預期。
需要注意一點是,即使窗口爲0時,也可以發送緊急數據。
如何使用緊急URG控制位,在socket編程中send函數flag參數
send(int socket, const void *buffer, size_t length, int flags);
flags參數傳MSG_OOB宏時,表示此時有緊急數據。MSG_OOB是個宏,
確認ACK
佔1bit,當ACK=1時生效。TCP有條硬性規定,當建立鏈接成功後所有傳輸的數據報文都必須把ACK置爲1。
推送PSH
佔1bit,發送方把PSH置爲1時 會立即發送該數據包,接收方收到PSH=1的報文會立即處理交付給應用層處理。是不是感覺和URG很像,其實還是有些區別的。
兩者相同點:
URG與PSH兩者都使用於緊急處理的情況,用來快速傳輸緊急數據。
兩者不同點
URG置爲1時,對於發送發,“帶外數據”與正常情況下應該發送的消息數據一起,封裝成數據報發送,省去了在隊列中等待的時間。 在接收方,解析報文後,獲取數據之後還是要放在緩存區中,等待滿了之後在向上往應用層交付。
PSH置爲1時,對於發送方,表明這些數據不需要等向下發送的緩存區滿,立刻封裝成報文,發送,省去了等待發送緩存區到達滿的狀態的時間。 在接收方,也不需要等接受緩存區滿,直接向上交付給應用層。
復位RST
佔1bit,當RST=1時,TCP會主動釋放鏈接,兩種情況會用上。
TCP出現嚴重差錯時,會主動釋放連接,重建鏈接,傳輸數據。
遇到非法報文或者拒絕連接時會把RST置爲1.
同步SYN
佔1bit,同步控制位,用來在傳輸連接建立時同步傳輸連接序號。
SYN=1時,表示這是一個連接請求或連接確認報文。
SYN=1,ACK=0,表明這是一個連接請求數據段,如果對方同意建立連接,則對方會返回一個SYN=1、ACK=1的確認。
FIN控制位
佔1bit,用於釋放一個傳輸連接。
FIN=1時,表示數據已全部傳輸完成,發送端沒有數據要傳輸了,要求釋放當前連接,但是接收端仍然可以繼續接收還沒有接收完的數據。
FIN=0,正常傳輸數據。
窗口大小
佔16bit,2byte,用於表示發送方可以接受的最大數據大小。
該窗口是動態變化的,用作流量控制時使用。
檢驗和
佔16bit,2byte,用於對TCP頭部,僞頭部,數據三個部分進行校驗。
緊急指針
佔16bit,2byte,用於記錄緊急數據的末尾在數據段中的位置。
當URG=1時,該指針才生效。
可選項
可選項最長可達40byte,是可選的,可以沒有。當可選項不存在時,TCP頭部長度爲20byte。
可選項可以包括窗口縮放選項(Window ScaleOption,WSopt)、MSS(最大數據段大小)選項、SACK(選擇性確認)選項、時間戳(Timestamp)選項等。
數據
TCP數據部分,由應用層應用程序提交的數據。
TCP頭部是基礎知識,必須瞭解才能更好的理解TCP數據如何封裝和傳輸,以及在建立鏈接和斷開鏈接時都在操作那些地方。
三次握手建立連接
三次握手如何建立連接?
從圖中可以清楚的看到,三次握手的過程,我在在把過程清楚的解釋一遍,順便說下每個過程容易被問到的知識點。
採用C/S模式解釋,假設C端發起傳輸請求。
在發送建立鏈接請求之前,C端是保持CLOSED狀態,S端最開始也是處於CLOSED狀態,當執行listen函數套接字進入被動監聽狀態。
所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字纔會被“喚醒”來響應請求。
第一次:C端發送SYN=1的請求報文,此時C端進入SYN SENT狀態,等待服務器確認。
此時如果報文丟失發送不到對端會如何?
C端發送報文之後會啓動一個定時器,在超時之後未收到S端的確認,會再次發送SYN請求,每次嘗試的時間會是第一次的二倍,如果總的總嘗試時間爲75秒,此次建立鏈接失敗。
第二次:S端收到C端發送的SYN報文(建立鏈接請求)後,S端必須返回確認號並且同時發送一條SYN報文,此時進入SYN RCVD狀態。
爲啥要連帶發送SYN報文?
TCP是全雙工通信,協議規定當收到建立鏈接請求後必須返回序列號,同時建立本端到對端的通信鏈接。這也叫做捎帶應答機制。
如果第二次報文丟失怎麼辦?
在發送完ACK+SYN報文後會啓動一個定時器,超時沒有收到ACK確認,會再次發送,會進行多次重試。超時時間依舊每次翻倍,重試次數可設置。
修改
/proc/sys/net/ipv4/tcp_synack_retries
的值
第三次:C端收到S端發的ACK+SYN報文,需要返回一個應答ACK的報文,此時該連接會進入半連接狀態的隊列,當S端收到ACK後,一條完整的全雙工TCP鏈接建立完成,雙方進入ESTABLISHED狀態。
這裏有個常用攻擊手段,攻擊者僞造一個SYN請求發送給服務端,服務端響應之後,會收不到C端的ACK確認,服務端會不斷的重試,默認會重試五次。
此時服務端會維持這個鏈接的所有資源,如果有大量這樣的請求,服務端的資源會被耗完。
這就是DOS攻擊。
如果第三次報文丟失怎麼辦?
S端在發出ACK+SYN報文後會啓動一個定時器,在超時觸發還沒收到ACK就確認是丟失了,會重試一次發送。
這裏面的每個狀態都必須搞明白,面試官也超級愛問上面的狀態轉移。
龍叔還遇到過一個面試官問我用過socket編程麼?問我用過哪些socket函數?
C端socket編程代碼
//C端
#define PORT 8080
#define BUFFER_SIZE 1024
int main(int argc, char **argv)
{
//定義IPV4的TCP連接的套接字描述符
int sock_cli = socket(AF_INET,SOCK_STREAM, 0);
//定義sockaddr_in
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
servaddr.sin_port = htons(PORT);
//連接服務器,成功返回0,錯誤返回-1
int ret = connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr));
//客戶端將控制檯輸入的信息發送給服務器端,服務器原樣返回信息,阻塞
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
ret=send(sock_cli, sendbuf, strlen(sendbuf),0); ///發送
recv(sock_cli, recvbuf, sizeof(recvbuf),0); ///接收
fputs(recvbuf, stdout);
}
close(sock_cli); // 關閉連接
return 0;
}
S端socket編程代碼
int main(int argc, char **argv)
{
//定義IPV4的TCP連接的套接字描述符
int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
//定義sockaddr_in
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
server_sockaddr.sin_port = htons(PORT);
//bind成功返回0,出錯返回-1
if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
//listen成功返回0,出錯返回-1,允許同時監聽的連接數爲QUEUE_SIZE
if(listen(server_sockfd,QUEUE_SIZE) == -1)
for(;;)
{
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
//進程阻塞在accept上,成功返回非負描述字,出錯返回-1
int conn = accept(server_sockfd, (struct sockaddr*)&client_addr,&length);
//處理數據部分
...
}
close(server_sockfd);
return 0;
}
爲什麼需要三次握手建立鏈接,2次可以麼,4次行不行?
這問題問的,面試官是咋了?在這明知故問的,整些有的沒的。肯定是不行啊,RFC 標準就是這樣寫的啊。
可不敢這樣回答啊,標準是說的三次握手建立鏈接,可沒說四次不行啊。要是這樣答,妥妥的會收到,同學我們今天的面試到此基本結束了,你回家等消息...
龍叔來說說這個問題,爲什麼不能兩次?
如果第二次不發送SYN+ACK,只是發送確認應答消息ACK,會造成只能建立單向通信,而且不能應答。而TCP是全雙工通信的,而且必須保證可靠性。
如果第二發送SYN+ACK,不用應答。此時會出現三種情況
一、二次握手失敗,C端會重複發送SYN報文,等待對端發送確認報文,S端會保存tcp連接的所有資源,大量的這種情況會導致S資源耗盡。
二、二次握手成功,S收不到ACK會重複發送SYN+ACK報文。
三、二次握手完以後,雙方以爲連接建立成功,即可開始通信。假如此時連接並沒有真的建立成功,S端開始發送消息,會造成網絡擁堵發生。
爲什麼不能是四次?
四次其實原則上來說是可以的,就是把第二次的ACK和SYN分兩次發送。在理論上是完全可以行得通的,但是TCP本着節約網絡網絡資源的前提。
還有一種是不拆開二次握手的捎帶應答,三次握手之後C端繼續發送SYN報文,其時這是徒勞的。第三次完成以後鏈接已經建立,後面無論多少次都是徒勞。
如果雙方同時建立連接,會發生什麼情況?
這就是雙方同時建立鏈接的情況,情況還不錯,反正能建立成功,這點是肯定的。但是要注意兩點
第一、此時只會建立一條全雙工的TCP鏈接,不是兩條。
第二、雙方沒有CS之分,兩端都是同時承擔兩個角色,客戶端和服務器。
四次揮手斷開鏈接
先整個圖看下四次揮手的整個過程和狀態轉移。狀態轉移會考看仔細點。
依舊採用C/S模式解釋此過程。
第一次:當C端的應用程序結束數據傳輸是,會向S端發送一個帶有FIN附加標記的報文段(FIN表示英文finish),此時C端進入FIN_WAIT1狀態,C端不能在發送數據到S端。
第二次:S端收到FIN報文會響應一個ACK報文,S端進入CLOSE_WAIT狀態。進入此狀態後S端把剩餘未發送的數據發送到C端,C端收到S端的ACK之後,進入FIN_WAIT2狀態。
同時繼續接受S端傳輸的其他數據包。
第三次:S端處理完自己待發送的數據之後,也會發送FIN斷開鏈接的請求,S端進入LAST_ACK狀態。
第四次:C端收到S端的斷開鏈接請求後會啓動一個定時器,該定時器時長是2MSL(最大段報文生存時間),同時發送最後一次ACK報文。
爲什麼要四次揮手?
TCP是全雙工的通信機制,每個方向必須單獨進行關閉。
TCP傳輸連接關閉的原則如下:
當一端完成它的數據發送任務後就可以發送一個FIN字段置1的數據段來終止這個方向的數據發送;當另一端收到這個FIN數據段後,必須通知它的應用層 對端已經終止了那個方向的數據傳送。
爲什麼不能用三次握手中捎帶應答機制減少一次握手?
這點到是很迷惑人,但是掌握了TCP傳輸的一些細節就會發現並不難。
TCP是全雙工通信的,S收到斷開鏈接請求後只是表示C端不會傳輸數據到S端了,但是並不表示S端不傳輸數據到C端。
如果採用捎帶應答,S端將無法把剩餘的數據傳輸到C端。
爲何最後一次ACK之後需要等待2MSL的時間?
網絡是不可靠的,TCP是可靠協議,必須保證最後一次報文送達之後才能斷開鏈接,否則會再次收到S端的FIN報文信息。
而等待2MSL時間就是爲了保證最後最後一次報文丟失時還能重新發送。
爲何是2MSL的時間?
2MSL是報文一個往返的最長時間,假設小於這個時間會發生,ACK丟了,但是還沒接收到對方重傳的FIN我方就重新發送了ACK。
如果已經建立了連接,但是客戶端突然出現故障了怎麼辦?
這個不難TCP自己做了保證,TCP默認有個定時器,每次收到客戶端的請求後會把定時器設置好,通常設置兩小時,超過兩小時還沒收到數據。
服務端會發送一個探測報文,以後每隔75秒鐘發送一次。若一連發送10個探測報文仍然沒反應,服務器就認爲客戶端出了故障,接着就關閉連接。
總結
三次握手和四次揮手的知識基本告一段落了,就講到這裏了,如果有什麼不明白的地方可以加我微信探討。
後面還會出一篇網絡編程常用的linux命令行工具,比如ping、tcpdump、netstat、nc等等,在出一篇計算機網絡的總結文章。計算機網絡這部分基本完結了,如果有不懂得可以看看公號裏面前面的文章。