select函数
原型: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数1:所监听的所有文件描述符中,最大的文件描述符+1
- 参数2/3/4:读/写/异常事件集合
- 参数5:超时时间
- 返回值:成功,所监听的所有监听集合中触发所监听事件的总数
相关操作函数:
- FD_ZERO(fd_set* set),将监听集合置空,用于初始化。
- FD_CLR(int fd, fd_set* set),将fd从集合中清除出去。
- FD_ISSET(int fd, fd_set* set),判断fd是否在监听集合中。
- FD_SET(int fd, fd_set* set),将fd设置到监听集合中去。
C实现
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <sys/un.h>
#include <arpa/inet.h> //inet_ntop的头文件
#define SERV_ADDR "127.0.0.1" //服务端IP
#define SERV_PORT 6666 //服务端端口
#define MAX_FD 1024 //最大文件句柄数
int main(void) {
//lfd服务端文件句柄, clients,全部客户端文件句柄, cfd,某客户端句柄
int lfd, clients[MAX_FD], cfd;
int nRet, i, len;
char buf[BUFSIZ]; //存储从客户端读取的字符串
char clie_ip[20]; //ip地址,字符串形式
int clie_port; //端口号
struct sockaddr_in serv_addr, clie_addr; //服务端和客户端地址
socklen_t clie_addr_len; //客户端地址长度
fd_set rset, allset; //监听集合位图
serv_addr.sin_family = AF_INET; //ipv4
inet_pton(AF_INET,SERV_ADDR, &serv_addr.sin_addr.s_addr); //转化字符串形式为网络字节序
serv_addr.sin_port = htons(SERV_PORT); //端口号,转化本机字节序为网络字节序
lfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字,返回服务端文件句柄
bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); //绑定服务端地址
listen(lfd, 128); //设置同时进行TCP链接的个数
FD_ZERO(&allset); //监听集合置空
FD_SET(lfd, &allset); //将服务端文件句柄加入到监听集合
for(i = 0; i < MAX_FD; ++i) { //将客户端文件句柄数组置为-1
clients[i] = -1;
}
while(1) {
rset = allset; //设置读监听集合
nRet = select(MAX_FD, &rset, NULL, NULL, NULL); //调用select,此处由内核进行监听,nRet,产生的事件个数
if(FD_ISSET(lfd, &rset)){ //若服务端有读事件产生
clie_addr_len = sizeof(clie_addr);
cfd = accept(lfd, (struct sockaddr*)&clie_addr, &clie_addr_len); //建立TCP链接
for(i = 0; i < MAX_FD; ++i) { //设置cfd到客户端文件句柄数组
if(clients[i] == -1) {
clients[i] = cfd;
FD_SET(cfd, &allset);
inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip, clie_addr_len); //转化网络字节序为字符串clie_ip
clie_port = ntohs(clie_addr.sin_port);
printf("%s:%d connected!\n", clie_ip, clie_port);
break;
}
}
if(i == MAX_FD) {
printf("客户端超出最大数\n");
exit(1);
}
--nRet;
}
for(i = 0; i < MAX_FD && nRet > 0; ++i) { //循环遍历客户端文件句柄数组
if(clients[i] == -1) continue;
if(FD_ISSET(clients[i], &rset)) { //客户端有读事件发生
--nRet;
//处理客户端请求
len = read(clients[i], &buf, sizeof(buf));
if(len <= 0) {
//客户端断开
FD_CLR(clients[i], &allset); //清空对应的监听事件
close(clients[i]);
clients[i] = -1;
printf("一个客户端断开连接!\n");
continue;
}
write(STDOUT_FILENO, buf, len);
write(clients[i], buf, len);
}
}
}
close(lfd);
return 0;
}
注意,上述程序是在linux环境下编译运行的c程序,没有写客户端程序,验证的方式可通过在新的终端下输入nc 127.0.0.1 6666命令来连接服务端。
总结
- 每次select返回后,需要对全部文件描述进行遍历,即使设置了nRet参数,当最后一个文件描述符即1023有事件产生,那么就必须遍历整个数组。
- 每次都要重新设置监听事件集合,本程序体现在将allset赋值给rset。
- 调用select需要将监听事件集合拷贝到内核,当select返回后又需要拷贝回用户空间,开销较大。
- select能够监听的事件个数有限,一般不超过1024。