1.介绍
select 函数的作用是检测一组 socket 中某个或某几个是否有“事件”就绪,即可读、可写。
在Linux平台下的select定义如下:
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
-
参数 nfds,select 函数监听的 fd 中最大 fd 值加 1。
-
参数 readfds,监听可读事件的 fd 集合。
-
参数 writefds,监听可写事件的 fd 集合。
-
参数 exceptfds,监听异常事件 fd 集合。
其中:readfds、writefds 和 exceptfds 类型都是 fd_set,下面是对fd操作的一系列函数:
将一个 fd 添加到 fd_set 这个集合中
void FD_SET(int fd, fd_set *set);
从 fd_set 上删除一个 fd
void FD_CLR(int fd, fd_set *set);
判断某个 fd 是否有事件
int FD_ISSET(int fd, fd_set *set);
将 fd_set 中所有的 fd 都清掉
void FD_ZERO(fd_set *set);
- 参数 timeout
设定的时间内检测这些 fd,超时select就返回,时长是tv_sec 与 tv_usec之和。
//The time structures involved are defined in <sys/time.h> and look like
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
2.代码实战,单个线程里监听多个客户端连接:
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdio.h>
#include<errno.h>
#include<vector>
#include<string.h>
int main(int argc, char* argv[]){
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0){
printf("create socket failed!, errno=%d\n",errno);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
int ret;
ret = bind(fd, (struct sockaddr*) &addr, sizeof(addr));
if(ret == -1){
printf("bind failed!, errno=%d",errno);
close(fd);
}
ret = listen(fd, SOMAXCONN);
if(ret == -1){
printf("listen failed!, errno=%d",errno);
}
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
int maxfd = fd;
std::vector<int> afds;
//开始用select来管理多个连接
while(true){
fd_set fdset;
FD_ZERO(&fdset);
FD_SET(fd, &fdset);
for(int i = 0; i < afds.size(); ++i){
if(afds[i] != -1)//INVALID_FD
FD_SET(afds[i], &fdset);
}
timeval tm;
tm.tv_sec = 1;
tm.tv_usec = 0;
ret = select(maxfd+1, &fdset, NULL, NULL, &tm);//监听可读事件
if(ret < 0){
printf("select error!\n");
break;
}
else if(ret == 0){
printf("timeout!\n");
continue;//超时
}
else{
if(FD_ISSET(fd, &fdset)){//fd上有事件
int afd = accept(fd, (struct sockaddr*) &clientaddr, &clientaddrlen);
printf("accept afd= %d\n",afd);
if(afd == -1){
break;
}
printf("A client connected!,ip=%s,port=%d\n",inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
afds.push_back(afd);//将客户端连接的socket fd 加入到afds集合
maxfd = maxfd > afd ? maxfd : afd;
}
else{
for(int i = 0; i < afds.size(); ++i){
if(afds[i] != -1 && FD_ISSET(afds[i], &fdset)){
char buf[256] = {0};
int len = recv(afds[i], buf, 256, 0);
if(len <= 0){
printf("afd[%d] close or error\n",i);
close(afds[i]);
afds[i] = -1;
}
printf("afd[%d] recv %d bytes:%s\n", i, len, buf);
}
}
}
}
}
//关闭客户端所以连接
for(int i = 0; i < afds.size(); ++i){
if(afds[i] != -1)
close(afds[i]);
}
close(fd);//关闭监听socket
}
模拟三个客户端连接:
注意事项:(1)以上代码使用了vector,所以需要g++进行编译
3.总结
(1)select执行后会对三个参数的 fd_set 进行修改,所以每次需要重置。
(2)timeval 的tv也会被修改,所以需要重置。tv为0select会立即返回,为NULL会一直阻塞。
(3)每执行一次select就会fd从用户态拷贝到内核态。
(4)单进程监听socket的数量有限,默认1024,要修改的话,得重新编译内核。
(5)每次处理I/O都需要采用遍历fd(轮询)的方式。