【修正版】动图图解!代码执行send成功后,数据就发出去了吗?

沙海 2021年8月9日01:56:18Java评论27字数 6774阅读22分34秒阅读模式
摘要

智能摘要

智能摘要文章源自JAVA秀-https://www.javaxiu.com/40735.html

编程的时候,如果要跟某个IP建立连接,我们需要调用操作系统提供的socket API。也就是说一个socket,会带有两个缓冲区,一个用于发送,一个用于接收。以前以为,这种情况下,内核会把发送缓冲区数据清空,然后四次挥手。每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。此时内核就会把这份数据先用发送缓冲区缓存起来,待会应用层说ok了,再一起发。文章源自JAVA秀-https://www.javaxiu.com/40735.html

原文约 3925 | 图片 16 | 建议阅读 8 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?

原创 小白 小白debug 文章源自JAVA秀-https://www.javaxiu.com/40735.html

收录于话题文章源自JAVA秀-https://www.javaxiu.com/40735.html

#计算机网络5文章源自JAVA秀-https://www.javaxiu.com/40735.html

#漫画图解网络7文章源自JAVA秀-https://www.javaxiu.com/40735.html

#计算机基础6文章源自JAVA秀-https://www.javaxiu.com/40735.html

#面试12文章源自JAVA秀-https://www.javaxiu.com/40735.html

#后端11文章源自JAVA秀-https://www.javaxiu.com/40735.html

今天又是被倾盆的需求淹没的一天。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

有没有人知道,那种“我用3句话,就让产品为我砍了18个需求”的鸡汤课在哪报名,想报。文章源自JAVA秀-https://www.javaxiu.com/40735.html

"听懂掌声"的那种课就算了,太费手了。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

扯远了,回到我们今天的正题,我们了解下这篇文的目录。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

目录文章源自JAVA秀-https://www.javaxiu.com/40735.html

代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

回答这个问题之前,需要了解什么是Socket 缓冲区文章源自JAVA秀-https://www.javaxiu.com/40735.html

Socket 缓冲区

什么是 socket 缓冲区

编程的时候,如果要跟某个IP建立连接,我们需要调用操作系统提供的 socket API文章源自JAVA秀-https://www.javaxiu.com/40735.html

socket 在操作系统层面,可以理解为一个文件文章源自JAVA秀-https://www.javaxiu.com/40735.html

我们可以对这个文件进行一些方法操作文章源自JAVA秀-https://www.javaxiu.com/40735.html

listen方法,可以让程序作为服务器监听其他客户端的连接。文章源自JAVA秀-https://www.javaxiu.com/40735.html

connect,可以作为客户端连接服务器。文章源自JAVA秀-https://www.javaxiu.com/40735.html

sendwrite可以发送数据,recvread可以接收数据。文章源自JAVA秀-https://www.javaxiu.com/40735.html

在建立好连接之后,这个 socket 文件就像是远端机器的 "代理人" 一样。比如,如果我们想给远端服务发点什么东西,那就只需要对这个文件执行写操作就行了。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

socket_api文章源自JAVA秀-https://www.javaxiu.com/40735.html

那写到了这个文件之后,剩下的发送工作自然就是由操作系统内核来完成了。文章源自JAVA秀-https://www.javaxiu.com/40735.html

既然是写给操作系统,那操作系统就需要提供一个地方给用户写。同理,接收消息也是一样。文章源自JAVA秀-https://www.javaxiu.com/40735.html

这个地方就是 socket 缓冲区文章源自JAVA秀-https://www.javaxiu.com/40735.html

用户发送消息的时候写给 send buffer(发送缓冲区)文章源自JAVA秀-https://www.javaxiu.com/40735.html

用户接收消息的时候写给 recv buffer(接收缓冲区)文章源自JAVA秀-https://www.javaxiu.com/40735.html

也就是说一个socket ,会带有两个缓冲区,一个用于发送,一个用于接收。因为这是个先进先出的结构,有时候也叫它们发送、接收队列文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

