https://blog.csdn.net/men_wen/article/details/53456435
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 1024typedef 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 1024int 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_SETSIZEperr_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;
}