博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

SO_LINGER 延时关闭 优雅关闭

Posted on 2016-02-01 10:06  bw_0927  阅读(18431)  评论(0编辑  收藏  举报

https://www.cnblogs.com/my_life/articles/5173081.html

http://blog.csdn.net/feiyinzilgd/article/details/5894446

http://blog.csdn.net/feiyinzilgd/article/details/5894300

 

https://www.cnblogs.com/rainbowzc/archive/2009/09/14/1566151.html

此选项指定函数close对面向连接的协议如何操作(如TCP)。内核缺省close操作是立即返回,如果有数据残留在套接口缓冲区中则系统将试着将这些数据发送给对方。

有下列三种情况:

1、设置 l_onoff为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close调用会立即返回给调用者,如果可能将会传输任何未发送的数据;

 

2、设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态

这种关闭方式称为“强制”或“失效”关闭,因为套接口的虚电路立即被复位,且丢失了未发送的数据。在远端的recv()调用将以WSAECONNRESET出错。

 

3、设置 l_onoff 为非0,l_linger为非0,当套接口关闭时内核将拖延一段时间(由l_linger决定)。

这种关闭称为“优雅”或“从容”关闭。请注意如果套接口置为非阻塞且SO_LINGER设为非零超时,则closesocket()调用将以WSAEWOULDBLOCK错误返回。

 

如果套接口缓冲区中仍残留数据,进程将处于睡眠状态,直 到(a)所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0)或(b)延迟时间到。

此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前 时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失。

close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果套接口设为非阻塞的,它将不等待close完成。

 

 


 

 


Linux网络编程中,socket的选项很多.其中几个比较重要的选项有:SO_LINGER(仅仅适用于TCP,SCTP), SO_REUSEADDR.

 

SO_LINGER
在默认情况下,当调用close关闭socke的使用,close会立即返回,但是,如果send buffer中还有数据,系统会试着先把send buffer中的数据发送出去,然后close才返回.

 

SO_LINGER选项则是用来修改这种默认操作的.于SO_LINGER相关联的一个结构体如下:

[cpp] view plaincopyprint?
#include <sys/socket.h>
struct linger {
int l_onoff //0=off, nonzero=on(开关)
int l_linger //linger time(延迟时间)
}

当调用setsockopt之后,该选项产生的影响取决于linger结构体中 l_onoff和l_linger的值:

  • 0 = l_onoff

当l_onoff被设置为0的时候,将会关闭SO_LINGER选项,即TCP或则SCTP保持默认操作:close立即返回.l_linger值被忽略.

  • l_lineoff值非0,0 = l_linger

当调用close的时候,TCP连接会立即断开.send buffer中未被发送的数据将被丢弃,并向对方发送一个RST信息.值得注意的是,由于这种方式,是非正常的4中握手方式结束TCP链接,所以,TCP连接将不会进入TIME_WAIT状态,这样会导致新建立的可能和就连接的数据造成混乱。具体原因详见我的上一篇文章《linux 网络编程之TIME_WAIT状态》

 

  • l_onoff和l_linger都是非0

在这种情况下,回事的close返回得到延迟。调用close去关闭socket的时候,内核将会延迟。也就是说,如果send buffer中还有数据尚未发送,该进程将会被休眠直到一下任何一种情况发生:

1) send buffer中的所有数据都被发送并且得到对方TCP的应答消息(这种应答并不是意味着对方应用程序已经接收到数据,在后面shutdown将会具体讲道)
2) 延迟时间消耗完。在延迟时间被消耗完之后,send buffer中的所有数据都将会被丢弃。

 

上面1),2)两种情况中,如果socket被设置为O_NONBLOCK状态,程序将不会等待close返回,send buffer中的所有数据都将会被丢弃。所以,需要我们判断close的返回值。在send buffer中的所有数据都被发送之前并且延迟时间没有消耗完,close返回的话,close将会返回一个EWOULDBLOCK的error.

下面用几个实例来说明:


A. Close默认操作:立即返回

 

此种情况,close立即返回,如果send buffer中还有数据,close将会等到所有数据被发送完之后之后返回。由于我们并没有等待对方TCP发送的ACK信息,所以我们只能保证数据已经发送到对方,我们并不知道对方是否已经接受了数据。由于此种情况,TCP连接终止是按照正常的4次握手方式,需要经过TIME_WAIT。

 

B. l_onoff非0,并且使之l_linger为一个整数

 

 

 

在这种情况下,close会在接收到对方TCP的ACK信息之后才返回(l_linger消耗完之前)。但是这种ACK信息只能保证对方已经接收到数据,并不保证对方应用程序已经读取数据。

 

 

C. l_linger设置值太小

 

 

 

 

这种情况,由于l_linger值太小,在send buffer中的数据都发送完之前,close就返回,此种情况终止TCP连接,更l_linger = 0类似,TCP连接终止不是按照正常的4步握手,所以,TCP连接不会进入TIME_WAIT状态,那么,client会向server发送一个RST信息.

 

 

D. Shutdown,等待应用程序读取数据

  

 

 

 

同上面的B进行对比,调用shutdown后紧接着调用read,此时read会被阻塞,直到接收到对方的FIN,也就是说read是在server的应用程序调用close之后才返回的。当server应用程序读取到来自client的数据和FIN之后,server会进入一个叫CLOSE_WAIT,关于CLOSE_WAIT,详见我的博客《 Linux 网络编程 之 TCP状态转换》 。那么,如果server端要断开该TCP连接,需要server应用程序调用一次close,也就意味着向client发送FIN。这个时候,说明server端的应用程序已经读取到client发送的数据和FIN。read会在接收到server的FIN之后返回。所以,shutdown 可以确保server端应用程序已经读取数据了,而不仅仅是server已经接收到数据而已。

 