一个socket有两个缓冲区文章源自JAVA秀-https://www.javaxiu.com/40735.html

怎么观察 socket 缓冲区

如果想要查看 socket 缓冲区,可以在linux环境下执行 netstat -nt 命令。文章源自JAVA秀-https://www.javaxiu.com/40735.html

# netstat -ntActive Internet connections (w/o servers)Proto Recv-Q Send-Q Local Address           Foreign Address         State      tcp        0     60 172.22.66.69:22         122.14.220.252:59889    ESTABLISHED

这上面表明了,这里有一个协议(Proto)类型为 TCP 的连接,同时还有本地(Local Address)和远端(Foreign Address)的IP信息,状态(State)是已连接。文章源自JAVA秀-https://www.javaxiu.com/40735.html

还有Send-Q 是发送缓冲区,下面的数字60是指,当前还有60 Byte在发送缓冲区中未发送。而 Recv-Q 代表接收缓冲区, 此时是空的,数据都被应用进程接收干净了。文章源自JAVA秀-https://www.javaxiu.com/40735.html

TCP部分

【动图缓冲区的收发流程,TCP执行发收的流程】文章源自JAVA秀-https://www.javaxiu.com/40735.html

我们在使用TCP建立连接之后,一般会使用 send 发送数据。文章源自JAVA秀-https://www.javaxiu.com/40735.html

int main(int argc, char *argv[]){    // 创建socket    sockfd=socket(AF_INET,SOCK_STREAM, 0))    // 建立连接      connect(sockfd, 服务器ip信息, sizeof(server))      // 执行 send 发送消息    send(sockfd,str,sizeof(str),0))      // 关闭 socket    close(sockfd);    return 0;}

上面是一段伪代码,仅用于展示大概逻辑,我们在建立好连接后,一般会在代码中执行 send 方法。那么此时,消息就会被立刻发到对端机器吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

执行 send 发送的字节,会立马发送吗?

答案是不确定!执行 send 之后,数据只是拷贝到了socket 缓冲区。至 什么时候会发数据,发多少数据,全听操作系统安排。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

tcp_sendmsg 逻辑文章源自JAVA秀-https://www.javaxiu.com/40735.html

在用户进程中,程序通过操作 socket 会从用户态进入内核态,而 send方法会将数据一路传到传输层。在识别到是 TCP协议后,会调用 tcp_sendmsg 方法。文章源自JAVA秀-https://www.javaxiu.com/40735.html

// net/ipv4/tcp.c// 以下省略了大量逻辑int tcp_sendmsg(){    // 如果还有可以放数据的空间  if (skb_availroom(skb) > 0) {    // 尝试拷贝待发送数据到发送缓冲区    err = skb_add_data_nocache(sk, skb, from, copy);  }    // 下面是尝试发送的逻辑代码,先省略     }

在 tcp_sendmsg 中, 核心工作就是将待发送的数据组织按照先后顺序放入到发送缓冲区中, 然后根据实际情况(比如拥塞窗口等)判断是否要发数据。如果不发送数据,那么此时直接返回。文章源自JAVA秀-https://www.javaxiu.com/40735.html

如果缓冲区满了会怎么办

前面提到的情况里是,发送缓冲区有足够的空间,可以用于拷贝待发送数据。文章源自JAVA秀-https://www.javaxiu.com/40735.html

如果发送缓冲区空间不足,或者满了,执行发送,会怎么样?

这里分两种情况。文章源自JAVA秀-https://www.javaxiu.com/40735.html

首先,socket在创建的时候,是可以设置是阻塞的还是非阻塞的。文章源自JAVA秀-https://www.javaxiu.com/40735.html

int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

比如通过上面的代码,就可以将 socket 设置为非阻塞SOCK_NONBLOCK)。文章源自JAVA秀-https://www.javaxiu.com/40735.html

