深入理解tcp网络编程中的send和recv

深入理解tcp网络编程中的send和recv

每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer以及此buffer的填充状态。

接收缓冲区把数据缓存入内核,应用进程一直没有调用read进行读取的话,此数据会一直缓存在相应 socket的接收缓冲区内。即不管进程是否读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。 read所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,仅此而已。

进程调用send发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send返回之时,数据不一定会发送到对端去(和 write写文件有点类似),send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。

每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。

 接收缓冲区被TCP和UDP用来缓存网络上来的数据,一直保存到应用进程读走为止。

  • 对于TCP,如果应用进程一直没有读取,buffer满了之后,发生的动作是:通知对端TCP协议中的窗口关闭。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。 
  • UDP:当套接口接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就被丢弃。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的UDP丢弃数据报。

TCP_CORK与TCP_NODELAY这两个选项是互斥的,打开或者关闭TCP的nagle算法。

TCP_CORK:尽量向发送缓冲区中攒数据,攒到多了再发送,这样网络的有效负载会升高。简单粗暴地解释一下这个有效负载的问题。假如每个包中只有一个字节的数据,为了发送这一个字节的数据,再给这一个字节外面包装一层厚厚的TCP包头,那网络上跑的几乎全是包头了,有效的数据只占其中很小的部分,很多访问量大的服务器,带宽可以很轻松的被这么耗尽。那么,为了让有效负载升高,我们可以通过这个选项指示TCP层,在发送的时候尽量多攒一些数据,把他们填充到一个TCP包中再发送出去。这个和提升发送效率是相互矛盾的,空间和时间总是一堆冤家!

TCP_NODELAY:尽量不要等待,只要发送缓冲区中有数据,并且发送窗口是打开的,就尽量把数据发送到网络上去。

三个概念

  • TCP socket的buffer
  • 滑动窗口
  • 单个TCP负载量和MSS的关系:以太网中MSS大小通常为1460字节

send发送结论

send()只是负责拷贝,拷贝完立即返回,不会等待发送和发送之后的ACK。如果socket出现问题,RST包被反馈回来。在RST包返回之时,如果send()还没有把数据全部放入内核或者发送出去,那么send()返回-1,errno被置错误值;如果RST包返回之时,send()已经返回,那么RST导致的错误会在下一次send()或者recv()调用的时候被立即返回。

send()只要完成拷贝就成功返回,如果发送数据的过程中出现各种错误,下一次send()或者recv()调用的时候被立即返回。

TCP协议本身是为了保证可靠传输,并不等于应用程序用tcp发送数据就一定是可靠的,必须要容错;

send()和recv()没有固定的对应关系,不定数目的send()可以触发不定数目的recv();

 关键点,send()只负责拷贝,拷贝到内核就返回,此次send()调用所触发的程序错误,可能会在本次返回,也可能在下次调用网络IO函数的时候被返回。

recv方法

 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

当应用程序调用recv接收数据的时候,recv函数会等待sockfd中发送数据的缓冲区的协议发送完数据,如果在等待过程中出现网络错误,则会返回SOCKET_ERROR。如果sockfd中的缓冲区中没有数据或者协议已经发送完数据,则recv会检查sockfd的接受缓冲区,如果该缓冲区正在接受数据,则recv会一直等待,知道缓冲区接受数据完毕,之后recv将数据从缓冲区拷贝一份值buf中,数据通过协议转发的,recv只是将数据从缓冲区拷贝过来。注,如果recv在拷贝数据时出现错误,则返回SOCKET_ERROT,如果在协议传输数据中出现网络错误,则返回0。

注意:如果recv在拷贝数据时出现错误,则返回SOCKET_ERROT,如果在协议传输数据中出现网络错误,则返回0。

阻塞与非阻塞recv返回值没有区别,都是:

  • <0:出错
  • =0:对方调用close,关闭连接
  • >0:接收到数据的大小

特别地:返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。

只是阻塞模式下recv会一直阻塞直到接收到数据,非阻塞模式下如果没有数据就会返回,不会阻塞着读,因此需要循环读取。

