在看I/O复用时看到了select()函数,说它可以让程序同时监视多个文件句柄的状态变化的。程序会停在select这里等待,
直到被监视的文件句柄有某一个或多个发生了状态改变。光这一句话发现这东东不太好理解,只好用例子来帮助理解这个。
先从函数原型及参数等入手:
int select(nfds, readfds, writefds, exceptfds, timeout)
当readfds或writefds中映象的文件可读或可写或超时,本次select() 就结束返回.
要注意,文件描述符即可以是套接字,也可以是普通文件。
参数:
ndfs : 需要select监视的文件描述符数,视进程中打开的文件数而定,值为要监视各文件中的最大文件号加一。
readfds : select监视的可读文件描述符集合,当此集合中的一个句柄上有数据到达时,系统通知调用select()的程序。
writefds : select监视的可写文件描述符集合,此集合中的一个描述符可发送数据时,程序将收到通知。
exceptfds :select监视的异常文件描述符集合,此集合中的一个句柄中的一个句柄发生异常时,程序将收到通知。
timeout :指定等待一组指定的文件描述符集合中的任一个准备好的时长,就是最大等待时间。
其参数
为NULL时,select()将一直阻塞。直到有文件描述符就绪,或进程出错之类,才返回
为0时,select()仅仅检测下文件描述符集的状态,然后立即返回
为非0时,在指定时间内,如果没有事件发生,select()将会超时返回,防止永远阻塞出现。
而在这段时间内有事件发生,则会通知select()程序。
返回值:
如果select监控的文件描述符集中如果有一个或多个描述符发生了事件,则会返回发生事件的文件描述符的个数。
当Select重新返回时,文件描述符集中内容已被为发现事件的文件描述符。
再通过FD_SET来确认,哪些文件描述符上发生了事件。
相关的宏定义:
FD_ZERO(fd_set *fdset) : 清空fdset与所有文件描述符的联系。
FD_SET(int fd, fd_set *fdset) : 建立文件描述符fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset) : 清除文件描述符fd与fdset的联系。
FD_ISSET(int fd, fdset *fdset) : 检查fdset联系的文件描述符fd是否可读写,>0表示可读写。
fd_set说明:
fd_set 是通过bit位来存放文件描述符,可通过sizeof(fd_set) * 8
来得可支持的最大文件描述符数,但受系统限制,基本达不到这个值.
一。简单的演示
在标准输入(stdin)上简单测试下
/*************************************************
Author: xiongchuanliang
Description: select函数演示代码
编译命令:
Linux:
g++ -m64 -o testselectdemo testselectdemo.cpp
**************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
void display_time(const char *str)
{
int seconds;
seconds = time( (time_t*)NULL);
printf("%s, %d\n",str,seconds);
}
int main(void)
{
fd_set readfds;
struct timeval timeout;
int ret;
//监控标准输入(0),即键盘是否有输入。
FD_ZERO(&readfds); //
FD_SET(STDIN_FILENO,&readfds); //也可直接用0表示stdio
//可尝试下将timeout的初始化放在while外面,会发现结果和想得不一样
//原因是select()中每执行一次,都会修改参数timeout为剩下的时间。
// 所以timeout的初始化要记得放在while内
//timeout.tv_sec = 5; //阻塞5秒
//timeout.tv_usec = 0;
while(1)
{
timeout.tv_sec = 5; //阻塞5秒
timeout.tv_usec = 0;
display_time("\nbefore select");
ret = select(1,&readfds,NULL,NULL,&timeout);
//ret = select(1,&readfds,NULL,NULL,NULL); //为NULL时
display_time("after select");
switch(ret)
{
case 0:
printf("No data in ten seconds. \n");
//如timeout.tv_sec与timeout.tv_usec值都为0,且没有这个exit,
//则不会进行阻塞,将会出现刷屏现象
exit(EXIT_SUCCESS);
break;
case -1:
perror("select");
exit(EXIT_FAILURE);
break;
default:
getchar();
printf("Data is available now.\n");
}
}
return 0;
}
二。把select函数应用在Socket客户端上
/*************************************************
Author: xiongchuanliang
Description: I/O复用(异步阻塞)模式例子_客户端代码
编译命令:
Linux:
g++ -o tcpclientasynselect tcpclientasynselect.cpp -m64 -I./common
**************************************************/
// 客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "initsock.h"
#include "common.h"
int main(int argc, char* argv[])
{
int sclient = 0; //连接服务端的套接字
int flags = 0; //fcntl返回标识
int recvbytes = 0; //服务端返回的数据长度
char recvData[MAXDATASIZE] = {0}; //保存服务端返回的数据
fd_set readset, writeset; //用于select()函数
int fp = 0; //文件句柄的
int selectMaxfd = 0; //fd_set集合中所有文件句柄的范围,即所有文件句柄的最大值加1,
int ret = 0;;
//取出传入参数
const size_t MaxLen = 500;
char testMsg[MaxLen]={0};
if(argc < 3)
{
printf("Usage:%s [ip address] [any string]\n",argv[0]);
exit(EXIT_FAILURE);
}else{
strncpy(testMsg,argv[2],strlen(argv[2]));
printf("Message: %s \n",testMsg);
}
//除了用open() 也可以用后面的方式打开文件:FILE * fpw = NULL; fpw = fopen(...) fp = fileno(fpw);
if( (fp = open("xcl.log",O_WRONLY|O_CREAT|O_TRUNC,0640)) < 0)
{
perror("open() failed\n");
exit(EXIT_FAILURE);
}
//建立套接字
sclient = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sclient == INVALID_SOCKET)
{
PrintError("invalid() failed");
exit(EXIT_FAILURE);
}
//指定要连接的服务器地址和端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVPORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr); //用户输入的ip参数
memset(&(server_addr.sin_zero),0,8);
//设为非阻塞模式
flags = fcntl(sclient, F_GETFL, 0);
fcntl(sclient, flags|O_NONBLOCK);
//将套接字连接上服务器
if( connect(sclient,(struct sockaddr *)&server_addr,sizeof(struct sockaddr) ) == -1)
{
PrintError("connect() failed");
exit(EXIT_FAILURE);
}
//I/O复用(异步阻塞)模式--发送数据到服务端
if( send(sclient,testMsg,strlen(testMsg),0) == -1)
{
PrintError("connect() failed");
exit(EXIT_FAILURE);
}
printf("send success!\n");
//I/O复用(异步阻塞)模式--接收返回的数据
while(1)
{
int check_timeval = 5; //轮询间隔
struct timeval timeout={check_timeval,0}; //阻塞式select, 超时时间. timeval{一个是秒数,另一个是毫秒数}
FD_ZERO(&readset); // 初始化,即将指定的文件句柄集清空
FD_SET(sclient,&readset); //添加描述符至fd_set集合
FD_ZERO(&writeset);
FD_SET(fp,&writeset); //添加描述符至fd_set集合
selectMaxfd = sclient > fp ? (sclient + 1) : (fp + 1);
ret = select(selectMaxfd,&readset,NULL,NULL,NULL); //阻塞模式
//ret = select(selectMaxfd,&readset,NULL,NULL,&timeout); //阻塞模式
switch( ret)
{
case -1: //error 发生错误
PrintError("select() error\n");
exit(EXIT_FAILURE);
break;
case 0: //timeout 超时
printf("select() timeout\n");
goto end;
break;
default:
if(FD_ISSET(sclient, &readset)) //测试sock是否可读,即是否网络上有数据
{
//接收数据
recvbytes = recv(sclient,recvData,MAXDATASIZE, MSG_DONTWAIT); //非阻塞模式
if( recvbytes == 0)
{
printf("recv() no data!\n");
}else if( recvbytes < 0){
PrintError("recv() failed");
exit(EXIT_FAILURE);
}else if(recvbytes > 0 ){
recvData[recvbytes] = '\0';
printf("recv:%s\n", recvData);
}
}
if(FD_ISSET(fp, &writeset)) //>0表示可读写
{
//printf("fp writeset .....\n");
if(recvbytes > 0 ){
printf("writeset= %s \n",recvData);
write(fp, recvData, strlen(recvData)+1);
printf("fp writeset end.\n");
//关闭
goto end;
}
}
}
}
end:
//关闭文件句柄
close(fp);
//关闭套接字,结束此次TCP会话
close(sclient);
exit(EXIT_SUCCESS);
}
代码中用到的头文件在; 网络编程(1)跨平台的Socket同步阻塞工作模式例子