TCP状态转换、半关闭、端口复用
目錄
1、TCP狀態轉換
1.1 三次握手
1.2 四次揮手
1.3 狀態轉換
1.4 相關命令
2、半關閉
3、端口復用
1、TCP狀態轉換
在 TCP 進行三次握手,或者四次揮手的過程中,通信的服務器和客戶端內部會發送狀態上的變化,發生的狀態變化在程序中是看不到的,這個狀態的變化也不需要程序猿去維護,但是在某些情況下進行程序的調試會去查看相關的狀態信息,下面是三次握手過程中的狀態轉換。
1.1 三次握手
當服務器監聽啟動之后,由客戶端發起的三次握手過程中狀態轉換如下:
第一次握手:
- 客戶端:調用了 connect() 函數,狀態變化:沒有狀態 -> SYN_SENT
- 服務器:收到連接請求 SYN,狀態變化:LISTEN -> SYN_RCVD
第二次握手:
- 服務器:給客戶端回復 ACK,并且請求和客戶端建立連接,狀態無變化,依然是 SYN_RCVD
- 客戶端:接收數據,收到了 ACK,狀態變化:SYN_SENT -> ESTABLISHED
第三次握手:
- 客戶端:給服務器回復 ACK,同意建立連接,狀態沒有變化,還是 ESTABLISHED
- 服務器:收到了 ACK,狀態變化:SYN_RCVD -> ESTABLISHED
三次握手完成之后,客戶端和服務器都變成了同一種狀態,這種狀態叫:ESTABLISHED,表示雙向連接已經建立, 可以通信了。在數據傳輸過程中,正常的通信狀態就是 ESTABLISHED。
1.2 四次揮手
關于四次揮手對于客戶端和服務器哪一端先斷開連接沒有要求,根據實際情況處理即可。下面根據上圖中的實例描述一下四次揮手過程中 TCP 的狀態轉換(上圖中主動斷開連接的一方是客戶端):
第一次揮手:
- 客戶端:調用 close() 函數,將 tcp 協議中的 FIN 設置為 1,請求和服務器斷開連接,狀態變化:ESTABLISHED -> FIN_WAIT_1
- 服務器:收到斷開連接請求,狀態變化: ESTABLISHED -> CLOSE_WAIT
第二次揮手:
- 服務器:回復 ACK,同意斷開連接的請求,狀態沒有變化,還是 CLOSE_WAIT
- 客戶端:收到 ACK,狀態變化:FIN_WAIT_1 -> FIN_WAIT_2
第三次揮手:
- 服務器端:調用 close () 函數,發送 FIN 給客戶端,請求斷開連接,狀態變化:CLOSE_WAIT -> LAST_ACK
- 客戶端:收到 FIN,狀態變化:FIN_WAIT_2 -> TIME_WAIT
第四次揮手:
- 客戶端:回復 ACK 給服務器,狀態是沒有變化的,狀態變化:TIME_WAIT -> 沒有狀態
- 服務器端:收到 ACK,雙向連接斷開,狀態變化:LAST_ACK -> 無狀態(沒有了)
1.3 狀態轉換
在下圖中同樣是描述 TCP 通信過程中的客戶端和服務器端的狀態轉換,看起來比較亂,其實只需要看兩條主線:紅色實線和綠色虛線。關于黑色的實線對應的是一些特殊情況下的狀態切換,在此不做任何分析。
因為三次握手是由客戶端發起的,據此分析紅色的實線表示的客戶端的狀態,綠色虛線表示的是服務器端的狀態。
- 客戶端:
- 服務器端:
在 TCP 通信的時候,當主動斷開連接的一方接收到被動斷開連接的一方發送的 FIN 和最終的 ACK 后(第三次揮手完成),連接的主動關閉方必須處于 TIME_WAIT 狀態并持續 2MSL(Maximum Segment Lifetime)時間,這樣就能夠讓 TCP 連接的主動關閉方在它發送的 ACK 丟失的情況下重新發送最終的 ACK。
一倍報文壽命 (MSL) 大概時長為 30s,因此兩倍報文壽命一般在 1 分鐘作用。
主動關閉方重新發送的最終ACK,是因為被動關閉方重傳了它的FIN。事實上,被動關閉方總是重傳FIN直到它收到一個最終的ACK。
1.4 相關命令
1? ?$ netstat 參數
2? ?$ netstat -apn?? ?| grep 關鍵字
- 參數:
2、半關閉
TCP 連接只有一方發送了 FIN,另一方沒有發出 FIN 包,仍然可以在一個方向上正常發送數據,這種狀態可以稱之為半關閉或者半連接。當四次揮手完成兩次的時候,就相當于實現了半關閉,在程序中只需要在某一端直接調用 close () 函數即可。套接字通信默認是雙工的,也就是雙向通信,如果進行了半關閉就變成了單工,數據只能單向流動了。比如下面的這個例子:
- 服務器端:
- 客戶端:
按照上述流程做了半關閉之后,從雙工變成了單工,數據單向流動的方向:客戶端 —–> 服務器端。
1? ?// 專門處理半關閉的函數 2? ?#include <sys/socket.h> 3? ?// 可以有選擇的關閉讀/寫, close()函數只能關閉寫操作 4? ?int shutdown(int sockfd, int how);- 參數:
SHUT_RD: 關閉文件描述符對應的讀操作
SHUT_WR: 關閉文件描述符對應的寫操作
SHUT_RDWR: 關閉文件描述符對應的讀寫操作
- 返回值:函數調用成功返回 0,失敗返回 - 1
3、端口復用
在網絡通信中,一個端口只能被一個進程使用,不能多個進程共用同一個端口。我們在進行套接字通信的時候,如果按順序執行如下操作:先啟動服務器程序,再啟動客戶端程序,然后關閉服務器進程,再退出客戶端進程,最后再啟動服務器進程,就會出如下的錯誤提示信息:bind error: Address already in use
# 第二次啟動服務器進程 $ ./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 狀態,發現上一個服務器進程其實還沒有真正退出。因為服務器進程是主動斷開連接的進程,最后狀態變成了 TIME_WAIT 狀態,這個進程會等待 2msl(大約1分鐘) 才會退出,如果該進程不退出,其綁定的端口就不會釋放,再次啟動新的進程還是使用這個未釋放的端口,端口被重復使用,就是提示 bind error: Address already in use 這個錯誤信息。
如果想要解決上述問題,就必須要設置端口復用,使用的函數原型如下:
// 這個函數是一個多功能函數, 可以設置套接字選項 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);參數:
- sockfd:用于監聽的文件描述符
- level:設置端口復用需要使用 SOL_SOCKET 宏
- optname:要設置什么屬性(下邊的兩個宏都可以設置端口復用)
SO_REUSEADDR
? ? ? ?SO_REUSEPORT
- optval:設置是去除端口復用屬性還是設置端口復用屬性,實際應該使用 int 型變量
0:不設置
? ? ? ?1:設置
- optlen:optval 指針指向的內存大小 sizeof (int)
這個函數應該添加到服務器端代碼中,具體應該放到什么位置呢?答:在綁定之前設置端口復用
參考代碼
#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[]) {// 創建監聽的套接字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);// 設置端口復用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);}// 監聽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; }?
?
?
?
總結
以上是生活随笔為你收集整理的TCP状态转换、半关闭、端口复用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python办公自动化系列之金蝶K3自动
- 下一篇: 龙芯Kodi打造视频直播娱乐中心