可靠传输的TCP协议send乐成绩意味着数据肯定发出去了?

[复制链接]
发表于 2025-10-10 10:57:52 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本文来自小白debug的原创分享,原题“【修正版】动图图解!代码实行send乐成后,数据就发出去了吗?”,下文有修订和排版优化。
1、弁言

复兴过很多IM初学者关于MobileIMSDK  通讯层代码的疑问,最底子的题目就是“显着用的是TCP协议,而TCP协议也被称为可靠的通讯协议,那为什么TCP代码中明白能知道数据是否发送乐成,为什么仍然必要应用层去实现消息应答和重传这种逻辑?”。
要真正讲清楚这个题目,还真不是三言两语能讲的明白。。。
本篇文章我们以TCP协议的网络编程逻辑,从Socket缓冲区的角度去拆解,为什么号称可靠传输的TCP协议,在代码中调用send并乐成发出数据,并不意味着这个数据就肯定通过物理网络发出去了。
  技能交换:
- 移动端IM开辟入门文章:《新手入门一篇就够:从零开辟移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地点点此)
(本文已同步发布于:http://www.52im.net/thread-4868-1-1.html)
2、系列文章

本文是系列文章中的第 21篇,大纲如下:
《不为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)》
《不为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)》
《不为人知的网络编程(三):关闭TCP毗连时为什么会TIME_WAIT、CLOSE_WAIT》
《不为人知的网络编程(四):深入研究分析TCP的非常关闭》
《不为人知的网络编程(五):UDP的毗连性和负载均衡》
《不为人知的网络编程(六):深入地明白UDP协议并用好它》
《不为人知的网络编程(七):怎样让不可靠的UDP变的可靠?》
《不为人知的网络编程(八):从数据传输层深度解密HTTP》
《不为人知的网络编程(九):理论接洽现实,全方位深入明白DNS》
《不为人知的网络编程(十):深入操纵体系,从内核明白网络包的吸取过程(Linux篇)》
《不为人知的网络编程(十一):从底层入手,深度分析TCP毗连耗时的机密》
《不为人知的网络编程(十二):彻底搞懂TCP协议层的KeepAlive保活机制》
《不为人知的网络编程(十三):深入操纵体系,彻底搞懂127.0.0.1本机网络通讯》
《不为人知的网络编程(十四):拔掉网线再插上,TCP毗连还在吗?一文即懂!》
《不为人知的网络编程(十五):深入操纵体系,一文搞懂Socket到底是什么》
《不为人知的网络编程(十六):深入分析与办理TCP的RST经典非常题目》
《不为人知的网络编程(十七):冰山之下,一次网络哀求背后的技能机密》
《不为人知的网络编程(十八):UDP比TCP高效?还真不肯定!》
《不为人知的网络编程(十九):能Ping通,TCP就肯定能毗连和通讯吗?》
《不为人知的网络编程(二十):网络ping不通到底有多少缘故原由?一文搞明白!》
《不为人知的网络编程(二十一):可靠传输的TCP协议send乐成绩意味着数据肯定发出去了?》(☜ 本文)
3、什么是 socket 缓冲区

编程的时间,如果要跟某个IP创建毗连,我们必要调用操纵体系提供的 socket API。socket 在操纵体系层面,可以明白为一个文件。
我们可以对这个文件举行一些方法操纵:

  • 1)用listen方法:可以让步调作为服务器监听其他客户端的毗连;
  • 2)用connect:可以作为客户端毗连服务器;
  • 3)用send或write:可以发送数据,recv或read可以吸取数据。
在创建好毗连之后,这个 socket 文件就像是远端呆板的 "署理人" 一样。好比,如果我们想给远端服务发点什么东西,那就只必要对这个文件实行写操纵就行了。
那写到了这个文件之后,剩下的发送工作自然就是由操纵体系内核来完成了。既然是写给操纵体系,那操纵体系就必要提供一个地方给用户写。同理,吸取消息也是一样。
这个地方就是 socket 缓冲区:

  • 1)用户发送消息的时间写给 send buffer(发送缓冲区);
  • 2)用户吸取消息的时间写给 recv buffer(吸取缓冲区)。
