close和shutdown關閉TCP鏈接

內核態用戶態讀寫流程

clipboard.png

write調用的過程shell

  • 用戶態的用戶程序對socket進行write調用
  • 內核會搬運用戶程序緩衝區的數據到內核寫緩衝區(發送緩衝區),搬運完畢write調用就會返回(即便緩衝區上的數據還沒發送出去)
  • 內核TCP協議棧會搬運數據從內核寫緩衝區(發送緩衝區)到網卡
  • 網卡在物理層把數據發送到目標網卡上,中間的網絡過程略過

read調用的過程:服務器

  • 用戶態的用戶程序對socket進行read調用
  • 內核TCP協議棧會搬運網卡上來自源的數據到內核讀緩衝區(接收緩衝區)
  • 內核會搬運內核讀緩衝區(接收緩衝區)的數據到用戶程序緩衝區
  • 用戶程序就能夠在用戶程序緩衝區訪問到這些數據了

shutdown與close

int close(int sockfd)

close函數會對套接字引用計數(引用了這個套接字描述符的進程數)減一,一旦發現套接字引用計數到0,就會對套接字進行完全釋放,而且會關閉TCP兩個方向的數據流並回收鏈接和相關資源,是所謂的粗暴式關閉:網絡

  • 在read方向,內核會將該套接字設置爲不可讀,對套接字的read都會返回異常
  • 在write方向,內核嘗試將發送緩衝區的數據發送給對端,並最後向對端發送一個FIN報文,接下來若是再對該套接字進行write會返回異常
int shutdown(int sockfd, int howto)

shutdown函數能夠單向或者雙向的關閉鏈接,是所謂的優雅式關閉,howto來設置:socket

  • SHUT_RD(0):關閉鏈接的read方向,對該套接字進行read直接返回EOF。從數據角度來看,套接字上接收緩衝區已有的數據將被丟棄,若是再有新的數據流到達,會對數據進行ACK,而後悄悄地丟棄。也就是說,對端仍是會接收到ACK,在這種狀況下根本不知道數據已經被丟棄了

    clipboard.png

  • SHUT_WR(1):關閉鏈接的write方向,這就是常被稱爲半關閉的鏈接。此時,無論套接字引用計數的值是多少,都會直接關閉鏈接的write方向。套接字上發送緩衝區已有的數據將被當即發送出去,並發送一個FIN報文給對端,以後應用程序若是對該套接字進行write會報錯

    clipboard.png

  • SHUT_RDWR(2):至關於SHUT_RD和SHUT_WR操做各一次,關閉套接字的read和write兩個方向

    clipboard.png

寫程序來看一下close和shutdown的區別

client:tcp

int main(int argc, char **argv) {
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }

    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    int n;

    fd_set readmask;
    fd_set allreads;

    FD_ZERO(&allreads);
    FD_SET(0, &allreads);
    FD_SET(socket_fd, &allreads);
    for (;;) {
        readmask = allreads;
        // IO多路複用select函數,能夠同時監聽socket_fd和標準輸入
        int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);
        if (rc <= 0)
            error(1, errno, "select failed");
        if (FD_ISSET(socket_fd, &readmask)) {
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {
                error(1, errno, "read error");
            } else if (n == 0) {
                error(1, 0, "server terminated \n");
            }
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        }
        if (FD_ISSET(0, &readmask)) {
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
                if (strncmp(send_line, "shutdown", 8) == 0) {
                    FD_CLR(0, &allreads);
                    if (shutdown(socket_fd, 1)) {
                        error(1, errno, "shutdown failed");
                    }
                } else if (strncmp(send_line, "close", 5) == 0) {
                    FD_CLR(0, &allreads);
                    if (close(socket_fd)) {
                        error(1, errno, "close failed");
                    }
                    sleep(6);
                    exit(0);
                } else {
                    int i = strlen(send_line);
                    if (send_line[i - 1] == '\n') {
                        send_line[i - 1] = 0;
                    }

                    printf("now sending %s\n", send_line);
                    size_t rt = write(socket_fd, send_line, strlen(send_line));
                    if (rt < 0) {
                        error(1, errno, "write failed ");
                    }
                    printf("send bytes: %zu \n", rt);
                }
            }
        }
    }
}
static void sig_int(int signo) {
    printf("\nreceived %d datagrams\n", count);
    exit(0);
}


