目錄node
《UNP》p159總結了以下的狀況:算法
情形 | 對端進程崩潰 | 對端主機崩潰 | 對端主機不可達 |
---|---|---|---|
本端TCP正主動發送數據 | 對端TCP發送一個FIN,這經過使用select判斷可讀條件當即能檢測出來,若是本端TCP發送另外一個分節,對端TCP就以RST響應。若是本端TCP在收到RST後應用進程仍試圖寫套接字,咱們的套接字實現就給該進程發送一個SIGPIPE信號 | 本端TCP將超時,且套接字的待處理錯誤被置爲ETIMEDOUT | 本端TCP將超時,且套接字的待處理錯誤被置爲EHOSTUNREACH |
本端TCP正主動接收數據 | 對端TCP發送一個FIN,咱們將把它做爲一個EOF讀入 | 咱們將中止接收數據 | 咱們將中止接收數據 |
鏈接空閒,保持存活選項已設置 | 對端TCP發送一個FIN,這經過select判斷可讀條件能當即檢測出來 | 在無數據交換2小時後,發送9個保持存活探測分節,而後套接字的待處理錯誤被置爲ETIMEDOUT | 在無數據交換2小時後,發送9個保持存活探測分節,而後套接字的待處理錯誤被置爲HOSTUNREACH |
鏈接空閒,保持存活選項未設置 | 對端TCP發送一個FIN,這經過select判斷可讀條件能當即檢測出來 | 無 | 無 |
服務端接收客戶端的數據並丟棄:c#
int acceptOrDie(uint16_t port) { int listenfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(listenfd >= 0); int yes = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))) { perror("setsockopt"); exit(1); } struct sockaddr_in addr; bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = INADDR_ANY; if (::bind(listenfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr))) { perror("bind"); exit(1); } if (::listen(listenfd, 5)) { perror("listen"); exit(1); } struct sockaddr_in peer_addr; bzero(&peer_addr, sizeof(peer_addr)); socklen_t addrlen = 0; int sockfd = ::accept(listenfd, reinterpret_cast<struct sockaddr*>(&peer_addr), &addrlen); if (sockfd < 0) { perror("accept"); exit(1); } ::close(listenfd); return sockfd; } void discard(int sockfd) { char buf[65536]; while (true) { int nr = ::read(sockfd, buf, sizeof buf); if (nr <= 0) break; } } int main(int argc, char* argv[]) { if (argc < 2) { cout << "usage:./server port\n"; exit(0); } int sockfd = acceptOrDie(atoi(argv[1])); //建立socket, bind, listen discard(sockfd); //讀取並丟棄全部客戶端發送的數據 return 0; }
客戶端從命令行接受字符串併發送給服務端:服務器
struct sockaddr_in resolveOrDie(const char* host, uint16_t port) { struct hostent* he = ::gethostbyname(host); if (!he) { perror("gethostbyname"); exit(1); } assert(he->h_addrtype == AF_INET && he->h_length == sizeof(uint32_t)); struct sockaddr_in addr; bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr = *reinterpret_cast<struct in_addr*>(he->h_addr); return addr; } int main(int argc, char* argv[]) { if (argc < 3) { cout << "usage:./cli host port\n"; exit(0); } struct sockaddr_in addr = resolveOrDie(argv[1], atoi(argv[2])); int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); assert(sockfd >= 0); int ret = ::connect(sockfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)); if (ret) { perror("connect"); exit(1); } char sendline[1024]; while (fgets(sendline, sizeof sendline, stdin) != NULL) { //從命令行讀數據 write_n(sockfd, sendline, strlen(sendline)); //發送給服務端 } return 0; }
先啓動tcpdump觀察數據包的流動,而後分別啓動服務端和客戶端。
下面是三次握手的數據包:併發
15:33:21.184993 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [S], seq 1654237964, win 64240, options [mss 1412,nop,wscale 8,nop,nop,sackOK], length 0 15:33:21.185027 IP 172.19.0.16.1234 > 221.218.38.144.53186: Flags [S.], seq 3710209371, ack 1654237965, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 15:33:21.230698 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [.], ack 1, win 259, length 0
而後終止服務端進程,觀察數據包的狀況。服務端進程終止後,會向客戶端發送一個FIN分節,客戶端內核迴應一個ACK。此時客戶端阻塞在fgets,感覺不到這個FIN分節。socket
15:33:49.310810 IP 172.19.0.16.1234 > 221.218.38.144.53186: Flags [F.], seq 1, ack 8, win 229, length 0 15:33:49.356453 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [.], ack 2, win 259, length 0
若是這時客戶端繼續發送數據,由於服務端進程已經不在了,因此服務端內核響應一個RST分節。tcp
15:34:31.198332 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [P.], seq 8:16, ack 2, win 259, length 8 15:34:31.198360 IP 172.19.0.16.1234 > 221.218.38.144.53186: Flags [R], seq 3710209373, win 0, length 0
若是客戶端在收到RST分節後,繼續發送數據,將會收到SIGPIPE信號,若是使用默認的處理方式,客戶端進程將會崩潰。函數
若是咱們在客戶端代碼中忽略SIGPIPE信號,那麼客戶端不會崩潰。ui
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信號
這種狀況本端TCP會超時,且套接字待處理錯誤會被置爲ETIMEDOUT。.net
服務端主機關機和崩潰不一樣,關機時會關閉進程打開的描述符,因此會發送FIN分節,客戶端若是處理得當,就能檢測到。可是若是是對端主機崩潰,除非設置了SO_KEEPALIVE
選項,不然本端沒法得知對端主機已經崩潰。
這一種狀況對應表格中的第3、四行。
TCP自己是可靠,可是若是使用不當會給人形成TCP不可靠的錯覺。
假設服務端接收鏈接後調用後打開一個本地文件,而後將文件內容經過socket發送給客戶端。
int main(int argc, char* argv[]) { if (argc < 3) { printf("Usage:%s filename port\n", argv[0]); return 0; } int sockfd = acceptOrDie(atoi(argv[2])); printf("accept client\n"); FILE* fp = fopen(argv[1], "rb"); if (!fp) { return 0; } printf("sleeping 10 seconds\n"); sleep(10); char buf[8192]; size_t nr = 0; while ((nr = fread(buf, 1, sizeof buf, fp)) > 0) { //讀文件 write_n(sockfd, buf, nr); //發送給客戶端 } fclose(fp); printf("finish sending file %s\n", argv[1]); }
首先在在服務端啓動該程序./send file_1M_size 1234
。file_1M_size的1M大小的文件。
用nc做爲客戶端nc localhost 1234 | wc -c
。
鏈接創建後,服務端會sleep 10秒,而後拷貝文件,最終客戶端輸出:
1048576
這裏沒問題,確實發送了1M數據的文件。
若是咱們在服務端sleep 10秒期間,在客戶端輸入了一些數據:
root@DESKTOP-2A432QS:/mnt/c/Users/12401/Desktop/network_programing/recipes-master/tpc# nc localhost 1234 | wc -c abcdfef 976824
abcdfef是咱們發送給服務端的,976824是收到的字節數。顯然不夠1M。
創建鏈接後,客戶端也向服務端發送了一些數據,這些數據到達服務端後,保存在服務端的內核緩衝區中。服務端讀取文件後調用write發送出去,雖然write返回了,但這僅僅表明要發送的數據已經被放到了內核發送緩衝區,並不表明已經被客戶端接收了。這時服務端while循環結束,直接退出了main函數,這會致使close鏈接,當接收緩衝區還有數據沒有讀取時調用close,將會向對端發送一個RST分節,該分節會致使發送緩衝區中待發送的數據被丟棄,而不是正常的TCP斷開鏈接序列,從而致使客戶端沒有收到完整的文件。
問題的本質是:在沒有確認對端進程已經收到了完整的數據,就close了socket。那麼如何保證確保對端進程已經收到了完整的數據呢?
一句話:read讀到0以後才close。
發送完數據後,調用shutdown(第二個參數設置爲SHUT_WR),後跟一個read調用,該read返回0,表示對端也關閉了鏈接(這意味着對端應用進程完整接收了咱們發送的數據),而後才close。
發送方接收方程序結構以下:
發送方:1.send() , 2.發送完畢後調用shutdown(WR), 5.read()->0(此時發送方纔算能確認接收方已經接收了所有數據), 6.close()。
接收方:3.read()->0(說明沒有數據可讀了), 4.若是沒有數據可發調用close()。
序號代表了時間的順序。
咱們修改以前的服務端代碼:
int main(int argc, char* argv[]) { if (argc < 3) { printf("Usage:%s filename port\n", argv[0]); return 0; } int sockfd = acceptOrDie(atoi(argv[2])); printf("accept client\n"); FILE* fp = fopen(argv[1], "rb"); if (!fp) { return 0; } printf("sleeping 10 seconds\n"); sleep(10); char buf[8192]; size_t nr = 0; while ((nr = fread(buf, 1, sizeof buf, fp)) > 0) { write_n(sockfd, buf, nr); } fclose(fp); shutdown(sockfd, SHUT_WR); //新增代碼,發送FIN分節 while ((nr = read(sockfd, buf, sizeof buf)) > 0) { //新增代碼,等客戶端close //do nothing } printf("finish sending file %s\n", argv[1]); }
此次在while循環結束後,不是直接退出main,而是shutdown,而後循環read,等客戶端先close,客戶端close後,read會返回0,而後退出main函數。這樣就能保證數據被完整發送了。
root@DESKTOP-2A432QS:/mnt/c/Users/12401/Desktop/network_programing/recipes-master/tpc# nc localhost 1234 | wc -c abcdefg 1048576
此次就算客戶端發送了數據,也能保證收到了完整的1M數據。
參考資料:
若是一個 socket 在接收到了 RST packet以後,程序仍然向這個socket寫入數據,那麼就會產生SIGPIPE信號。
具體例子見「本端TCP發送數據時對端進程已經崩潰」這一節。
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信號
直接忽略該信號,此時write()會返回-1,而且此時errno的值爲EPIPE。
Nagle算法的基本定義是任意時刻,最多隻能有一個未被確認的小段。 所謂「小段」,指的是小於MSS尺寸的數據塊,所謂「未被確認」,是指一個數據塊發送出去後,沒有收到對方發送的ACK確認該數據已收到。
經過TCP_NODELAY選項關閉Nagle算法,通常都須要。
TCP主動關閉的一端在發送最後一個ACK後,必須在TIME_WAIT狀態等待2倍的MSL(報文最大生存時間)。
在鏈接處於2MSL狀態期間,由該插口對(src_ip:src_port, dest_ip:dest_port)定義的鏈接不能被再次使用。對於服務端,若是服務器主動斷開鏈接,那麼在2MSL時間內,該服務器沒法在相同的端口,再次啓動。
可使用SO_REUSEADDR選項,容許一個進程從新使用處於2MSL等待的端口。
這樣能夠防止最後一個ACK丟失,若是丟失了,在2倍的MSL時間內,對端會重發FIN,而後主動關閉的一端能夠再次發送ACK,以確保鏈接正確關閉。
假設處於2MSL狀態的插口對,能再次被使用,那麼前一個鏈接遲到的報文對這個新的鏈接會有影響。
之前文的sender爲例,在服務端執行./sender file_1M_size 1234
,而後客戶端進行鏈接nc localhost 1234 | wc -c
,鏈接後,終止sender進程。
用netstat查看會發現這個鏈接處於TIME_WAIT狀態,而後試圖再在1234端口啓動sender會發現:
bind: Address already in use
開啓套接字的SO_REUSEADDR選項。
int yes = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))) { perror("setsockopt"); exit(1); }