TCP状态转换、半关闭、端口复用
目錄
1、TCP狀態(tài)轉(zhuǎn)換
1.1 三次握手
1.2 四次揮手
1.3 狀態(tài)轉(zhuǎn)換
1.4 相關(guān)命令
2、半關(guān)閉
3、端口復(fù)用
1、TCP狀態(tài)轉(zhuǎn)換
在 TCP 進(jìn)行三次握手,或者四次揮手的過程中,通信的服務(wù)器和客戶端內(nèi)部會發(fā)送狀態(tài)上的變化,發(fā)生的狀態(tài)變化在程序中是看不到的,這個狀態(tài)的變化也不需要程序猿去維護(hù),但是在某些情況下進(jìn)行程序的調(diào)試會去查看相關(guān)的狀態(tài)信息,下面是三次握手過程中的狀態(tài)轉(zhuǎn)換。
1.1 三次握手
當(dāng)服務(wù)器監(jiān)聽啟動之后,由客戶端發(fā)起的三次握手過程中狀態(tài)轉(zhuǎn)換如下:
第一次握手:
- 客戶端:調(diào)用了 connect() 函數(shù),狀態(tài)變化:沒有狀態(tài) -> SYN_SENT
- 服務(wù)器:收到連接請求 SYN,狀態(tài)變化:LISTEN -> SYN_RCVD
第二次握手:
- 服務(wù)器:給客戶端回復(fù) ACK,并且請求和客戶端建立連接,狀態(tài)無變化,依然是 SYN_RCVD
- 客戶端:接收數(shù)據(jù),收到了 ACK,狀態(tài)變化:SYN_SENT -> ESTABLISHED
第三次握手:
- 客戶端:給服務(wù)器回復(fù) ACK,同意建立連接,狀態(tài)沒有變化,還是 ESTABLISHED
- 服務(wù)器:收到了 ACK,狀態(tài)變化:SYN_RCVD -> ESTABLISHED
三次握手完成之后,客戶端和服務(wù)器都變成了同一種狀態(tài),這種狀態(tài)叫:ESTABLISHED,表示雙向連接已經(jīng)建立, 可以通信了。在數(shù)據(jù)傳輸過程中,正常的通信狀態(tài)就是 ESTABLISHED。
1.2 四次揮手
關(guān)于四次揮手對于客戶端和服務(wù)器哪一端先斷開連接沒有要求,根據(jù)實際情況處理即可。下面根據(jù)上圖中的實例描述一下四次揮手過程中 TCP 的狀態(tài)轉(zhuǎn)換(上圖中主動斷開連接的一方是客戶端):
第一次揮手:
- 客戶端:調(diào)用 close() 函數(shù),將 tcp 協(xié)議中的 FIN 設(shè)置為 1,請求和服務(wù)器斷開連接,狀態(tài)變化:ESTABLISHED -> FIN_WAIT_1
- 服務(wù)器:收到斷開連接請求,狀態(tài)變化: ESTABLISHED -> CLOSE_WAIT
第二次揮手:
- 服務(wù)器:回復(fù) ACK,同意斷開連接的請求,狀態(tài)沒有變化,還是 CLOSE_WAIT
- 客戶端:收到 ACK,狀態(tài)變化:FIN_WAIT_1 -> FIN_WAIT_2
第三次揮手:
- 服務(wù)器端:調(diào)用 close () 函數(shù),發(fā)送 FIN 給客戶端,請求斷開連接,狀態(tài)變化:CLOSE_WAIT -> LAST_ACK
- 客戶端:收到 FIN,狀態(tài)變化:FIN_WAIT_2 -> TIME_WAIT
第四次揮手:
- 客戶端:回復(fù) ACK 給服務(wù)器,狀態(tài)是沒有變化的,狀態(tài)變化:TIME_WAIT -> 沒有狀態(tài)
- 服務(wù)器端:收到 ACK,雙向連接斷開,狀態(tài)變化:LAST_ACK -> 無狀態(tài)(沒有了)
1.3 狀態(tài)轉(zhuǎn)換
在下圖中同樣是描述 TCP 通信過程中的客戶端和服務(wù)器端的狀態(tài)轉(zhuǎn)換,看起來比較亂,其實只需要看兩條主線:紅色實線和綠色虛線。關(guān)于黑色的實線對應(yīng)的是一些特殊情況下的狀態(tài)切換,在此不做任何分析。
因為三次握手是由客戶端發(fā)起的,據(jù)此分析紅色的實線表示的客戶端的狀態(tài),綠色虛線表示的是服務(wù)器端的狀態(tài)。
- 客戶端:
- 服務(wù)器端:
在 TCP 通信的時候,當(dāng)主動斷開連接的一方接收到被動斷開連接的一方發(fā)送的 FIN 和最終的 ACK 后(第三次揮手完成),連接的主動關(guān)閉方必須處于 TIME_WAIT 狀態(tài)并持續(xù) 2MSL(Maximum Segment Lifetime)時間,這樣就能夠讓 TCP 連接的主動關(guān)閉方在它發(fā)送的 ACK 丟失的情況下重新發(fā)送最終的 ACK。
一倍報文壽命 (MSL) 大概時長為 30s,因此兩倍報文壽命一般在 1 分鐘作用。
主動關(guān)閉方重新發(fā)送的最終ACK,是因為被動關(guān)閉方重傳了它的FIN。事實上,被動關(guān)閉方總是重傳FIN直到它收到一個最終的ACK。
1.4 相關(guān)命令
1? ?$ netstat 參數(shù)
2? ?$ netstat -apn?? ?| grep 關(guān)鍵字
- 參數(shù):
2、半關(guān)閉
TCP 連接只有一方發(fā)送了 FIN,另一方?jīng)]有發(fā)出 FIN 包,仍然可以在一個方向上正常發(fā)送數(shù)據(jù),這種狀態(tài)可以稱之為半關(guān)閉或者半連接。當(dāng)四次揮手完成兩次的時候,就相當(dāng)于實現(xiàn)了半關(guān)閉,在程序中只需要在某一端直接調(diào)用 close () 函數(shù)即可。套接字通信默認(rèn)是雙工的,也就是雙向通信,如果進(jìn)行了半關(guān)閉就變成了單工,數(shù)據(jù)只能單向流動了。比如下面的這個例子:
- 服務(wù)器端:
- 客戶端:
按照上述流程做了半關(guān)閉之后,從雙工變成了單工,數(shù)據(jù)單向流動的方向:客戶端 —–> 服務(wù)器端。
1? ?// 專門處理半關(guān)閉的函數(shù) 2? ?#include <sys/socket.h> 3? ?// 可以有選擇的關(guān)閉讀/寫, close()函數(shù)只能關(guān)閉寫操作 4? ?int shutdown(int sockfd, int how);- 參數(shù):
SHUT_RD: 關(guān)閉文件描述符對應(yīng)的讀操作
SHUT_WR: 關(guān)閉文件描述符對應(yīng)的寫操作
SHUT_RDWR: 關(guān)閉文件描述符對應(yīng)的讀寫操作
- 返回值:函數(shù)調(diào)用成功返回 0,失敗返回 - 1
3、端口復(fù)用
在網(wǎng)絡(luò)通信中,一個端口只能被一個進(jìn)程使用,不能多個進(jìn)程共用同一個端口。我們在進(jìn)行套接字通信的時候,如果按順序執(zhí)行如下操作:先啟動服務(wù)器程序,再啟動客戶端程序,然后關(guān)閉服務(wù)器進(jìn)程,再退出客戶端進(jìn)程,最后再啟動服務(wù)器進(jìn)程,就會出如下的錯誤提示信息:bind error: Address already in use
# 第二次啟動服務(wù)器進(jìn)程 $ ./server bind error: Address already in use$ netstat -apn|grep 9999 (Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.) tcp 0 0 127.0.0.1:9999 127.0.0.1:50178 TIME_WAIT通過 netstat 查看 TCP 狀態(tài),發(fā)現(xiàn)上一個服務(wù)器進(jìn)程其實還沒有真正退出。因為服務(wù)器進(jìn)程是主動斷開連接的進(jìn)程,最后狀態(tài)變成了 TIME_WAIT 狀態(tài),這個進(jìn)程會等待 2msl(大約1分鐘) 才會退出,如果該進(jìn)程不退出,其綁定的端口就不會釋放,再次啟動新的進(jìn)程還是使用這個未釋放的端口,端口被重復(fù)使用,就是提示 bind error: Address already in use 這個錯誤信息。
如果想要解決上述問題,就必須要設(shè)置端口復(fù)用,使用的函數(shù)原型如下:
// 這個函數(shù)是一個多功能函數(shù), 可以設(shè)置套接字選項 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);參數(shù):
- sockfd:用于監(jiān)聽的文件描述符
- level:設(shè)置端口復(fù)用需要使用 SOL_SOCKET 宏
- optname:要設(shè)置什么屬性(下邊的兩個宏都可以設(shè)置端口復(fù)用)
SO_REUSEADDR
? ? ? ?SO_REUSEPORT
- optval:設(shè)置是去除端口復(fù)用屬性還是設(shè)置端口復(fù)用屬性,實際應(yīng)該使用 int 型變量
0:不設(shè)置
? ? ? ?1:設(shè)置
- optlen:optval 指針指向的內(nèi)存大小 sizeof (int)
這個函數(shù)應(yīng)該添加到服務(wù)器端代碼中,具體應(yīng)該放到什么位置呢?答:在綁定之前設(shè)置端口復(fù)用
參考代碼
#include <stdio.h> #include <ctype.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/select.h>// server int main(int argc, const char* argv[]) {// 創(chuàng)建監(jiān)聽的套接字int lfd = socket(AF_INET, SOCK_STREAM, 0);if(lfd == -1){perror("socket error");exit(1);}// 綁定struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(9999);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地多有的IP// 127.0.0.1// inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);// 設(shè)置端口復(fù)用int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 綁定端口int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if(ret == -1){perror("bind error");exit(1);}// 監(jiān)聽ret = listen(lfd, 64);if(ret == -1){perror("listen error");exit(1);}fd_set reads, tmp;FD_ZERO(&reads);FD_SET(lfd, &reads);int maxfd = lfd;while(1){tmp = reads;int ret = select(maxfd+1, &tmp, NULL, NULL, NULL);if(ret == -1){perror("select");exit(0);}if(FD_ISSET(lfd, &tmp)){int cfd = accept(lfd, NULL, NULL);FD_SET(cfd, &reads);maxfd = cfd > maxfd ? cfd : maxfd;}for(int i=lfd+1; i<=maxfd; ++i){if(FD_ISSET(i, &tmp)){char buf[1024];int len = read(i, buf, sizeof(buf));if(len > 0){printf("client say: %s\n", buf);write(i, buf, len);}else if(len == 0){printf("客戶端斷開了連接\n");FD_CLR(i, &reads);close(i);}else{perror("read");exit(0);}}}}return 0; }?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的TCP状态转换、半关闭、端口复用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python办公自动化系列之金蝶K3自动
- 下一篇: 龙芯Kodi打造视频直播娱乐中心