本章笼统归为“高级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是可以的。