Linux网络编程—I/O复用模型之select
1. IO复用模型
- IO复用能够预先告知内核,一旦发现进程指定的一个或者多个IO条件就绪,它就通知进程。
- IO复用阻塞在select或poll系统调用上,而不是阻塞在真正的IO系统调用上。
2. 函数select
select函数能够告知内核对哪些描述符(不局限于套接字)感兴趣以及等待多长事件
#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);
//返回值:返回就绪描述符数目,若超时则为0, 若出错则为-1
- timeout用来指定内核等待所指定描述符的任何一个就绪花多长事件。timeval结构用于指定这段事件的秒数和微妙数
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
//当timeout为NULL,则永远等待。
//当timeout为timeval,等待固定时间。
//当timeout为timeval,但timeval时间设置为0,则检查描述符字立即返回,称为轮询。
- 中间的三个参数readfds、writefds、exceptfds指定内核测试读、写、异常条件的描述符,这三个参数都是fd_set结构的指针类型,fd_set结构实现如下:
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
select函数使用描述符集,通常是一个整数数组,其中每一个整数中的每一位对应一个描述符。如何操作这些描述符则系统提供了四个宏
void FD_CLR(int fd, fd_set *set);
//把文件描述符集合里fd清零
int FD_ISSET(int fd, fd_set *set);
//测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set);
//把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set);
//把文件描述符集合里所有位清0
- 如果对应哪一个条件不感兴趣,则可以将它设置为空指针。
- nfds参数指定待测试的描述符个数,它的值是待测试的最大描述符加1,描述符0到nfds-1都会被测试。
- select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率。
- select函数修改由指针readfds、writefds、exceptfds指向的描述符集,调用函数时,我们指定所关心的描述符的值,函数返回时,结果将指示哪些描述符已就绪,函数返回后,使用FD_ISSET宏来测试fd_set数据类型中的描述符,描述符集内任何与未就绪描述符对应的位均清成0.为此,每次重新调用select函数时都要将描述符集内所关心的位置为1。
3. select模型实现
3.1 服务器端
#include "wrap.h"
#define MAXLINE 1024
int start_ser(char *ipaddr, char *port)
{
int sock = Socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(port));
inet_pton(AF_INET, ipaddr, &serveraddr.sin_addr);
Bind(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
Listen(sock, 128);
return sock;
}
int main(int argc, char *argv[])
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set readset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in clientaddr;
listenfd = start_ser(argv[1], argv[2]); //监听文件描述符
maxfd = listenfd;//最大的文件描述符
maxi = -1;//数组中最大文件描述符下标
for(i = 0; i < FD_SETSIZE; i++){
client[i] = -1;
}
FD_ZERO(&allset);//初始化allset集合
FD_SET(listenfd, &allset);//添加监听文件描述符到集合allset中
while(1){
readset = allset;//每次select都要初始化集合,结构体可以直接赋值
nready = select(maxfd+1, &readset, NULL, NULL, NULL);//组摄等待连接或请求
if(FD_ISSET(listenfd, &readset)){//当监听文件描述符相应有新的客户端连接时
clilen = sizeof(clientaddr);
connfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);//接收该客户端
if(connfd < 0){
perr_exit("accept err");
break;
}
for(i = 0; i < FD_SETSIZE; i++){
if(client[i] < 0){
client[i] = connfd; //保存文件描述符到数组
break;
}
}
if(i == FD_SETSIZE){ //连接个数不能大于内核规定的FD_SETSIZE
perr_exit("too many clients");
}
FD_SET(connfd, &allset);//添加新描述符到allset集合中
if(connfd > maxfd){
maxfd = connfd; //更新最大文件描述符
}
if(i > maxi){
maxi = i; //更新最大文件描述符下标
}
if(--nready == 0){ //只有一个listenfd响应则直接跳过下面的语句
continue;
}
}
for(i = 0; i <= maxi; i++){ //处理已连接客户端的请求
if((sockfd = client[i]) < 0){
continue;
}
if(FD_ISSET(sockfd, &readset)){ //sockfd是否在readset集合中
memset(buf, '\0', MAXLINE);
if((n = Read(sockfd, buf, MAXLINE-1)) == 0){
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
}else{
printf("client:%s\n", buf);
}
if(--nready == 0){//判断是否查找完
break;
}
}
}
}
return 0;
}
3.2 客户端
#include "wrap.h"
int main(int argc, char *argv[])
{
int connfd;
struct sockaddr_in serveraddr;
char buf[1024];
connfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);
Connect(connfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
while(fgets(buf, 1024, stdin) != NULL){
Write(connfd, buf, strlen(buf));
}
Close(connfd);
return 0;
}