也就是说:一个socket 会带有两个缓冲区,一个用于发送,一个用于吸取(如下图所示)。由于这是个先辈先出的结构,偶然候也叫它们发送、吸取队列。
4、怎么观察 socket 缓冲区

如果想要查察 socket 缓冲区,可以在linux情况下实行 netstat -nt 下令:
# netstat -nt
Active 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)是已毗连。
另有Send-Q 是发送缓冲区,下面的数字60是指,当前另有60 Byte在发送缓冲区中未发送。而 Recv-Q 代表吸取缓冲区, 此时是空的,数据都被应用历程吸取干净了。
5、实行 send 发送的字节,会立马发送吗?

我们在利用TCP创建毗连之后,一样平常会利用 send 发送数据:
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 方法。那么此时,消息就会被立刻发到对端呆板吗?
答案是不确定!实行 send 之后,数据只是拷贝到了socket 缓冲区。至 什么时间会发数据,发多少数据,全听操纵体系安排。
tcp_sendmsg 逻辑:
在用户历程中,步调通过操纵 socket 会从用户态进入内核态,而 send方法会将数据一起传到传输层。在辨认到是 TCP协议后,会调用 tcp_sendmsg 方法。
// net/ipv4/tcp.c
// 以下省略了大量逻辑
int tcp_sendmsg()
{ 
  // 如果另有可以放数据的空间
  if (skb_availroom(skb) > 0) {
    // 实行拷贝待发送数据到发送缓冲区
    err = skb_add_data_nocache(sk, skb, from, copy);
  } 
  // 下面是实行发送的逻辑代码,先省略    
}
在 tcp_sendmsg 中, 焦点工作就是将待发送的数据构造按照先后序次放入到发送缓冲区中, 然后根据现实情况(好比拥塞窗口等)判定是否要发数据。如果不发送数据,那么此时直接返回。
6、如果Socket缓冲区满了会怎么办

前面提到的情况里是,发送缓冲区有充足的空间,可以用于拷贝待发送数据。
6.1 如果发送缓冲区空间不敷,大概满了,实行发送,会怎么样?

这里分两种情况。
起首:socket在创建的时间,是可以设置是壅闭的还黑白壅闭的。
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
好比通过上面的代码,就可以将 socket 设置为非壅闭 (SOCK_NONBLOCK)。
当发送缓冲区满了,如果还向socket实行send。。。。
1)如果此时 socket 是壅闭的,那么步调会在那干等、死等,直到开释出新的缓存空间,就继承把数据拷进去,然后返回。
send壅闭:
2)如果此时 socket 黑白壅闭的,步调就会立刻返回一个 EAGAIN 错误信息,意思是  Try again , 如今缓冲区满了,你也别等了,待会再试一次。
send非壅闭:
我们可以简朴看下源码是怎么实现的,还是回到刚才的 tcp_sendmsg 发送方法中:
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是否壅闭来决定是不停等等一会就返回。
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;
}
6.2 如果吸取缓冲区为空,实行 recv 会怎么样?

吸取缓冲区也是类似的情况。当吸取缓冲区为空,如果还向socket实行 recv。
1)如果此时 socket 是壅闭的,那么步调会在那干等,直到吸取缓冲区有数据,就会把数据从吸取缓冲区拷贝到用户缓冲区,然后返回。
recv壅闭:
2)如果此时 socket 黑白壅闭的,步调就会立刻返回一个 EAGAIN 错误信息。
recv非壅闭:
下面用一张图汇总一下,方便各人生存口试的时间用哈哈哈。
socket读写缓冲区满了的情况汇总:
7、如果Socket缓冲区满了会怎么办?

7.1概述

