1、缓冲区简介
      每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

      write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
      read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

    I/O缓冲区在每个TCP套接字中单独存在;
    I/O缓冲区在创建套接字时自动生成;
    即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
    关闭套接字将丢失输入缓冲区中的数据。
    2、使用write()/send()发送数据
    【阻塞模式下】:

    首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据;

    如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒;

    如果要写入的数据大于缓冲区的最大长度,那么将分批写入。如果要写入的数据大于缓冲区的最大长度,那么将分批写入;直到所有数据被写入缓冲区 write()/send() 才能返回。直到所有数据被写入缓冲区 write()/send() 才能返回。

    send()函数默认情况下会使用Nagle算法。Nagle算法通过将未确认的数据存入缓冲区直到积攒到一定数量一起发送的方法,来降低主机发送零碎小数据包的数目。所以假设send()函数发送数据过快的话,该算法会将一些数据打包后统一发出去。通过setsockopt()的TCP_NODELAY选项来禁用Nagle算法。

    【非阻塞模式下】:

    send()函数的过程仅仅是将数据拷贝到协议栈的缓冲区而已,如果缓冲区可用空间不够,则尽可能拷贝,返回成功拷贝的大小;如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN。
    3、使用read()/recv()读取数据
    【阻塞模式下】:

    首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来;

    如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

    【非阻塞模式下】:

    接收数据时perror时常遇到“Resource temporarilyunavailable”的提示,errno代码为11(EAGAIN)。这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,继续循环接着recv就可以。
    4、系统调用read()的返回错误场景
    EINTR:在读取到数据以前调用被信号所中断。

    EAGAIN:使用 O_NONBLOCK 标志指定了非阻塞式输入输出,但当前没有数据可读或者使用了阻塞操作。

    EIO:输入输出错误.可能是正处于后台进程组进程试图读取其控制终端,但读操作无效,或者被信号SIGTTIN所阻塞,或者其进程组是孤儿进程组.也可能执行的是读磁盘或者磁带机这样的底层输入输出错误。

    EISDIR:fd 指向一个目录。

    EBADF:fd 不是一个合法的文件描述符,或者不是为读操作而打开。

    EINVAL:fd 所连接的对象不可读。

    EFAULT:buf 超出用户可访问的地址空间。

    EWOULDBLOCK:用于非阻塞模式,表示不需要重新读或者写。

    五、面试题—>TCP服务端一直sleep,客户端发送数据问题
    1、TCP发送数据的过程
      TCP发送数据的大体过程:首先,TCP是有链接的可靠传输协议,所谓可靠也就是说保证客户端发送的数据服务端都能够收到,并且是按序收到。那么对于上面的问题就不可能存在数据的丢弃。那么客户端一直发送数据越来越多怎么办?下面我们分析一下TCP的传输过程。

    1. 数据首先由应用程序缓冲区复制到发送端的输出缓冲区(位于内核),注意这个过程是用类似write功能的函数完成的。有的人通常看到write成功就以为数据发送到了对端主机,其实这是错误的,write成功仅仅表示数据成功的由应用进程缓冲区复制到了输出缓冲区。

    2. 然后内核协议栈将输出缓冲区中的数据发送到对端主机,注意这个过程不受应用程序控制,而是发送端内核协议栈完成,其中包括使用滑动窗口、拥塞控制等功能。

    3. 数据到达接收端主机的输入缓冲区,注意这个接收过程也不受应用程序控制,而是由接收端内核协议栈完成,其中包括发送ack确认等。

    4. 数据由套接字接收缓冲区复制到接收端应用程序缓冲区,注意这个过程是由类似read等函数来完成。

    2、阻塞方式的情况
      阻塞方式下,如果服务端一直sleep不接收数据,而客户端一直write,也就是只能执行上述过程中的前三步,这样最终结果肯定是接收端的输入缓冲区和发送端的输出缓冲区都被填满,这样write就无法继续将数据从应用程序复制到发送端的输出缓冲区了,从而使进程进入睡眠。

    3、非阻塞方式的情况
      非阻塞情况下,服务端一直sleep,客户端一直write数据的结果:开始客户端write成功,随着客户端write,接收端的输入缓冲区和发送端的输出缓冲区会被填满。当发送端的输出缓冲区的可用空间小于write请求写的字节数时,write立即返回-1,并将errno置为EWOULDBLOCK。

    image.png