网络编程(13)高级IO函数 (2)排队的数据量

本章笼统归为“高级I/O”的各个函数和技术,包含三部分内容。第一,在I/O操作上设置超时,有三种方法;第二,read与write函数的变体,recv与send、readv与writev、recvmsg与sendmsg。第三,确定套接字缓冲区数据量和其他有关说明。

这里说明第三个方法,前两个方法见博客 网络编程(13)高级IO函数



有时候需在不正真读取数据的前提下,要知道套接字或文件描述符上已有多少数据排队等着读取。有三种方式获悉已排队数据量。

5.1 使用非阻塞I/O

如果获悉已排队数据量的目的在于避免读操作阻塞在内核中,即没有数据可读时能做其他事情。这种情况值仅需要知道是否有数据而关心具体排队数量时,可以使用非阻塞IO。详细介绍在后面章节讨论。

5.2 使用MSG_PEEK标志的系统调用

既想查看数据,又想数据留在接收队列以供进程中其他业务部分稍后读取,可以使用MSG_PEEK标志。注意,设定recv或recvfrom等系统调用的flags不能肯定对否真有数据可读,可以结合非阻塞套接字使用,也可以组合使用MSG_DONTWAIT标志。
另外,对于流式套接字TCP和数据报套接字UDP,先后两次调用recv或recvfrom的结果有一定差异,下面分情况说明。

5.2.1 获取UDP套接字上的待读取数量

假设UDP套接字接收队列中已有一个数据,如果第一次加上该标志调用recv/recvfrom一次,接着不加该标志再调用recv/recvfrom一次,即使另有数据报在这两次调用中间加入该套接字的接收队列,这两个返回值也完全相同。

1)以recv为例的udp服务端,创建和bindsocket之后,给出简单接收数据部分代码

int len ;
char buf[100]={}; 

while (1)
{
  // 接收
  len = ::recv(socket_fd, buf, sizeof(buf), MSG_PEEK);
  if(len < 0){  
    LOG("recv failed. %s", strerror(errno));
    break;
  }
  LOG("recv (%d) one: %s",len, buf);

  sleep(5); // 在第二次调用recv之前客户端继续发送数据

  int buf2[100]={};
  len = ::recv(socket_fd, buf2, sizeof(buf2), 0);
  if(len < 0){  
    LOG("recv failed. %s", strerror(errno));
    break;
  }
  LOG("recv (%d) two: %s\n",len, buf2);

  break;
}

第一次发送3个字符,服务端收到进入中间sleep(5),第二次发送6个字符,服务端sleep结束后结果不变,仍然为3个字符。如下截图
在这里插入图片描述
(2)根据第一次调用带MSG_PEEK标记的结果,第二次调用读取指定数量数据

int len ;
char buf[100]={}; 

while (1)
{
  // 接收
  len = ::recv(socket_fd, buf, sizeof(buf), MSG_PEEK);  //阻塞以等待数据到来
  if(len < 0){  
    LOG("recv failed. %s", strerror(errno));
    break;
  }
  LOG("recv (%d) one: %s",len, buf);

  int buf2[100]={};
  len = ::recv(socket_fd, buf2, len, 0); // 读取已就绪的数据,长度为len
  if(len < 0){  
    LOG("recv failed. %s", strerror(errno));
    break;
  }
  LOG("recv (%d) two: %s\n",len, buf2);
}

实际测试时,发现recv(socket_fd, buf, sizeof(buf), MSG_PEEK);会阻塞直到接收到数据,并返回队列中的数据长度。若不执行recv(socket_fd, buf2, len, 0);读取指定长度数据,再次调用recv(socket_fd, buf, sizeof(buf), MSG_PEEK);会立即返回队列中的待读取数据。
先使用指定MSG_PEEK标志的读取当前套接字上就绪的数据量,第二次不指定该标志读取队列中的就绪数据。
在这里插入图片描述
5.2.2 获取TCP套接字上的待读取数量
不同于udp,两次recv之间若有新的数据进来,第二次调用recv会返回更新后就绪数据的总长度。

(1) 以TCP服务端为例,给出连续调用两次使用MSG_PEEK标志的recv

服务端代码两次调用recv之间调用一次sleep(5)。客户端发送4个字节数据,服务端收到后进入sleep(5),客户端立即再发送2个字节,服务端第二次调用recv结果为6个字符,而不同于UDP时的6个字符。
在这里插入图片描述
(2)若一直没有读取队列中就绪数据,函数将返回队列中最新的就绪数据长度

例如修改服务端每个一秒调用一次带标记的recv
在这里插入图片描述
(3) 先调用带标志的recv一次,再调用不带标志的recv一次

第一次调用获取队列中已就绪的数据长度,第二次调用读取指定长度的数据。结果同udp。

5.3 使用ioctl函数

目前有些实现支持ioctl的FIONREAD命令,第三个参数是一个指向某整数的指针,用于接收内核返回当前套接字接收队列中可读的数据。

以UDP为例,用ioctl查询当前套接字接收队列的数据量,当数据量达到某个阈值进行打印输出给出。

// 3、 发送到服务端
while (1)
{
  // ioctl查询就绪数据
  int len;
  if(ioctl(socket_fd, FIONREAD, &len) < 0){
    LOG("ioctl FIONREAD failed. %s", strerror(errno));
  }
  if(len<1){
    //LOG("ioctl get bytes to read len=%d", len);
    usleep(1000);
    continue;
  }

  LOG("buf len=%d", len);   
  // 等到数据达到指定长度才进行读取
  if(len < 10) {
    sleep(1);
    continue;
  }

  // 接收
  char buf[100]={}; 
    
  // 方式1
  // len = ::recv(socket_fd, buf, sizeof(buf), 0);  //仅能读取上一次缓冲数据
  // if(len < 0){    
  //   LOG("recv failed. %s", strerror(errno));
  //   break;
  // }
  // LOG("recv (%d): %s",len, buf);
    
  // 方式2
  // len = ::recv(socket_fd, buf, len, MSG_WAITALL); //阻塞模式使用
  // LOG("recv (%d): %s",len, buf);  // 仅能读取上一次的数据,不能读取完全

  //方式3
  int readN=0;
  while(readN < len){
    int tmpN = ::recv(socket_fd, buf + readN, len, 0);
    readN += tmpN;
  }
  LOG("recv (%d): %s",len, buf);
}

前两种方式不能一次性读完当前接收队列的中的数据,尝试在读取前进行sleep一段时间也不行(原因暂未深究)。第三种进行循环recv是可以的。
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aworkholic

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值