客戶端斷連,服務端也斷?
- 2019 年 10 月 6 日
- 筆記
客戶端斷連,服務端也斷?
0.導語
在socket網路編程中,如果此時客戶端忽然由於某種原因斷開連接或者崩潰,服務端沒有處理好,便會同時崩潰掉,本篇文章將會從崩潰到問題分析,解決,一步步入手。
1.問題分析
問題分析可以結合TCP的"四次握手"關閉。
TCP是全雙工的信道, 可以看作兩條單工信道, TCP連接兩端的兩個端點各負責一條. 當對端調用close時, 雖然本意是關閉整個兩條信道, 但本端只是收到FIN包. 按照TCP協議的語義, 表示對端只是關閉了其所負責的那一條單工信道, 仍然可以繼續接收數據. 也就是說, 因為TCP協議的限制, 一個端點無法獲知對端的socket是調用了close還是shutdown。
對一個已經收到FIN包的socket調用read方法, 如果接收緩衝已空, 則返回0, 這就是常說的表示連接關閉. 但第一次對其調用write方法時, 如果發送緩衝沒問題, 會返回正確寫入(發送). 但發送的報文會導致對端發送RST報文, 因為對端的socket已經調用了close, 完全關閉, 既不發送, 也不接收數據. 所以, 第二次調用write方法(假設在收到RST之後), 會生成SIGPIPE訊號, 導致進程退出。
上述簡化:SIGPIPE
產生的原因是這樣的:如果一個 socket 在接收到了 RST packet 之後,程式仍然向這個 socket 寫入數據,那麼就會產生SIGPIPE
訊號。
舉例如下:當 client 連接到 server 之後,這時候 server 準備向 client 發送多條消息,但在發送消息之前,client 進程意外奔潰了,那麼接下來 server 在發送多條消息的過程中,就會出現SIGPIPE
訊號。下面看一下 server 的程式碼:
#include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #include <csignal> #define MAXLINE 120 int main(int argc, char *argv[]) { signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 訊號 //1.創建一個偵聽socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << "create listen socket error." << std::endl; return -1; } //2.初始化伺服器地址 struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(8088); if (bind(listenfd, (struct sockaddr *) &bindaddr, sizeof(bindaddr)) == -1) { std::cout << "bind listen socket error." << std::endl; return -1; } //3.啟動偵聽 if (listen(listenfd, SOMAXCONN) == -1) { std::cout << "listen error." << std::endl; return -1; } /** * 服務端連續寫兩次數據到客戶端 */ while (true) { struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); //4. 接受客戶端連接 int clientfd = accept(listenfd, (struct sockaddr *) &clientaddr, &clientaddrlen); if (clientfd != -1) { // 假設此時 client 奔潰, 那麼 server 將接收到 client 發送的 FIN sleep(5); // 寫入第一條消息 char msg1[MAXLINE] = {"first message"}; ssize_t n = write(clientfd, msg1, strlen(msg1)); printf("write %ld bytesn", n); // 此時第一條消息發送成功,server 接收到 client 發送的 RST sleep(1); // 寫入第二條消息,出現 SIGPIPE 訊號,導致 server 被殺死 char msg2[MAXLINE] = {"second message"}; n = write(clientfd, msg2, strlen(msg2)); printf("%ld, %sn", n, strerror(errno)); } close(clientfd); } //關閉偵聽socket close(listenfd); return 0;
我們可以使用 Linux 的 nc 工具作為 client,當 client 連接到 server 之後,就立即殺死 client (模擬 client 的意外奔潰)。這時可以觀察 server 的運行情況:
$ gcc -o server server.c $ ./server & # 後台運行 server $ nc localhost 8888 # 運行 nc 連接到 server ^C # Ctrl-C 殺死 nc write 13 bytes [1]+ Broken pipe ./server
分析一下整個過程:
- client 連接到 server 之後,client 進程意外奔潰,這時它會發送一個 FIN 給 server。
- 此時 server 並不知道 client 已經奔潰了,所以它會發送第一條消息給 client。但 client 已經退出了,所以 client 的 TCP 協議棧會發送一個 RST 給 server。
- server 在接收到 RST 之後,繼續寫入第二條消息。往一個已經收到 RST 的 socket 繼續寫入數據,將導致
SIGPIPE
訊號,從而殺死 server。
2.如何解決
對 server 來說,為了不被SIGPIPE
訊號殺死,那就需要忽略SIGPIPE
訊號:
int main(){ signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 訊號 ... }
重新運行上面的程式,server 在發送第二條消息的時候,write()
會返回-1
,並且此時errno
的值為EPIPE
,所以這時並不會產生SIGPIPE
訊號。