代码和知识有引用自 高性能服务器开发
select系统调用:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写、异常事件
select 函数API如下
int select(int nfds, fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数含义:
nfds 被监听的文件描述符总数,通常被设置为select监听的所有文件描述符中的最大值加1(因为文件描述符是从0开始的)
readfds、writefds、exceptfds 对应需要监听的可读、可写、异常事件的fd集合
timeout select函数超时时间,不设置的话select函数会一直阻塞等待事件发生
其中readfds、writefds、exceptfds都为同个结构体fd_set,定义在/usr/include/sys/select.h中
/usr/include/bits/typesizes.h
#define __FD_SETSIZE 1024
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE
由此可见,fd_set就只有一个整型数组,该数组的每一位(bit)标记一个文件描述符。fd_set可以容纳的文件描述符数量由FD_SETSIZE决定。因此select在同时处理文件描述符的总量是受到限制的,在/usr/include/sys/select.h中还定义了一系列的宏来处理fd_set结构体的位:
#include <sys/select.h>
FD_ZERO(fd_set * fdset); //清除fdset的所有位
FD_SET(int fd,fd_set *fdset); //设置fdset的位fd
FD_CLR(int fd,fd_set *fdset); //清除fdset的位fd
int FD_ISSET(int fd,fd_set * fdset) //验证fdset的位fd是否被设置
/* We don't use `memset' because this would require a prototype and
the array isn't too big. */
# define __FD_ZERO(set) \
do { \
unsigned int __i; \
fd_set *__arr = (set); \
for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \
__FDS_BITS (__arr)[__i] = 0; \
} while (0)
#endif /* GNU CC */
#define __FD_SET(d, set) \
((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
#define __FD_CLR(d, set) \
((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d)))
#define __FD_ISSET(d, set) \
((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
接下来是参数timeout,结构体形式为:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
一般服务端实现的基础流程如下图
对应代码如下
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <iostream>
#include <string.h>
#include <vector>
#define INVALID_FD -1
int main(int argc,char * argv[])
{
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if(listenfd == -1)
{
std::cout << "create socket err" << std::endl;
return -1;
}
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if(bind(listenfd,(struct sockaddr *)&bindaddr,sizeof(bindaddr)) == -1)
{
std::cout << "bind err" << std::endl;
}
if(listen(listenfd,SOMAXCONN) == -1)
{
std::cout << "listen err" << std::endl;
}
// 存储客户端socket的数组
std::vector<int> clientfds;
int maxfd = listenfd;
while(true)
{
fd_set readset;
FD_ZERO(&readset); //清除readset的所有位
FD_SET(listenfd,&readset); //设置readset的位fd
int clientfdslength = clientfds.size();
for(int i=0; i<clientfdslength; ++i)
{
if(clientfds[i] != INVALID_FD)
{
FD_SET(clientfds[i],&readset);
}
}
timeval tm;
tm.tv_sec = 1;
tm.tv_usec = 0;
// 只检测可读事件
int ret = select(maxfd+1,&readset,NULL,NULL,&tm);
if(ret == -1)
{
if(errno != EINTR) //出错退出
break;
}
else if (ret == 0)
{
continue; //超时重来
}
else
{
if(FD_ISSET(listenfd,&readset)) //检测到有事件
{
// 侦听sockct的可读事件,表明新的连接来到
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
// 接收客户端连接,生成新的对应客户端连接socket
int clientfd = accept(listenfd,(struct sockaddr *)&clientaddr,&clientaddrlen);
if(clientfd == -1)
{
break;
}
// 只接收连接不调用recv收取数据
std::cout << "accept a client connection, fd:" << clientfd << std::endl;
clientfds.push_back(clientfd);
// 记录最新的最大fd值作为下一轮循环中select的第一个参数
if (clientfd > maxfd)
{
maxfd = clientfd;
}
}
else
{
// 做客户端的时候保证传送数据不超过63个字符
char recvbuf[64];
int clientfdslength = clientfds.size();
for(int i=0; i<clientfdslength; ++i)
{
if(clientfds[i] != INVALID_FD && FD_ISSET(clientfds[i],&readset))
{
memset(recvbuf,0,sizeof(recvbuf));
// 非侦听socket,则接收socket
int length = recv(clientfds[i],recvbuf,64,0);
if( length <= 0 && errno != EINTR)
{
std::cout << "recv data err,clientfd: " << clientfds[i] << std::endl;
close(clientfds[i]);
clientfds[i] = INVALID_FD;
continue;
}
std::cout << "clientfd:" << clientfds[i] << ", recv data:" << recvbuf << std::endl;
}
}
}
}
}
int clientlength = clientfds.size();
for (int i=0; i< clientlength; ++i)
{
if(clientfds[i] != INVALID_FD)
{
close(clientfds[i]);
}
}
close(listenfd);
return 0;
}
但是select也有几个点需要注意:
1. 看代码在循环里面,会先进行清除readset的所有位以及设置readset的位fd这两步操作,为何不在循环外面做?这是因为select 函数调用前后会修改 readfds、writefds 和 exceptfds 这三个集合中的内容。所以清零再重新设置,就可以复用该位。
2. 在循环中也可以看出需要修改函数超时时间timeout的结构,这也是因为在select函数调用后也会将timeout结构体的内容重置。