socket缓冲区以及阻塞模式详解
每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
send()/recv()并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于send()/recv()函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
send()/recv()函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
TCP套接字的I/O缓冲区示意图
这些I/O缓冲区特性可整理如下:
- I/O缓冲区在每个TCP套接字中单独存在;
- I/O缓冲区在创建套接字时自动生成;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
默认的套节字缓冲区大小可能不够用。因此我们可以想办法去修改默认缓存区大小,改成一个合适的值。
我们使用的方法是利用套节字对象的setsocketopt( )的方法修改默认的套节字缓冲区大小。
首先我们定义两个常量:SEND_BUF_SIZE和RECV_BUF_SIZE。然后在一个函数中调用套节字的实例setsocketopt( )方法。以下是具体代码:
1 | SEND_BUF_SIZE = 4096 |
在套节字对象可调用方法getsocketopt( )和setsocketopt( )分别获取和修改套节字对象属性。
setsocketopt( )方法接收三个参数:level、optname和value。其中optname是选项名、value是该选项的值、level代表选项所在的协议层,以下列出了level常用的符号常量(so_*等)的意义。
在套节字对象可调用方法getsocketopt( )和setsocketopt( )分别获取和修改套节字对象属性。
setsocketopt( )方法接收三个参数:level、optname和value。其中optname是选项名、value是该选项的值、level代表选项所在的协议层,以下列出了level常用的符号常量(so_*等)的意义。
选项 意义
SO_BROADCAST 允许套接口传送广播信息
SO_DEBUG 记录调试信息
SO_DONTLINER 不要因为数据未发送就阻塞关闭操作
SO_DONTROUTE 禁止选径;直接传送
SO_KEEPALⅣE 发送“保持活动”包
SO_LINGER struct linger FAR* 如关闭时有未发送数据,则逗留
SO_OOBINLINE 在常规数据流中接收带外数据
SO_RCVBUF 为接收确定缓冲区大小
SO_REUSEADDR 允许套接口和一个已在使用中的地址捆绑(参见bind())
SO_SNDBUF 指定发送缓冲区大小
TCP_NODELAY 禁止发送合并的Nagle算法
阻塞模式
对于TCP套接字默认情况下,当使用send()发送数据时。
1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 send() 函数继续写入数据。
2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,send() 也会被阻塞,直到数据发送完毕缓冲区解锁,send() 才会被唤醒。
3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
4) 直到所有数据被写入缓冲区send() 才能返回。
当使用recv() 读取数据时:
1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 recv() 函数再次读取。
3) 直到读取到数据后recv() 函数才会返回,否则就一直被阻塞。
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。