int main(int argc, char **argv) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGINT, sig_int);
    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    char message[MAXLINE];
    count = 0;

    for (;;) {
        int n = read(connfd, message, MAXLINE);
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }
        message[n] = 0;
        printf("received %d bytes: %s\n", n, message);
        count++;

        char send_line[MAXLINE];
        sprintf(send_line, "Hi, %s", message);
        // 休眠幾秒模擬服務器工做一段時間
        sleep(5);

        int write_nc = send(connfd, send_line, strlen(send_line), 0);
        printf("send bytes: %zu \n", write_nc);
        if (write_nc < 0) {
            error(1, errno, "error write");
        }
    }
}

close的效果

client:函數

aaa
now sending aaa
send bytes: 3 
close

server:oop

received 3 bytes: aaa
send bytes: 7 
error read: Connection reset by peer (54)

能夠看到client發送完aaa的數據後隨即調用close,會致使client的TCP鏈接斷開且資源回收,server處理完數據發回來的時候發現TCP鏈接已經沒有了,因此就connection reset了,下面用tcpdump追蹤一下:spa

> sudo tcpdump 'tcp and port 9527' -i lo0 -S
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
11:06:23.013648 IP localhost.55463 > localhost.9527: Flags [S], seq 3739428838, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904150004 ecr 0,sackOK,eol], length 0
11:06:23.013755 IP localhost.9527 > localhost.55463: Flags [S.], seq 2449498522, ack 3739428839, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904150004 ecr 904150004,sackOK,eol], length 0
11:06:23.013771 IP localhost.55463 > localhost.9527: Flags [.], ack 2449498523, win 6379, options [nop,nop,TS val 904150004 ecr 904150004], length 0
11:06:23.013783 IP localhost.9527 > localhost.55463: Flags [.], ack 3739428839, win 6379, options [nop,nop,TS val 904150004 ecr 904150004], length 0
11:06:30.327692 IP localhost.55463 > localhost.9527: Flags [P.], seq 3739428839:3739428842, ack 2449498523, win 6379, options [nop,nop,TS val 904157265 ecr 904150004], length 3
11:06:30.327740 IP localhost.9527 > localhost.55463: Flags [.], ack 3739428842, win 6379, options [nop,nop,TS val 904157265 ecr 904157265], length 0
11:06:31.826987 IP localhost.55463 > localhost.9527: Flags [F.], seq 3739428842, ack 2449498523, win 6379, options [nop,nop,TS val 904158750 ecr 904157265], length 0
11:06:31.827034 IP localhost.9527 > localhost.55463: Flags [.], ack 3739428843, win 6379, options [nop,nop,TS val 904158750 ecr 904158750], length 0
11:06:35.328859 IP localhost.9527 > localhost.55463: Flags [P.], seq 2449498523:2449498530, ack 3739428843, win 6379, options [nop,nop,TS val 904162236 ecr 904158750], length 7
11:06:35.328946 IP localhost.55463 > localhost.9527: Flags [R], seq 3739428843, win 0, length 0

分析一下上面的抓包結果:code

C -> S [S]
S -> C [S.]
C -> S [.]
S -> C [.]
C -> S [P.] aaa
S -> C [.]
C -> S [F.]
S -> C [.]
S -> C [P.] Hi, aaa
C -> S [R]

client發完數據aaa後server響應了ack,而後client主動close,client會發送了FIN包給server,server響應了ack後client回收了鏈接和資源,server處理完數據發告終果Hi, aaa給client,這時client鏈接已經斷了因此沒法識別這個鏈接響應了RST包。server

shutdown的效果

client:

aaa
now sending aaa
send bytes: 3 
shutdown
Hi, aaa
server terminated

server:

received 3 bytes: aaa
send bytes: 7 
client closed