起首我们要知道,一样平常正常情况下,发送缓冲区和吸取缓冲区都应该是空的。如果发送、吸取缓冲区长时间非空,阐明有数据堆积,这通常是由于一些网络题目或用户应用层题目,导致数据没有正常处置惩罚。
那么正常情况下,如果 socket 缓冲区为空,实行 close。就会触发四次挥手。
TCP四次挥手:
这个也是口试老八股文内容了,这里我们只必要关注第一次挥手,发的是 FIN 就够了。
相干文章可以进一步阅读:
7.2 如果吸取缓冲区有数据时,实行close了,会怎么样?

socket close 时,重要的逻辑在 tcp_close() 里实现。
先说结论,关闭过程重要有两种情况:
1)如果吸取缓冲区另有数据未读,会先把吸取缓冲区的数据清空,然后给对端发一个RST;
2)如果吸取缓冲区是空的,那么就调用 tcp_send_fin() 开始举行四次挥手过程的第一次挥手。
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);
}
recvbuf非空:
7.3 如果发送缓冲区有数据时,实行close了,会怎么样?

从前以为,这种情况下,内核会把发送缓冲区数据清空,然后四次挥手。
但是发现源码并不是如许的:
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。
socket 缓冲区是个先辈先出的队列,这种情况是指内核会期待TCP层安静把发送缓冲区数据都发完,末了再实行 四次挥手的第一次挥手(FIN包)。
有一点必要留意的是,只有在吸取缓冲区为空的条件下,我们才有大概走到 tcp_send_fin() 。而只有在进入了这个方法之后,我们才有大概思量发送缓冲区是否为空的场景。
8、拓展阅读:UDP有缓冲区吗?

8.1 UDP也有缓冲区吗

说完TCP了,我们聊聊UDP。这对好基友,同时都是传输层里的告急协议。既然前面提到TCP有发送、吸取缓冲区,那UDP有吗?
从前我以为:
"每个UDP socket都有一个吸取缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以精确吸取,以是不缓冲,不必要发送缓冲区。"
厥后我发现我错了:UDP socket 也是 socket,一个socket 就是会有收和发两个缓冲区。跟用什么协议关系不大。
有没有是一回事,用不消又是一回事。
8.2 UDP不消发送缓冲区?

毕竟上,UDP不光有发送缓冲区,也用发送缓冲区。
一样平常正常情况下,会把数据直接拷到发送缓冲区后直接发送。另有一种情况,是在发送数据的时间,设置一个 MSG_MORE 的标志。
ssize_t send(int sock, const void *buf, size_t len, int flags); // flag 置为 MSG_MORE
大概的意思是告诉内核,待会另有其他更多消息要一起发,先别发急发出去。此时内核就会把这份数据先用发送缓冲区缓存起来,待会应用层说ok了,再一起发。
我们可以看下源码:
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都会先把数据放到发送队列中,然后根据现实情况再思量是不是立刻发送。
而我们大部分情况下,都不会用  MSG_MORE,也就是来一个数据包就直接发一个数据包。从这个活动上来说,固然UDP用上了发送缓冲区,但现实上并没有起到"缓冲"的作用。
9、参考资料

[1] TCP/IP详解 - 第21章·TCP的超时与重传
[2] 快速明白TCP协议一篇就够
[3] 如果你来操持TCP协议,会怎么做?
[4] 手把手教你写基于TCP的Socket长毗连
[5] 到底什么是Socket?一文即懂!
[6] 我们在读写Socket时,毕竟在读写什么?
[7] 拔掉网线再插上,TCP毗连还在吗?一文即懂!
[8] 深入操纵体系,一文搞懂Socket到底是什么
[9] 为何基于TCP协议的移动端IM仍然必要心跳保活机制?
[10] 从客户端的角度来谈谈移动端IM的消息可靠性和送达机制

(本文已同步发布于:http://www.52im.net/thread-4868-1-1.html)
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表