簡體   English   中英

為什么應用程序可以在 CLOSE_WAIT 狀態(Linux)下接受和讀/寫 TCP 連接?

[英]Why app can accept and read/write TCP connection in CLOSE_WAIT state (Linux)?

我的假設是當 TCP 連接處於 ESTABLISHED 狀態時,服務器可以從/向套接字讀/寫。 但是我看到當 TCP 連接處於 CLOSE_WAIT 狀態時,服務器實際上可以讀取和寫入套接字。 當客戶端關閉了它的連接,但服務器還沒有檢測/處理流結束的情況時,就會發生這種情況。

例如。 同步的、阻塞的單線程服務器:

#include <unistd.h>
#include <netdb.h>
#include <signal.h>

int main (void)
{
    signal(SIGPIPE, SIG_IGN); // ignore SIGPIPE from last send()
    int listen_sock = socket(PF_INET, SOCK_STREAM, 0);
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
    struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(8000), .sin_addr.s_addr = htonl(INADDR_ANY)};
    bind(listen_sock, (struct sockaddr *) &addr, sizeof(addr));
    listen(listen_sock, 10);
    while (1) {
        int sock = accept(listen_sock, 0, 0);
        char buffer[1024];
        recv(sock, buffer, sizeof(buffer), 0);
        sleep(1);
        send(sock, buffer, 10, 0);
        send(sock, buffer, 10, 0);
        sleep(1);
        send(sock, buffer, 10, 0);
        close(sock);
    }
}

編譯並運行:

gcc server_short.c -o server_short && strace ./server_short

觀看同一主機上的連接:

watch -n0.1 'sudo ss -lnt | grep 8000; sudo netstat -tpvn | grep 8000'

在另一台主機上運行短期客戶端:

seq 20 | xargs -P100 -n1 bash -c 'echo -n "0123456789ABCDEF" | telnet $SERVER_IP 8000'

然后我看到連接處於 CLOSE_WAIT 和 SYN_RECV 狀態,SYN_RECV 將變成 CLOSE_WAIT,因為排隊的連接將由服務器處理:

LISTEN    11        10                 0.0.0.0:8000             0.0.0.0:*       
tcp       17      0 server_IP:8000       client_IP:33016        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33030        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33020        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33028        CLOSE_WAIT  -                   
tcp        0      0 server_IP:8000       client_IP:33012        CLOSE_WAIT  21282/./server 
tcp        0      0 server_IP:8000       client_IP:33046        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33040        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33044        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33036        SYN_RECV    -                   
tcp       17      0 server_IP:8000       client_IP:33018        CLOSE_WAIT  -                   
tcp        0      0 server_IP:8000       client_IP:33048        SYN_RECV    -                   
tcp       17      0 server_IP:8000       client_IP:33034        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33022        CLOSE_WAIT  -                   
tcp        0      0 server_IP:8000       client_IP:33042        SYN_RECV    -                   
tcp        0      0 server_IP:8000       client_IP:33038        SYN_RECV    -                   
tcp       17      0 server_IP:8000       client_IP:33032        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33026        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33014        CLOSE_WAIT  -                   
tcp       17      0 server_IP:8000       client_IP:33024        CLOSE_WAIT  -

然后服務器一一處理這些 CLOSE_WAIT 連接:

socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(8000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 10)                           = 0
accept(3, NULL, NULL)                   = 4
recvfrom(4, "0123456789ABCDEF", 1024, 0, NULL, NULL) = 16
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=22658, si_uid=1001} ---
close(4)                                = 0
accept(3, NULL, NULL)                   = 4
recvfrom(4, "0123456789ABCDEF", 1024, 0, NULL, NULL) = 16
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=22658, si_uid=1001} ---
close(4)                                = 0
... AND SO ON

因此,服務器可以通過調用 read 或通過調用 write 檢測管道損壞,在檢測流結束之前成功接受、讀取請求、處理它並發送響應。

我知道這個服務器代碼並不理想,但即使你讓它異步(使用 select、epoll、boost::asio 等)你也將接受 CLOSE_WAIT 連接,在檢測到連接之前讀取傳入請求和啟動請求處理沒活着。 雖然服務器端知道連接已被客戶端關閉,然后才被服務器接受。

所以問題:

  1. 這種行為是否遵循 TCP 規范? 接受和讀取 CLOSE_WAIT 連接是否合法?
  2. 為什么內核允許接受 CLOSE_WAIT 連接? 或者為什么在這種情況下 recv 不返回錯誤? 誰能對從關閉的連接中讀取數據感興趣? 這種行為的目的是什么? 似乎我沒有看到如何使用這種情況。
  3. 當 100% 確定沒有人會收到響應時,如何檢測這種情況而不開始處理請求?

TCP 堆棧獨立於您的用戶空間應用程序。 當您將套接字設置為LISTEN ,TCP 堆棧已經接受傳入的連接(傳入的SYNSYN|ACK響應)並且還將傳入的數據包存儲到接收緩沖區中。

當您程序調用accept()它要么阻塞並等待某人連接到套接字,要么返回從 TCP 堆棧(或 TCP 狀態機)的角度來看已經存在的已建立的套接字。

有完全合法的用例,客戶端打開連接,傳遞一些數據並立即關閉連接。 然后,服務器可以花時間從網絡緩沖區中檢索該數據並處理請求(例如,如果該請求不需要對客戶端的回答)。

CLOSE_WAIT本質上是“TCP 堆棧也在等待本地應用程序關閉套接字”的狀態,因此您假設只有在狀態為ESTABLISHED時才能read()是錯誤的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM