Linux-TCP之深刻浅出send和recv

内容摘自:TCP之深刻浅出send和recv再次深刻理解TCP网络编程中的send和recvhtml

建议阅读时参考:Unix环境高级编程-TCP、UDP缓冲区node

概念web

  先明确一个概念:每一个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工做模式以及TCP的滑动窗口即是依赖于这两个独立的buffer以及此buffer的填充状态。接收缓冲区把数据缓存入内核,应用进程一直没有调用read进行读取的话,此数据会一直缓存在相应 socket的接收缓冲区内。再啰嗦一点,无论进程是否读取socket,对端发来的数据都会经由内核接收而且缓存到socket的内核接收缓冲区之中。 read所作的工做,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,仅此而已。进程调用send发送的数据的时候,最简单状况(也是通常状况),将数据拷贝进入socket的内核发送缓冲区之中,而后send便会在上层返回。换句话说,send返回之时,数据不必定会发送到对端去(和 write写文件有点相似),send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。后续我会专门用一篇文章介绍 read和send所关联的内核动做。每一个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来讲就是只要有数据就发,无论对方是否能够正确接收,因此不缓冲,不须要发送缓冲区。编程

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

  以上即是TCP可靠,UDP不可靠的实现。服务器

if(条件1){
    向buffer_last_modified填充协议内容“Last-Modified: Sat, 04 May 2012 05:28:58 GMT”;
    send(buffer_last_modified);
}
if(条件2){
    向buffer_expires填充协议内容“Expires: Mon, 14 Aug 2023 05:17:29 GMT”;
    send(buffer_expires);
}
if(条件N){
    向buffer_N填充协议内容“。。。”;
    send(buffer_N);
}

  对于这样的实现,当前的http应答在执行这段代码时,假设有M(M<=N)个条件都知足,那么会有连续的M个send调用,那是否是下层会依次向客户端发出M个TCP包呢?答案是否认的,包的数目在应用层是没法控制的,而且应用层也是不须要控制的网络

  用下列四个假设场景来解释一下这个答案:socket

    因为TCP是流式的,对于TCP而言,每一个TCP链接只有syn开始和fin结尾,中间发送的数据是没有边界的,多个连续的send所干的事情仅仅是:tcp

      假如socket的文件描述符被设置为阻塞方式,并且发送缓冲区还有足够空间容纳这个send所指示的应用层buffer的所有数据,那么把这些数据从应用层的buffer,拷贝到内核的发送缓冲区,而后返回。函数

      假如socket的文件描述符被设置为阻塞方式,可是发送缓冲区没有足够空间容纳这个send所指示的应用层buffer的所有数据,那么能拷贝多少就拷贝多少,而后进程挂起,等到TCP对端的接收缓冲区有空余空间时,经过滑动窗口协议(ACK包的又一个做用----打开窗口)通知TCP本端:“亲,我已经作好准备,您如今能够继续向我发送X个字节的数据了”,而后本端的内核唤醒进程,继续向发送缓冲区拷贝剩余数据,而且内核向TCP对端发送TCP数据,若是send所指示的应用层buffer中的数据在本次仍然没法所有拷贝完,那么过程重复。。。直到全部数据所有拷贝完,返回。请注意,对于send的行为,我用了“拷贝一次”,send和下层是否发送数据包,没有任何关系。

      假如socket的文件描述符被设置为非阻塞方式,并且发送缓冲区还有足够空间容纳这个send所指示的应用层buffer的所有数据,那么把这些数据从应用层的buffer,拷贝到内核的发送缓冲区,而后返回。

      假如socket的文件描述符被设置为非阻塞方式,可是发送缓冲区没有足够空间容纳这个send所指示的应用层buffer的所有数据,那么能拷贝多少就拷贝多少,而后返回拷贝的字节数。多涉及一点,返回以后有两种处理方式:

        1.死循环,一直调用send,持续测试,一直到结束(基本上不会这么搞)。

        2.非阻塞搭配epoll或者select,用这两种东西来测试socket是否达到可发送的活跃状态,而后调用send(高性能服务器必需的处理方式)。

    综上,以及请参考本文前述的SO_RCVBUF和SO_SNDBUF,你会发现,在实际场景中,你能发出多少TCP包以及每一个包承载多少数据,除了受到自身服务器配置和环境带宽影响,对端的接收状态也能影响你的发送情况。

 

  至于为何说“应用层也是不须要控制发送行为的”,这个说法的缘由是:

    软件系统分层处理、分模块处理各类软件行为,目的就是为了各司其职,分工。应用层只关心业务实现,控制业务。数据传输由专门的层面去处理,这样应用层开发的规模和复杂程度会大为下降,开发和维护成本也会相应下降。

  

  再回到发送的话题上来:)以前说应用层没法精确控制和彻底控制发送行为,那是否是就是不控制了?非也!虽然没法控制,但也要尽可能控制!

  如何尽可能控制?如今引入本节主题----TCP_CORK和TCP_NODELAY。

    cork:塞子,塞住

    nodelay:不要延迟

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

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

  很明显,两个选项是互斥的。实际场景中该怎么选择这两个选项呢?再次举例说明

    webserver,,下载服务器(ftp的发送文件服务器),须要带宽量比较大的服务器,用TCP_CORK。

    涉及到交互的服务器,好比ftp的接收命令的服务器,必须使用TCP_NODELAY。默认是TCP_CORK。设想一下,用户每次敲几个字节的命令,而下层在攒这些数据,想等到数据量多了再发送,这样用户会等到发疯。这个糟糕的场景有个专门的词汇来形容-----粘(nian拼音二声)包

 

接下来咱们用一个测试机上的阻塞socket实例来讲明主题。文章中全部图都是在测试系统上现截取的。

须要理解的3个概念

1. TCP socket的buffer

  每一个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工做模式以及TCP的流量(拥塞)控制即是依赖于这两个独立的buffer以及buffer的填充状态。接收缓冲区把数据缓存入内核,应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。再啰嗦一点,无论进程是否调用recv()读取socket,对端发来的数据都会经由内核接收而且缓存到socket的内核接收缓冲区之中。recv()所作的工做,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回,仅此而已。进程调用send()发送的数据的时候,最简单状况(也是通常状况),将数据拷贝进入socket的内核发送缓冲区之中,而后send便会在上层返回。换句话说,send()返回之时,数据不必定会发送到对端去(和write写文件有点相似),send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止。对于TCP,若是应用进程一直没有读取,接收缓冲区满了以后,发生的动做是:收端通知发端,接收窗口关闭(win=0)。这个即是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。由于对方不容许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,若是对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
  查看测试机的socket发送缓冲区大小,如图1所示

图1

  第一个值是一个限制值,socket发送缓存区的最少字节数;
  第二个值是默认值;
  第三个值是一个限制值,socket发送缓存区的最大字节数;
  根据实际测试,发送缓冲区的尺寸在默认状况下的全局设置是16384字节,即16k。
  在测试系统上,发送缓存默认值是16k。
  proc文件系统下的值和sysctl中的值都是全局值,应用程序可根据须要在程序中使用setsockopt()对某个socket的发送缓冲区尺寸进行单独修改,详见文章《TCP选项之SO_RCVBUF和SO_SNDBUF》,不过这都是题外话。


2. 接收窗口(滑动窗口)

  TCP链接创建之时的收端的初始接受窗口大小是14600,细节如图2所示(129是收端,130是发端)

  图2

  接收窗口是TCP中的滑动窗口,TCP的收端用这个接受窗口----win=14600,通知发端,我目前的接收能力是14600字节。
后续发送过程当中,收端会不断的用ACK(ACK的所有做用请参照博文《TCP之ACK发送情景》)通知发端本身的接收窗口的大小状态,如图3,而发端发送数据的量,就根据这个接收窗口的大小来肯定,发端不会发送超过收端接收能力的数据量。这样就起到了一个流量控制的的做用。

图3

  图3说明
    21,22两个包都是收端发给发端的ACK包
    第21个包,收端确认收到的前7240个字节数据,7241的意思是指望收到的包从7241号开始,序号加了1.同时,接收窗口从最初的14656(如图2)通过慢启动阶段增长到了如今的29120。用来代表如今收端能够接收29120个字节的数据,而发端看到这个窗口通告,在没有收到新的ACK的时候,发端能够向收端发送29120字节这么多数据。
    第22个包,收端确认收到的前8688个字节数据,并通告本身的接收窗口继续增加为32000这么大。


3. 单个TCP的负载量和MSS的关系

  MSS在以太网上一般大小是1460字节,而咱们在后续发送过程当中的单个TCP包的最大数据承载量是1448字节,这两者的关系能够参考博文《TCP之1460MSS和1448负载》。


  实例详解send()

    实例功能说明:接收端129做为客户端去链接发送端130,链接上以后并不调用recv()接收,而是sleep(1000),把进程暂停下来,不让进程接收数据。内核会缓存数据至接收缓冲区。发送端做为服务器接收TCP请求以后,当即用ret = send(sock,buf,70k,0);这个C语句,向接收端发送70k数据。
咱们如今来观察这个过程。看看究竟发生了些什么事。wireshark抓包截图以下图4


图4

    图4说明,包序号等同于时序
       1. 客户端sleep在recv()以前,目的是为了把数据压入接收缓冲区。服务端调用"ret = send(sock,buf,70k,0);"这个C语句,向接收端发送70k数据。因为发送缓冲区大小16k,send()没法将70k数据所有拷贝进发送缓冲区,故先拷贝16k进入发送缓冲区,下层发送缓冲区中有数据要发送,内核开始发送。上层send()在应用层处于阻塞状态;
      2. 11号TCP包,发端从这儿开始向收端发送1448个字节的数据;
      3. 12号TCP包,发端没有收到以前发送的1448个数据的ACK包,仍然继续向收端发送1448个字节的数据;
      4. 13号TCP包,收端向发端发送1448字节的确认包,代表收端成功接收总共1448个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入1448字节。因为处于慢启动状态,win接收窗口持续增大,代表接受能力在增长,吞吐量持续上升;
      5. 14号TCP包,收端向发端发送2896字节的确认包,代表收端成功接收总共2896个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入2896字节。因为处于慢启动状态,win接收窗口持续增大,代表接受能力在增长,吞吐量持续上升;
      6. 15号TCP包,发端继续向收端发送1448个字节的数据;
      7. 16号TCP包,收端向发端发送4344字节的确认包,代表收端成功接收总共4344个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入4344字节。因为处于慢启动状态,win接收窗口持续增大,代表接受能力在增长,吞吐量持续上升;
      8. 从这儿开始,我略去不少包,过程相似上面过程。同时,因为不断的发送出去的数据被收端用ACK确认,发送缓冲区的空间被逐渐腾出空地,send()内部不断的把应用层buf中的数据向发送缓冲区拷贝,从而不断的发送,过程重复。70k数据并无被彻底送入内核,send()无论是否发送出去,send无论发送出去的是否被确认,send()只关心buf中的数据有没有被所有送往内核发送缓冲区。若是buf中的数据没有被所有送往内核发送缓冲区,send()在应用层阻塞,负责等待内核发送缓冲区中有空余空间的时候,逐步拷贝buf中的数据;若是buf中的数据被所有拷入内核发送缓冲区,send()当即返回
      9. 通过慢启动阶段接收窗口增大到稳定阶段,TCP吞吐量升高到稳定阶段,收端一直处于sleep状态,没有调用recv()把内核中接收缓冲区中的数据拷贝到应用层去,此时收端的接收缓冲区中被压入大量数据;
      10. 66号、67号TCP数据包,发端继续向收端发送数据;
      11. 68号TCP数据包,收端发送ACK包确认接收到的数据,ACK=62265代表收端已经收到62265字节的数据,这些数据目前被压在收端的接收缓冲区中。win=3456,比较以前的16号TCP包的win=23296,代表收端的窗口已经处于收缩状态,收端的接收缓冲区中的数据迟迟未被应用层读走,致使接收缓冲区空间吃紧,故收缩窗口,控制发送端的发送量,进行流量控制;
      12. 69号、70号TCP数据包,发端在接收窗口容许的数据量的范围内,继续向收端发送2段1448字节长度的数据;
      13. 71号TCP数据包,至此,收端已经成功接收65160字节的数据,所有被压在接收缓冲区之中,接收窗口继续收缩,尺寸为1600字节;
      14. 72号TCP数据包,发端在接收窗口容许的数据量的范围内,继续向收端发送1448字节长度的数据;
      15. 73号TCP数据包,至此,收端已经成功接收66609字节的数据,所有被压在接收缓冲区之中,接收窗口继续收缩,尺寸为192字节。
      16. 74号TCP数据包,和咱们这个例子没有关系,是别的应用发送的包;
      17. 75号TCP数据包,发端在接收窗口容许的数据量的范围内,向收端发送192字节长度的数据;
      18. 76号TCP数据包,至此,收端已经成功接收66609字节的数据,所有被压在接收缓冲区之中,win=0接收窗口关闭,接收缓冲区满,没法再接收任何数据;
      19. 77号、78号、79号TCP数据包,由keepalive触发的数据包,响应的ACK持有接收窗口的状态win=0,另外,ACK=66801代表接收端的接收缓冲区中积压了66800字节的数据。
      20. 从以上过程,咱们应该熟悉了滑动窗口通告字段win所说明的问题,以及ACK确认数据等等。如今可得出一个结论,接收端的接收缓存尺寸应该是66800字节(此结论并不是本篇主题)。
      send()要发送的数据是70k,如今发出去了66800字节,发送缓存中还有16k,应用层剩余要拷贝进内核的数据量是N=70k-66800-16k。接收端仍处于sleep状态,没法recv()数据,这将致使接收缓冲区一直处于积压满的状态,窗口会一直通告0(win=0)。发送端在这样的状态下完全没法发送数据了,send()的剩余数据没法继续拷贝进内核的发送缓冲区,最终致使send()被阻塞在应用层
      21. send()一直阻塞中。。。

    图4和send()的关系说明完毕。


  那何时send返回呢?有3种返回场景

  send()返回场景

    场景1,咱们继续图4这个例子,不过这儿开始咱们就跳出图4所示的过程了

      22. 接收端sleep(1000)到时间了,进程被唤醒,代码片断如图5