当发送缓冲区满了,如果还向socket执行send文章源自JAVA秀-https://www.javaxiu.com/40735.html

  • 如果此时 socket 是阻塞的,那么程序会在那干等、死等,直到释放出新的缓存空间,就继续把数据拷进去,然后返回文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

send阻塞文章源自JAVA秀-https://www.javaxiu.com/40735.html

  • 如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息,意思是  Try again , 现在缓冲区满了,你也别等了,待会再试一次。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

send非阻塞文章源自JAVA秀-https://www.javaxiu.com/40735.html

我们可以简单看下源码是怎么实现的。还是回到刚才的 tcp_sendmsg 发送方法中。文章源自JAVA秀-https://www.javaxiu.com/40735.html

int tcp_sendmsg(){    if (skb_availroom(skb) > 0) {    // ..如果有足够缓冲区就执行balabla  } else {    // 如果发送缓冲区没空间了,那就等到有空间,至于等的方式,分阻塞和非阻塞    if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)        goto do_error;  }   }        

里面提到的  sk_stream_wait_memory 会根据socket是否阻塞来决定是一直等等一会就返回。文章源自JAVA秀-https://www.javaxiu.com/40735.html

int sk_stream_wait_memory(struct sock *sk, long *timeo_p){    while (1) {    // 非阻塞模式时,会等到超时返回 EAGAIN        if (等待超时))            return -EAGAIN;          // 阻塞等待时,会等到发送缓冲区有足够的空间了,才跳出        if (sk_stream_memory_free(sk) && !vm_wait)            break;    }    return err;}

如果接收缓冲区为空,执行 recv 会怎么样?

接收缓冲区也是类似的情况。文章源自JAVA秀-https://www.javaxiu.com/40735.html

当接收缓冲区为空,如果还向socket执行 recv文章源自JAVA秀-https://www.javaxiu.com/40735.html

  • 如果此时 socket 是阻塞的,那么程序会在那干等,直到接收缓冲区有数据,就会把数据从接收缓冲区拷贝到用户缓冲区,然后返回文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

recv阻塞文章源自JAVA秀-https://www.javaxiu.com/40735.html

  • 如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

recv非阻塞文章源自JAVA秀-https://www.javaxiu.com/40735.html

下面用一张图汇总一下,方便大家保存面试的时候用哈哈哈。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

socket读写缓冲区满了的情况汇总文章源自JAVA秀-https://www.javaxiu.com/40735.html

如果socket缓冲区还有数据,执行close了,会怎么样?

首先我们要知道,一般正常情况下,发送缓冲区和接收缓冲区 都应该是空的。文章源自JAVA秀-https://www.javaxiu.com/40735.html

如果发送、接收缓冲区长时间非空,说明有数据堆积,这往往是由于一些网络问题或用户应用层问题,导致数据没有正常处理。文章源自JAVA秀-https://www.javaxiu.com/40735.html

那么正常情况下,如果 socket 缓冲区为空,执行 close。就会触发四次挥手。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

TCP四次挥手文章源自JAVA秀-https://www.javaxiu.com/40735.html

这个也是面试老八股文内容了,这里我们只需要关注第一次挥手,发的是 FIN 就够了文章源自JAVA秀-https://www.javaxiu.com/40735.html

如果接收缓冲区有数据时,执行close了,会怎么样?

socket close 时,主要的逻辑在 tcp_close() 里实现。文章源自JAVA秀-https://www.javaxiu.com/40735.html

先说结论,关闭过程主要有两种情况:文章源自JAVA秀-https://www.javaxiu.com/40735.html

  • 如果接收缓冲区还有数据未读,会先把接收缓冲区的数据清空,然后给对端发一个RST。文章源自JAVA秀-https://www.javaxiu.com/40735.html

  • 如果接收缓冲区是空的,那么就调用 tcp_send_fin() 开始进行四次挥手过程的第一次挥手。文章源自JAVA秀-https://www.javaxiu.com/40735.html

void tcp_close(struct sock *sk, long timeout){  // 如果接收缓冲区有数据,那么清空数据    while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {        u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq -              tcp_hdr(skb)->fin;        data_was_unread += len;        __kfree_skb(skb);    }   if (data_was_unread) {    // 如果接收缓冲区的数据被清空了,发 RST        tcp_send_active_reset(sk, sk->sk_allocation);     } else if (tcp_close_state(sk)) {    // 正常四次挥手, 发 FIN        tcp_send_fin(sk);    }    // 等待关闭    sk_stream_wait_close(sk, timeout);}

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

recvbuf非空文章源自JAVA秀-https://www.javaxiu.com/40735.html

如果发送缓冲区有数据时,执行close了,会怎么样?

以前以为,这种情况下,内核会把发送缓冲区数据清空,然后四次挥手。文章源自JAVA秀-https://www.javaxiu.com/40735.html

但是发现源码并不是这样的文章源自JAVA秀-https://www.javaxiu.com/40735.html

void tcp_send_fin(struct sock *sk){  // 获得发送缓冲区的最后一块数据    struct sk_buff *skb, *tskb = tcp_write_queue_tail(sk);    struct tcp_sock *tp = tcp_sk(sk);  // 如果发送缓冲区还有数据    if (tskb && (tcp_send_head(sk) || sk_under_memory_pressure(sk))) {        TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN; // 把最后一块数据值为 FIN         TCP_SKB_CB(tskb)->end_seq++;        tp->write_seq++;    }  else {    // 发送缓冲区没有数据,就造一个FIN包  }  // 发送数据    __tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);}

此时,还有些数据没发出去,内核会把发送缓冲区最后一个数据块拿出来。然后置为 FIN。文章源自JAVA秀-https://www.javaxiu.com/40735.html

socket 缓冲区是个先进先出的队列,这种情况是指内核会等待TCP层安静把发送缓冲区数据都发完,最后再执行 四次挥手的第一次挥手(FIN包)。文章源自JAVA秀-https://www.javaxiu.com/40735.html

有一点需要注意的是,只有在接收缓冲区为空的前提下,我们才有可能走到 tcp_send_fin() 。而只有在进入了这个方法之后,我们才有可能考虑发送缓冲区是否为空的场景。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

sendbuf非空文章源自JAVA秀-https://www.javaxiu.com/40735.html

UDP部分

UDP也有缓冲区吗

说完TCP了,我们聊聊UDP。这对好基友,同时都是传输层里的重要协议。既然前面提到TCP有发送、接收缓冲区,那UDP有吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

以前我以为。文章源自JAVA秀-https://www.javaxiu.com/40735.html

"每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。"文章源自JAVA秀-https://www.javaxiu.com/40735.html

后来我发现我错了。文章源自JAVA秀-https://www.javaxiu.com/40735.html

UDP socket 也是 socket,一个socket 就是会有收和发两个缓冲区。跟用什么协议关系不大。文章源自JAVA秀-https://www.javaxiu.com/40735.html

有没有是一回事,用不用又是一回事。文章源自JAVA秀-https://www.javaxiu.com/40735.html

UDP不用发送缓冲区?

事实上,UDP不仅有发送缓冲区,也用发送缓冲区。文章源自JAVA秀-https://www.javaxiu.com/40735.html

一般正常情况下,会把数据直接拷到发送缓冲区后直接发送。文章源自JAVA秀-https://www.javaxiu.com/40735.html

还有一种情况,是在发送数据的时候,设置一个 MSG_MORE 的标记。文章源自JAVA秀-https://www.javaxiu.com/40735.html

ssize_t send(int sock, const void *buf, size_t len, int flags); // flag 置为 MSG_MORE

大概的意思是告诉内核,待会还有其他更多消息要一起发,先别着急发出去。此时内核就会把这份数据先用发送缓冲区缓存起来,待会应用层说ok了,再一起发。文章源自JAVA秀-https://www.javaxiu.com/40735.html

我们可以看下源码。文章源自JAVA秀-https://www.javaxiu.com/40735.html

int udp_sendmsg(){    // corkreq 为 true 表示是 MSG_MORE 的方式,仅仅组织报文,不发送;    int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;    //  将要发送的数据,按照MTU大小分割,每个片段一个skb;并且这些    //  skb会放入到套接字的发送缓冲区中;该函数只是组织数据包,并不执行发送动作。    err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,                 sizeof(struct udphdr), &ipc, &rt,                 corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);    // 没有启用 MSG_MORE 特性,那么直接将发送队列中的数据发送给IP。     if (!corkreq)        err = udp_push_pending_frames(sk);}

因此,不管是不是 MSG_MORE, IP都会先把数据放到发送队列中,然后根据实际情况再考虑是不是立刻发送。文章源自JAVA秀-https://www.javaxiu.com/40735.html

而我们大部分情况下,都不会用  MSG_MORE,也就是来一个数据包就直接发一个数据包。从这个行为上来说,虽然UDP用上了发送缓冲区,但实际上并没有起到"缓冲"的作用。文章源自JAVA秀-https://www.javaxiu.com/40735.html

最后

这篇文章,我也就写了20个小时吧。画图也就画吐了而已,每天早上7点钟爬起来写一个多小时再去上班。文章源自JAVA秀-https://www.javaxiu.com/40735.html

兄弟们都是自家人,点不点赞,在不在看什么的,没关系的,大家看开心了就好。文章源自JAVA秀-https://www.javaxiu.com/40735.html

在看,点赞什么的,我不是特别在意,真的,真的,别不信啊。文章源自JAVA秀-https://www.javaxiu.com/40735.html

不三连也真的没关系的。文章源自JAVA秀-https://www.javaxiu.com/40735.html

兄弟们不要在意啊。文章源自JAVA秀-https://www.javaxiu.com/40735.html

我是心口不一的小白,我们下期见!文章源自JAVA秀-https://www.javaxiu.com/40735.html

最近,想建个群。考虑到大家平时估计已经加了不少技术群了,所以决定建了个聊天摸鱼划水斗图群文章源自JAVA秀-https://www.javaxiu.com/40735.html

虽然我的技术比不上大佬们,但我表情包多啊!文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

大家午休了,或者下班了。文章源自JAVA秀-https://www.javaxiu.com/40735.html

可以偶尔刷刷群,互相聊聊天,吹吹牛,就很解压。文章源自JAVA秀-https://www.javaxiu.com/40735.html

在群里多交朋友。你下一次面试的面试官,很可能也在群里。文章源自JAVA秀-https://www.javaxiu.com/40735.html

加我,拉你进群。文章源自JAVA秀-https://www.javaxiu.com/40735.html

【修正版】动图图解!代码执行send成功后,数据就发出去了吗?文章源自JAVA秀-https://www.javaxiu.com/40735.html

别说了,一起在知识的海洋里呛水吧文章源自JAVA秀-https://www.javaxiu.com/40735.html

点击下方名片,关注公众号:【小白debug】文章源自JAVA秀-https://www.javaxiu.com/40735.html

文章源自JAVA秀-https://www.javaxiu.com/40735.html

文章推荐:

文章源自JAVA秀-https://www.javaxiu.com/40735.html

阅读原文文章源自JAVA秀-https://www.javaxiu.com/40735.html

继续阅读
速蛙云 - 极致体验,强烈推荐!!!购买套餐就免费送各大视频网站会员!快速稳定、独家福利社、流媒体稳定解锁!速度快,全球上网、视频、游戏加速、独立IP均支持!基础套餐性价比很高!这里不多说,我一直正在使用,推荐购买:https://www.javaxiu.com/59919.html
weinxin
资源分享QQ群
本站是JAVA秀团队的技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定