TCP之send函数研究

From:http://bbs.chinaunix.net/thread-1380350-1-1.html

tcp协议本身是可靠的,并不等于应用程序用tcp发送数据就一定是可靠的.不管是否阻塞,send发送的大小,并不代表对端recv到多少的数据.


在阻塞模式下, send函数的过程是将应用程序请求发送的数据拷贝到发送缓存中发送并得到确认后再返回.但由于发送缓存的存在,表现为:如果发送缓存大小比请求发送的大小要大,那么send函数立即返回,同时向网络中发送数据;否则,send向网络发送缓存中不能容纳的那部分数据,并等待对端确认后再返回(接收端只要将数据收到接收缓存中,就会确认,并不一定要等待应用程序调用recv);

在非阻塞模式下,send函数的过程仅仅是将数据拷贝到协议栈的缓存区而已,如果缓存区可用空间不够,则尽能力的拷贝,返回成功拷贝的大小;如缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.


linux下可用sysctl -a | grep net.ipv4.tcp_wmem查看系统默认的发送缓存大小:
net.ipv4.tcp_wmem = 4096 16384 81920
这有三个值,第一个值是socket的发送缓存区分配的最少字节数,第二个值是默认值(该值会被net.core.wmem_default覆盖),缓存区在系统负载不重的情况下可以增长到这个值,第三个值是发送缓存区空间的最大字节数(该值会被net.core.wmem_max覆盖).
根据实际测试,如果手工更改了net.ipv4.tcp_wmem的值,则会按更改的值来运行,否则在默认情况下,协议栈通常是按net.core.wmem_default和net.core.wmem_max的值来分配内存的.

应用程序应该根据应用的特性在程序中更改发送缓存大小:

socklen_t sendbuflen = 0;
socklen_t len = sizeof(sendbuflen);
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);
printf("default,sendbuf:%d\n", sendbuflen);

sendbuflen = 10240;
setsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, len);
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);
printf("now,sendbuf:%d\n", sendbuflen);


需要注意的是,虽然将发送缓存设置成了10k,但实际上,协议栈会将其扩大1倍,设为20k.
-------------------实例分析---------------

在实际应用中,如果发送端是非阻塞发送,由于网络的阻塞或者接收端处理过慢,通常出现的情况是,发送应用程序看起来发送了10k的数据,但是只发送了2k到对端缓存中,还有8k在本机缓存中(未发送或者未得到接收端的确认).那么此时,接收应用程序能够收到的数据为2k.假如接收应用程序调用recv函数获取了1k的数据在处理,在这个瞬间,发生了以下情况之一,双方表现为:

A. 发送应用程序认为send完了10k数据,关闭了socket:
发送主机作为tcp的主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack),并且,发送缓存中的8k数据并不清除,依然会发送给对端.如果接收应用程序依然在recv,那么它会收到余下的8k数据(这个前题是,接收端会在发送端FIN_WAIT1状态超时前收到余下的8k数据.), 然后得到一个对端socket被关闭的消息(recv返回0).这时,应该进行关闭.

B. 发送应用程序再次调用send发送8k的数据:
假如发送缓存的空间为20k,那么发送缓存可用空间为20-8=12k,大于请求发送的8k,所以send函数将数据做拷贝后,并立即返回8192;

假如发送缓存的空间为12k,那么此时发送缓存可用空间还有12-8=4k,send()会返回4096,应用程序发现返回的值小于请求发送的大小值后,可以认为缓存区已满,这时必须阻塞(或通过select等待下一次socket可写的信号),如果应用程序不理会,立即再次调用send,那么会得到-1的值, 在linux下表现为errno=EAGAIN.

C. 接收应用程序在处理完1k数据后,关闭了socket:
接收主机作为主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack).然后,发送应用程序会收到socket可读的信号(通常是 select调用返回socket可读),但在读取时会发现recv函数返回0,这时应该调用close函数来关闭socket(发送给对方ack);

如果发送应用程序没有处理这个可读的信号,而是在send,那么这要分两种情况来考虑,假如是在发送端收到RST标志之后调用send,send将返回-1,同时errno设为ECONNRESET表示对端网络已断开,但是,也有说法是进程会收到SIGPIPE信号,该信号的默认响应动作是退出进程,如果忽略该信号,那么send是返回-1,errno为EPIPE(未证实);如果是在发送端收到RST标志之前,则send像往常一样工作;

以上说的是非阻塞的send情况,假如send是阻塞调用,并且正好处于阻塞时(例如一次性发送一个巨大的buf,超出了发送缓存),对端socket关闭,那么send将返回成功发送的字节数,如果再次调用send,那么会同上一样.

D. 交换机或路由器的网络断开:
接收应用程序在处理完已收到的1k数据后,会继续从缓存区读取余下的1k数据,然后就表现为无数据可读的现象,这种情况需要应用程序来处理超时.一般做法是设定一个select等待的最大时间,如果超出这个时间依然没有数据可读,则认为socket已不可用.

发送应用程序会不断的将余下的数据发送到网络上,但始终得不到确认,所以缓存区的可用空间持续为0,这种情况也需要应用程序来处理.