图5

      随着进程不断的用"recv(fd,buf,2048,0);"将数据从内核的接收缓冲区拷贝至应用层的buf,在使用win=0关闭接收窗口以后,如今接收缓冲区又逐渐恢复了缓存的能力,这个条件下,收端会主动发送携带"win=n(n>0)"这样的ACK包去通告发送端接收窗口已打开
      23. 发端收到携带"win=n(n>0)"这样的ACK包以后,开始继续在窗口运行的数据量范围内发送数据。发送缓冲区的数据被发出;
      24. 收端继续接收数据,并用ACK确认这些数据;
      25. 发端收到ACK,能够清理出一些发送缓冲区空间,应用层send()的剩余数据又能够被不断的拷贝进内核的发送缓冲区;
      26. 不断重复以上发送过程;
      27. send()的70k数据所有进入内核,send()成功返回。

    场景2,咱们继续图4这个例子,不过这儿开始咱们就跳出图4所示的过程了
      22. 收端进程或者socket出现问题,给发端发送一个RST,请参考博文《》;
      23. 内核收到RST,send返回-1。

    场景3,和以上例子不要紧
      链接上以后,立刻send(1k),这样,发送的数据确定能够一次拷贝进入发送缓冲区,send()拷贝完数据当即成功返回。


send()发送结论

  其实场景1和场景2说明一个问题
  send()只是负责拷贝,拷贝完当即返回,不会等待发送和发送以后的ACK。若是socket出现问题,RST包被反馈回来。在RST包返回之时,若是send()尚未把数据所有放入内核或者发送出去,那么send()返回-1,errno被置错误值;若是RST包返回之时,send()已经返回,那么RST致使的错误会在下一次send()或者recv()调用的时候被当即返回
  场景3彻底说明send()只要完成拷贝就成功返回,若是发送数据的过程当中出现各类错误,下一次send()或者recv()调用的时候被当即返回。


概念上容易疑惑的地方

  1. TCP协议自己是为了保证可靠传输,并不等于应用程序用tcp发送数据就必定是可靠的,必需要容错;
  2. send()和recv()没有固定的对应关系,不定数目的send()能够触发不定数目的recv(),这话不专业,可是仍是必须说一下,初学者容易疑惑;
  3. 关键点,send()只负责拷贝,拷贝到内核就返回,我通篇在说拷贝完返回,不少文章中说send()在成功发送数据后返回,成功发送是说发出去的东西被ACK确认过。send()只拷贝,不会等ACK
  4. 这次send()调用所触发的程序错误,可能会在本次返回,也可能在下次调用网络IO函数的时候被返回

 

实际上理解了阻塞式的,就能理解非阻塞的。

相关文章
相关标签/搜索