shutdown参数如下:

SHUT_RD:调用shutdown的一端receive buffer将被丢弃掉,无法接受数据,但是可以发送数据,send buffer的数据可以被发送出去

SHUT_WR:调用shutdown的一端无法发送数据,但是可以接受数据.该参数表示不能调用send.但是如果还有数据在send buffer中,这些数据还是会被继续发送出去的.

 

 

 

SO_REUSEADDR和SO_REUSEPORT
最近,看到CSDN的linux版块,有人提问,说为什么server程序重启之后,无法连接,需要过一段时间才能连接上.我想对于这个问题,有两种可能:一种可能就是该server一直停留在TIME_WAIT状态(有一个服务进程一直在为客户端服务,还没结束).这个时候,需要等待2MSL的时间才能重新连接上,具体细节原因请见我的另一篇文章《linux 网络编程之TIME_WAIT状态》

另一种可能就是SO_REUSEADDR参数设置问题.关于TIME_WAIT的我就不在这里重述了,这里我讲一讲SO_REUSEADDR.

 

SO_REUSEADDR允许一个server程序listen监听并bind到一个端口,既是这个端口已经被一个正在运行的连接使用了.
我们一般会在下面这种情况中遇到:

 

一个监听(listen)server已经启动
当有client有连接请求的时候,server产生一个子进程去处理该client的事物.
server主进程终止了,但是子进程还在占用该连接处理client的事情.虽然主进程终止了,但是由于子进程没有终止,该socket的引用计数不会为0,所以该socket不会被关闭.
server程序重启.

默认情况下,server重启,调用socket,bind,然后listen,会失败.因为该端口正在被使用.如果设定SO_REUSEADDR,那么server重启才会成功.因此,所有的TCP server都必须设定此选项,用以应对server重启的现象.

 

 

SO_REUSEADDR允许同一个端口上绑定多个IP.只要这些IP不同.另外,还可以在绑定IP通配符.但是最好是先绑定确定的IP,最后绑定通配符IP.一面系统拒绝.简而言之,SO_REUSEADDR允许多个server绑定到同一个port上,只要这些server指定的IP不同,但是SO_REUSEADDR需要在bind调用之前就设定.在TCP中,不允许建立起一个已经存在的相同的IP和端口的连接.但是在UDP中,是允许的.

 

 


 

问题

  在使用 TCP 网络编程时,有时发送了一些数据后,要传输的数据的最后几 kb,有时是几 Mb 没有到达。

  具体参见博客:The ultimate SO_LINGER page, or: why is my tcp not reliable

原因

  send()成功返回只意味着内核接收了数据,并准备在某些时候发送它们。内核接收数据后,还要把数据包发送到网卡,并在网络中各个网卡遍历,最终到达远程主机。远程主机的内核确认到数据,拥有该 socket 的进程从中读取数据,此时数据才真正到达应用程序,用文件系统的话来说,是 “hit the disk”。

  当调用close()关闭 socket fd 时,整个 TCP 连接也关闭了,即使一些数据还在内核的发送缓冲区里,或者已经发送但未被确认。

发送方如果 send() 后立即 close() ,就可能出现数据其实还未发送的情况。设置 socket 选项SO_LINGER尝试将残留在发送缓冲区的数据发送给对方,看似解决了这种问题,但有时依然会出现数据发送不全的问题。

  原因在于,发送方执行 close() 时,如果它的接收缓冲区中仍有数据没有读取,或者调用 close() 后有新的数据到达,这时它会发送一个RST告知对方数据丢失,没有正常使用FIN断开连接,因此设置SO_LINGER没有效果。

解决

  那么如果发送方先读取了自己接受缓冲区的数据,再 close(),问题会得到解决吗?并不会。

这时需要借助shutdown()shutdown() 会确实发送一个FIN给对方,说明对方我方即将关闭 socket。此时对方可以通过 recv() 返回 0 (收到 EOF)检测到接受端的关闭

  正确的关闭逻辑如下,建议用这种方式代替SO_LINGER

  • 发送方:send() → shutdown(WR) → recv() == 0(由接收方 close 导致) → close()
  • 接收方:recv() == 0(由发送方 shutdown 导致) → more to send? → close()

  值得注意,如果遇到恶意或错误 client,永远不 close(),则服务器 recv() 不会返回 0(阻塞且 errno == EAGAIN),因此需要加一个超时控制,若 shutdown(WR) 若干秒后 recv() 未返回 0,则直接 close() 强制关闭连接。

  即使如此,shutdown() 也不能保证接收方接受到所有数据,这只是发送方能做到的最大努力。最好的办法还是像 HTTP 协议那样,附有消息的长度信息,这就需要有能力自己设计协议

  还有一种方法,Linux 记录了未确认数据的数量,可以使用ioctlSIOCOUTQ选项查询,如果这个数字达到 0,我们至少可以确认所有的发送数据到达了远程操作系统,只是只能在 Linux 平台下实现。

 

 


 

https://segmentfault.com/a/1190000020303015

内核态用户态读写流程

 

 

write调用的过程

  • 用户态的用户程序对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来设置:

  • 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:

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:

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

可以看到client发送完aaa的数据后随即调用close,会导致client的TCP连接断开且资源回收,server处理完数据发回来的时候发现TCP连接已经没有了,所以就connection reset了,下面用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: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

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

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包。

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调用不行了。再明白的来看其实还是下面这张图:

 

 

用户态里红色的write虽然关闭了,但是内核态里面写缓冲到网卡之间还是通的。