一个服务器在处理多个客户端连接的情况时,如果为每个客户端创建一个线程来进行连接,显然是不太合适的。比较合适的方法就是使用IO多路复用,本文主要介绍使用select函数方式实现的IO多路复用,poll,epoll等方式后续文章介绍。所谓的IO多路复用模型即为:一个线程,通过记录I/O流的状态来同时管理多个I/O。
select()函数可以监视多个文件描述符的状态,当所监视的文件描述由阻塞变为非阻塞状态时给出相应的返回值,接下来可以进行读/写/异常操作。
使用select()函数对文件描述符进行监视时,需要用到FD系列函数:
void FD_CLR(int fd, fd_set *set); // 清除某一个被监视的文件描述符。
int FD_ISSET(int fd, fd_set *set); // 测试一个文件描述符是否是集合中的一员
void FD_SET(int fd, fd_set *set); // 添加一个文件描述符,将set中的某一位设置成1;
void FD_ZERO(fd_set *set); // 清空集合中的文件描述符,将每一位都设置为0;
假设此时有两个客户端socket1,socket2同时连接服务器,select()函数使用方法如下:
1、调用FD_ZERO清空集合fReadds中的文件描述符;
2、调用DF_SET将文件描述符socket1和socket2添加到集合fReadds中;
3、调用select()函数,第一个参数设置为socket1和socket2的最大值加1;
4、如果select()函数返回值大于0,调用FD_ISST判断所监视的文件描述符socket1和socket2是否有数据可读。
原理说明:
1、假设取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
2、执行FD_ZERO(&set);则set用位表示是0000,0000。
3、若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
4、若再加入fd=2,fd=1,则set变为0001,0011
5、执行select(6,&set,0,0,0),最后一个参数为0,所以阻塞等待
6、若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
根据原理说明可以看出,每次FD_SET和FD_ISSET都是需要根据socket数量来进行循环执行的。也就是需要注意以下几点:
1、需要通过循环把所有的socket放入集合fReadds;
2、select之后,再通过循环对所有的socket进行判断是否有数据传输;
3、所有的socket判断完毕,数据处理完成之后。需要重新从FD_ZERO开始再次执行。
示例代码:
list<int> listSocket;
fd_set fReadds;
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 200;
listSocket.push_back(socket1);
listSocket.push_back(socket2);
while (1)
{
// 清空集合
FD_ZERO(&fReadds);
int nMaxSocked = 0;
for (list<int>::iterator itr = listSocket.begin(); itr != listSocket.end(); itr++)
{
// 将文件描述符添加到集合
FD_SET(*itr, &fReadds);
if (nMaxSocked < itr->nSocket)
{
nMaxSocked = itr->nSocket;
}
}
// 第一个参数为socket的最大值加1,这样才能监视所有的socket
// 最后一个参数不能为0,否则阻塞模式会导致后续连接不上
int ret = select(nMaxSocked+1, &fReadds, NULL, NULL, &timeout);
if (ret == SOCKET_ERROR)
{
printf("select error\n");
continue;
}
else if (ret == 0)
{
continue;
}
else
{
for (list<int>::iterator itr = listSocket.begin(); itr != listSocket.end(); itr++)
{
// 判断所有监视的socket是否有数据传输
if (FD_ISSET(*itr, &fReadds))
{
// 接收数据,进行处理
}
}
}
}