深入浅出TCP之send与recv

网络技术是从1990年代中期发展起来的新技术 , 它把互联网上分散的资源融为有机整体 , 实现资源的全面共享和有机协作 , 使人们能够透明地使用资源的整体能力并按需获取信息 。资源包括高性能计算机、存储资源、数据资源、信息资源、知识资源、专家资源、大型数据库、网络、传感器等 。当前的互联网只限于信息共享 , 网络则被认为是互联网发展的第三阶段 。先明确一个概念:每个TCPsocket在内核中都有一个发送缓冲区和一个接收缓冲区 , TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer以及此buffer的填充状态 。接收缓冲区把数据缓存入内核 , 应用进程一直没有调用read进行读取的话 , 此数据会一直缓存在相应socket的接收缓冲区内 。再啰嗦一点 , 不管进程是否读取socket , 对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中 。read所做的工作 , 就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面 , 仅此而已 。进程调用send发送的数据的时候 , 最简单情况(也是一般情况) , 将数据拷贝进入socket的内核发送缓冲区之中 , 然后send便会在上层返回 。换句话说 , send返回之时 , 数据不一定会发送到对端去(和write写文件有点类似) , send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中 。后续我会专门用一篇文章介绍read和send所关联的内核动作 。每个UDPsocket都有一个接收缓冲区 , 没有发送缓冲区 , 从概念上来说就是只要有数据就发 , 不管对方是否可以正确接收 , 所以不缓冲 , 不需要发送缓冲区 。
接收缓冲区被TCP和UDP用来缓存网络上来的数据 , 一直保存到应用进程读走为止 。对于TCP , 如果应用进程一直没有读取 , buffer满了之后 , 发生的动作是:通知对端TCP协议中的窗口关闭 。这个便是滑动窗口的实现 。保证TCP套接口接收缓冲区不会溢出 , 从而保证了TCP是可靠传输 。因为对方不允许发出超过所通告窗口大小的数据 。这就是TCP的流量控制 , 如果对方无视窗口大小而发出了超过窗口大小的数据 , 则接收方TCP将丢弃它 。UDP:当套接口接收缓冲区满时 , 新来的数据报无法进入接收缓冲区 , 此数据报就被丢弃 。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者 , 导致接收方的UDP丢弃数据报 。
以上便是TCP可靠 , UDP不可靠的实现 。
TCP_CORK TCP_NODELAY
这两个选项是互斥的 , 打开或者关闭TCP的nagle算法 , 下面用场景来解释
典型的webserver向客户端的应答 , 应用层代码实现流程粗略来说 , 一般如下所示:
if(条件1){
向buffer_last_modified填充协议内容“Last-Modified: Sat, 04 May 2012 05:28:58GMT”;
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);
}
【深入浅出TCP之send与recv】对于这样的实现 , 当前的http应答在执行这段代码时 , 假设有M(M<=N)个条件都满足 , 那么会有连续的M个send调用 , 那是不是下层会依次向客户端发出M个TCP包呢?答案是否定的 , 包的数目在应用层是无法控制的 , 并且应用层也是不需要控制的 。
我用下列四个假设场景来解释一下这个答案
由于TCP是流式的 , 对于TCP而言 , 每个TCP连接只有syn开始和fin结尾 , 中间发送的数据是没有边界的 , 多个连续的send所干的事情仅仅是:
假如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拼音二声)包 。
原文博客:http://blog.chinaunix/uid-29075379-id-3895700.html

    推荐阅读