能夠看到client發送完aaa的數據後隨即調用shutdown,會致使client的TCP鏈接處於半關閉狀態,這時read方向仍是正常的可是write方向已經斷開了,server處理完數據發回來的時候client還能夠讀到,等一段時間client exit退出鏈接就所有斷開了,服務端read到EOF也就關閉了,一樣的用tcpdump追蹤一下:

> sudo tcpdump 'tcp and port 9527' -i lo0 -S
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
11:06:53.692427 IP localhost.55594 > localhost.9527: Flags [S], seq 2938836011, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904180495 ecr 0,sackOK,eol], length 0
11:06:53.692546 IP localhost.9527 > localhost.55594: Flags [S.], seq 2801533649, ack 2938836012, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904180495 ecr 904180495,sackOK,eol], length 0
11:06:53.692562 IP localhost.55594 > localhost.9527: Flags [.], ack 2801533650, win 6379, options [nop,nop,TS val 904180495 ecr 904180495], length 0
11:06:53.692577 IP localhost.9527 > localhost.55594: Flags [.], ack 2938836012, win 6379, options [nop,nop,TS val 904180495 ecr 904180495], length 0
11:06:58.429387 IP localhost.55594 > localhost.9527: Flags [P.], seq 2938836012:2938836015, ack 2801533650, win 6379, options [nop,nop,TS val 904185206 ecr 904180495], length 3
11:06:58.429435 IP localhost.9527 > localhost.55594: Flags [.], ack 2938836015, win 6379, options [nop,nop,TS val 904185206 ecr 904185206], length 0
11:07:00.789790 IP localhost.55594 > localhost.9527: Flags [F.], seq 2938836015, ack 2801533650, win 6379, options [nop,nop,TS val 904187548 ecr 904185206], length 0
11:07:00.789847 IP localhost.9527 > localhost.55594: Flags [.], ack 2938836016, win 6379, options [nop,nop,TS val 904187548 ecr 904187548], length 0
11:07:03.431085 IP localhost.9527 > localhost.55594: Flags [P.], seq 2801533650:2801533657, ack 2938836016, win 6379, options [nop,nop,TS val 904190180 ecr 904187548], length 7
11:07:03.431161 IP localhost.55594 > localhost.9527: Flags [.], ack 2801533657, win 6379, options [nop,nop,TS val 904190180 ecr 904190180], length 0
11:07:03.431663 IP localhost.9527 > localhost.55594: Flags [F.], seq 2801533657, ack 2938836016, win 6379, options [nop,nop,TS val 904190180 ecr 904190180], length 0
11:07:03.431728 IP localhost.55594 > localhost.9527: Flags [.], ack 2801533658, win 6379, options [nop,nop,TS val 904190180 ecr 904190180], length 0

分析一下上面的抓包結果:

C -> S [S]
S -> C [S.]
C -> S [.]
S -> C [.]
C -> S [P.] aaa
S -> C [.]
C -> S [F.]
S -> C [.]
S -> C [P.] Hi, aaa
C -> S [.]
S -> C [F.]
C -> S [.]

client發完數據aaa後server響應了ack,而後client主動shutdown,client會發送了FIN包給server,server響應了ack後client半關閉只能讀不能再寫了,server處理完數據發告終果Hi, aaa給client,這時client讀了最後的結果全關閉讀寫,注意這時只是關閉了讀寫沒有回收資源,server讀到了EOF發鬆了最後的FIN,client回覆了ACK,最後是完整的四次揮手。

注意關閉的是socket不是鏈接

以前分析問題的時候我有一個疑問:既然client處於半關閉了,也就是隻能讀不能寫了,那爲何還能夠發送ack給server呢,其實這裏就是沒完全理解關閉的意義,半關閉是說socket這個套接字描述符半關閉了,不是鏈接自己半關閉了,鏈接在內核態還存在,因此仍是能夠經過內核TCP協議棧正常通訊,可是用戶態的程序對socket的write調用不行了。再明白的來看其實仍是下面這張圖:

clipboard.png

用戶態裏紅色的write雖然關閉了,可是內核態裏面寫緩衝到網卡之間仍是通的。

相關文章
相關標籤/搜索