如果不由应用程序来处理这种情况超时的情况,也可以通过tcp协议本身来处理,具体可以查看sysctl项中的:
net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time
<think>好的,用户现在想了解TCPsend函数的作用以及它在之前提供的代码片段中的具体使用方式。我需要结合之前的对话和用户提供的引用来给出详细的解释。 首先,用户的问题分为两部分:send函数的功能和在循环中的实现。之前的回答已经涉及了send函数的基本使用、错误处理以及循环发送的机制,但可能需要更系统地整理。 用户提供的引用中,引用[2]详细说明了send函数的参数和返回值,引用[3]和[4]强调了send函数将数据拷贝到发送缓冲区后立即返回,而不是等待数据发送到对端。而引用[1]则概述了TCP编程的流程,包括send的使用场景。 现在需要将send函数的功能和循环中的使用结合起来。用户给出的代码片段是一个循环发送数据的例子,其中包含错误处理和中断恢复机制。需要解释这段代码如何确保数据完整发送,处理EINTR错误,以及如何更新发送进度。 可能需要注意用户提到的“循环中的实现详解”,需要分步骤解释循环结构、错误处理逻辑、中断恢复以及数据更新的部分。同时,要引用之前的引用资料来支持解释,比如引用[3]和[4]中关于发送缓冲区的说明,说明为什么需要循环发送。 另外,用户可能对send函数在阻塞和非阻塞模式下的行为差异感兴趣,但问题中没有明确提到,是否需要提及?根据用户的问题,重点是在循环中的实现,所以可能不需要深入非阻塞模式,但可以简要提及。 需要确保回答结构清晰,分点说明,并引用提供的资料作为支持。同时,生成的相关问题需要与主题紧密相关,比如错误处理、阻塞与非阻塞的区别、缓冲区机制等。 最后,检查是否覆盖了用户的所有疑问:send的作用,代码中的循环结构如何工作,错误处理机制,以及数据完整性如何保证。同时确保引用正确的来源,如引用[2]、[3]、[4]来解释send的行为和缓冲区机制。</think>### TCP中`send`函数功能与循环实现详解 #### 一、`send`函数核心功能 `send`函数TCP协议实现数据可靠传输的核心接口,其核心功能可归纳为以下三点(基于引用[2][3][4]): 1. **缓冲区数据拷贝** ```c ssize_t send(int sockfd, const void *buf, size_t len, int flags); ``` - 将用户空间`buf`中的数据**拷贝到内核的发送缓冲区**(TCP协议栈维护) - 拷贝完成后立即返回,**不保证数据已到达对端** - 发送缓冲区大小可通过`SO_SNDBUF`套接字选项查询/设置 2. **返回值语义** - **>0**:实际拷贝到缓冲区的字节数(可能小于`len`,需循环发送) - **=0**:对端已关闭连接(需终止发送) - **-1**:错误发生,需通过`errno`判断错误类型 3. **流量控制** - 当发送缓冲区满时: - **阻塞模式**:线程挂起直到缓冲区空间释放 - **非阻塞模式**:立即返回`EAGAIN`/`EWOULDBLOCK` #### 二、代码片段中的关键实现 分析用户提供的代码逻辑(核心流程示意图): ```mermaid sequenceDiagram participant App as 应用程序 participant Kernel as 内核协议栈 App->>Kernel: send(fd,buf,BUFSIZE,0) alt 发送成功 Kernel-->>App: 返回实际发送字节数 else 系统中断(EINTR) App->>Kernel: 立即重试发送 else 其他错误 App->>App: 终止流程 end ``` 1. **循环发送机制** ```c while (1) { do { ret = send(fd, buf, BUFSIZ, 0); } while (ret < 0 && errno == EINTR); // 后续处理... } ``` - **外层`while(1)`**:确保发送完整报文(未处理部分代码需补充发送进度跟踪) - **内层`do-while`**:专用于处理**可恢复错误**(如信号中断) 2. **EINTR处理原理** - 产生条件:系统调用被信号中断(如`SIGALRM`、`SIGCHLD`等) - 处理方式:立即重新调用`send`(引用[3]指出必须处理此类错误以保证程序健壮性) 3. **数据完整性缺陷** - 当前代码**缺少已发送字节计数**,会导致: - 无法处理`send`部分成功的情况(返回值 < `BUFSIZ`) - 无法正确发送大体积数据(需要分块发送) - 改进方案: ```c size_t total_sent = 0; while (total_sent < data_len) { ssize_t ret = send(fd, buf + total_sent, data_len - total_sent, 0); // 错误处理... total_sent += ret; } ``` #### 三、TCP发送缓冲区工作机制 通过引用[3][4]说明的关键机制: 1. **协议栈处理流程** ```plaintext +----------------+ +-------------------+ | 应用层调用send | --> | 数据拷贝至发送缓冲区 | --> 内核协议栈负责 +----------------+ +-------------------+ 分片/超时重传等操作 ``` 2. **缓冲区状态影响** - **高水位线**:当缓冲数据量超过`SO_SNDLOWAT`时,`select`/`epoll`会报告可写事件 - **Nagle算法**:小数据包合并发送(可通过`TCP_NODELAY`禁用) #### 四、最佳实践建议 1. **错误处理模板** ```c ssize_t safe_send(int fd, const void* buf, size_t len) { size_t sent = 0; while (sent < len) { ssize_t ret = send(fd, (char*)buf + sent, len - sent, MSG_NOSIGNAL); if (ret == -1) { if (errno == EINTR) continue; // 被中断则重试 if (errno == EAGAIN || errno == EWOULDBLOCK) { // 非阻塞模式处理 wait_for_write_event(fd); continue; } return -1; // 不可恢复错误 } if (ret == 0) return sent; // 对端关闭连接 sent += ret; } return sent; } ``` 2. **性能优化方向** - 设置合理的`SO_SNDBUF`大小(建议值:带宽时延积BDP) - 批量发送数据避免小包(结合应用层协议设计) - 使用`writev`实现分散写(减少系统调用次数) --相关问题--: 1. 如何通过`ioctl`获取当前发送缓冲区可用空间? 2. `MSG_MORE`标记在`send`函数中的作用是什么? 3. TCP协议如何处理发送缓冲区中的数据丢失? 4. 为什么`send`返回成功不能说明数据已到达对端?[^3][^4]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值