专栏名称: 高可用架构
高可用架构公众号。
目录
相关文章推荐
架构师之路  ·  你的提示词根本只是在浪费算力,如何让deep ... ·  3 天前  
架构师之路  ·  你的提示词根本只是在浪费算力,让deepse ... ·  4 天前  
架构师之路  ·  90%的用户不知道!触发DeepSeek深度 ... ·  5 天前  
51好读  ›  专栏  ›  高可用架构

一个有关tcp的非常有意思的问题

高可用架构  · 公众号  · 架构  · 2020-12-25 09:35

正文

作者:wangyuntao, 小公司CTO一枚,热爱底层研究,喜欢技术分享。

假设以下场景:


在tcp建立连接后,先主动关闭其服务端,之后再在客户端下对其socket进行写操作,正常思维都会认为,这个写操作肯定会返回错误吧?


还真不一定。


今天在写代码时就遇到了这个问题,还纠结了挺久的,最后翻了下linux内核源码,才确定了答案。


先用下面的程序模拟下这个场景:


#include #include #include #include #include #include #include #include #include 
int tcp_connect() { int sockfd, err; struct sockaddr_in addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0); assert(sockfd != -1);
bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(9999);
err = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)); assert(err == 0);
return sockfd;}
int main(int argc, char **argv) { int n; int sockfd = tcp_connect();
signal(SIGPIPE, SIG_IGN); // 防止write触发SIGPIPE,便于测试
printf("请于5秒钟内关闭服务端...\n"); sleep(5);
// write 1 n = write(sockfd, "hello\n", 6); if (n == -1) { perror("第一次write失败"); return -1; } assert(n == 6); printf("第一次write成功!\n");
sleep(1); // 确保客户端收到tcp的reset消息
// write 2 n = write(sockfd, "world\n", 6); if (n == -1) { perror("第二次write失败"); return -1; } assert(n == 6); printf("第二次write成功!\n");
return 0;}


这段程序代表客户端,服务端就用ncat来模拟。


下面是执行流程:


先打开一个terminal,用ncat开一个服务端:


ncat




    
 -l 9999


再打开另一个terminal,编译上面的程序,然后执行:


$ gcc main.c$ ./a.out请于5秒钟内关闭服务端...第一次write成功!第二次write失败: Broken pipe


当客户端提示关闭服务端时,要切换到对应的terminal,关闭服务端。


从上面的输出可以看到,之后的两次写,第一次成功了,第二次才失败。


奇怪吧。


我们用tcpdump抓包看下,第一次是否是真的写成功了:


sudo tcpdump -i any -nport 9999    1  17:59:07.812599 IP 127.0.0.1.51614 > 127.0.0.1.9999: Flags [S], seq 1076934668, win 65495, options [mss 65495,sackOK,TS val 134308422 ecr 0,nop,wscale 7], length 0    2  17:59:07.812648 IP 127.0.0.1.9999 > 127.0.0.1.51614: Flags [S.], seq 3833531274, ack 1076934669, win 65483, options [mss 65495,sackOK,TS val 134308422 ecr 134308422,nop,wscale 7], length 0    3  17:59:07.812691 IP 127.0.0.1.51614 > 127.0.0.1.9999: Flags [.], ack 1, win 512, options [nop,nop,TS val 134308422 ecr 134308422], length 0        4  17:59:09.832579 IP 127.0.0.1.9999 > 127.0.0.1.51614: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 134310442 ecr 134308422], length 0    5  17:59:09.835181 IP 127.0.0.1.51614 > 127.0.0.1.9999: Flags




    
 [.], ack 2, win 512, options [nop,nop,TS val 134310445 ecr 134310442], length 0        6  17:59:12.813697 IP 127.0.0.1.51614 > 127.0.0.1.9999: Flags [P.], seq 1:7, ack 2, win 512, options [nop,nop,TS val 134313423 ecr 134310442], length 6    7  17:59:12.813735 IP 127.0.0.1.9999 > 127.0.0.1.51614Flags [R]seq 3833531276, win 0, length 0


还真是成功了,看上面第6个包,发送的数据长度是6,即:我们代码中的hello\n。


这里大概解释下tcpdump的输出:


前三个包是tcp的三次握手,完成之后代表tcp建立连接成功。


第四个包是我们在关闭服务端时,服务端发给客户端的fin包,表示关闭连接请求。


第五个包是客户端发给服务端的tcp层的ack,表示已经收到fin包。


第六个包是客户端发给服务端的hello\n字符串。


第七个包是服务端的tcp层发给客户端的reset包,因为此时服务端的socket已经关闭了。


由tcpdump的输出可以确定,第一次write的确是写成功了,但为什么呢? 明明服务端的socket都已经关闭了,为什么还可以发送呢? 并且为什么第一次可以发送,第二次就不行了呢?


来看下内核源码是怎么做的:


// net/ipv4/tcp_input.cint tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size){        ...        err = -EPIPE;        if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))                goto do_error;        ...        // 省略这部分是tcp发送数据的代码        ...        return copied + copied_syn;        ...do_error:        ...        return err;}EXPORT_SYMBOL_GPL(tcp_sendmsg_locked);


该方法就是tcp发消息的方法。


由上可见,只有当socket发生错误时,或者我们关闭了socket的send端,上面的write方法才会返回错误,其他情况下,write的数据都会正常发送。


由tcp的相关知识我们可以知道,当服务端发送fin消息给客户端时,客户端的socket进入了CLOSE_WAIT状态,即: 等待客户端的程序关闭其socket。


也就是说,fin消息并没有使客户端的socket发生错误,也并没有关闭客户端socket的send端(但是关闭了客户端socket的receive端),所以第一次write就成功的将数据发送出去了。


那第二次write为什么失败呢?







请到「今天看啥」查看全文