写点什么

可靠传输的 TCP 协议 send 成功就意味着数据一定发出去了?

作者:JackJiang
  • 2025-10-17
    江苏
  • 本文字数:5511 字

    阅读完需:约 18 分钟

可靠传输的TCP协议send成功就意味着数据一定发出去了?

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

1、引言

回复过很多 IM 初学者关于 MobileIMSDK  通信层代码的疑问,最基础的问题就是“明明用的是 TCP 协议,而 TCP 协议也被称为可靠的通信协议,那为什么 TCP 代码中明确能知道数据是否发送成功,为什么仍然需要应用层去实现消息应答和重传这种逻辑?”。

要真正讲清楚这个问题,还真不是三言两语能讲的明白。。。

本篇文章我们以 TCP 协议的网络编程逻辑,从 Socket 缓冲区的角度去拆解,为什么号称可靠传输的 TCP 协议,在代码中调用 send 并成功发出数据,并不意味着这个数据就一定通过物理网络发出去了。


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 就够了。

相关文章可以进一步阅读:


  1. 理论经典:TCP协议的3次握手与4次挥手过程详解

  2. 脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手

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 都会先把数据放到发送队列中,然后根据实际情况再考虑是不是立刻发送。(本文已同步发布于:http://www.52im.net/thread-4868-1-1.html

而我们大部分情况下,都不会用  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 的消息可靠性和送达机制

用户头像

JackJiang

关注

还未添加个人签名 2019-08-26 加入

开源IM框架MobileIMSDK、BeautyEye的作者。

评论

发布
暂无评论
可靠传输的TCP协议send成功就意味着数据一定发出去了?_网络编程_JackJiang_InfoQ写作社区