返回说明:

  • 成功执行时,返回接收到的字节数。
  • 若另一端已关闭连接则返回0,这种关闭是对方主动且正常的关闭
  • 失败返回-1,errno被设为以下的某个值 
  1. EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
  2. EBADF:sock不是有效的描述词
  3. ECONNREFUSE:远程主机阻绝网络连接
  4. EFAULT:内存空间访问出错
  5. EINTR:操作被信号中断
  6. EINVAL:参数无效
  7. ENOMEM:内存不足
  8. ENOTCONN:与面向连接关联的套接字尚未被连接上
  9. ENOTSOCK:sock索引的不是套接字

 对recv方法来说,将接收缓冲区中的数据拷贝至应用层的缓冲区中,当应用缓冲区满或者接受缓冲区数据接收完,就会返回。如果将接受缓冲区大小设置为0,那么该方法会直接从协议中的滑动窗口中获取数据。要么缓冲区接收满为止。要么当push标志位1的时候 ,recv返回实际接收的数据大小。

协议层收到TCP数据包后(保存在滑动窗口区),本方的滑动窗口合拢(窗口值减小);当协议层将数据拷贝到接收缓冲区(滑动窗口区—>接收缓冲区),或者应用层调用recv接收数据(接收缓冲区—>应用层缓冲区,滑动窗口区—>应用层缓冲区)后,本方的滑动窗口张开(窗口值增大)。收到数据更新window后,协议层向对方发送ACK确认。
 

send方法

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

 当程序使用send方法的时候,send会首先检查协议sockfd中的发送缓冲区中是否有数据发送,send会比较发送数据的buf长度和sockfd发送数据的缓冲区长度,如果len大于sockfd的发送长度,则send返回SOCKET_ERROR,如果发送缓冲区的大小足够,则将数据buf中的数据发送至发送缓冲区中,确认send函数将数据拷贝至发送换区中,另外,如果send检测发送缓冲区有数据但是还未发送,就比较该缓冲区的剩余空间和和len的大小,如果len大于剩余空间,就一直等待,直到缓冲区中的数据发送玩为止,如果len<缓冲区剩余的大小,就将发送的数据拷贝至该缓冲区中,如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。(send函数只是将数据拷贝至发送缓冲中,就返回,此刻数据不一定发送至接收端)。


总结,不管是send还是recv方法,都是数据的缓冲区和发送的缓冲区的拷贝操作过程,真正发送数据的是协议功能,注意三种返回值的可能性,>0表示成功,返回实际发送或接受的字节数,=0表示超时,对方主动关闭了连接过程,<0出错,此种情况可能出现过重情况,如上所示。其中errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN这三种是特殊情况,实际使用中表示继续正常接受数据即可。

对于send方法,将需要发送的数据拷贝至发送缓冲区,否则进入阻塞或者进入超时等待。如果改变这种状态,将发送缓冲区大小设置为0,这样,当send方法返回是,所发送的数据就都到达目的机器。但是,只是到达目标服务器的接受缓冲区,并不保证数据以被应用层所接收。另外, 在发送数据时,协议根据滑动窗口和MSS值来确定tcp报文段的数据字段大小,这样就能保证接收缓冲区不会溢出。如果接收方的滑动窗口为0,但是发送方还有数据尚未发送完成,就是用探测机制,一方面检测对方方的滑动窗口的大小变化(探测机制是通过每次发送一个字节来进行检测,由先前的30s到之后的1分钟,最终达到2分钟间隔)),另一方面检测对方的连接是否异常。

     push标志指示接收端应尽快将数据提交给应用层。如果send函数提交的待发送数据量较小,例如小于1460B(参照MSS值确定),那么协议层会将该报文中的TCP头部的push字段置为1;如果待发送的数据量较大,需要拆成多个数据段发送时,协议层只会将最后一个分段报文的TCP头部的push字段置1。
 

 

 

 

 

 

 

 

 

 

 

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值