一. select
前面提到Linux下的五种IO模型中有一个是IO复用模型,这种IO模型是可以调用一个特殊的函数同时监听多个IO事件,当多个IO事件中有至少一个就绪的时候,被调用的函数就会返回通知用户进程来处理已经ready事件的数据,这样通过同时等待IO事件来代替单一等待一个IO窗口数据的方式,可以大大提高系统的等待数据的效率;而接下来,就要讨论在Linux系统中提供的一个用来进行IO多路等待的函数——select;
二. select函数的用法
首先在使用select之前,要分清在IO事件中,往往关心的不是数据的读取就是数据的发送,也就是数据的读和写,当然也有同时关心读写的,没有任何一个IO事件既不关系读也不关心写的,因此,在对于使用select对多个IO事件进行监听检测的时候,就要对这些事件进行读写的分类,以便日后在select返回时通过检测能够得知当前事件是读发生了还是写发生了;
函数参数中,
nfds表示当前最大文件描述符值+1;
readfds表示当前的事件中有多少是关心数据的读取的;
writefds表示当前的事件中有多少是关心数据的写入的;
excptfds表示当前事件中关心异常发生的事件集,也是数据的写入;
其中,fd_set是一个文件符集的数据类型;
对于fd_set文件描述符集的设置,系统提供了四个函数来进行操作:
FD_CLR是对文件描述符集中的所有文件描述符进行清除;
FD_ISSET是判断某个文件描述符是否已经被设置进某个文件描述符集中;
FD_SET是将某个文件描述符设置进某个文件描述符集中;
FD_ZERO是对某个文件描述符集进行初始化;
timeout是时间的设定,表示当超过设定的时间仍然没有事件就绪时就超时返回不再等待;
timeout的结构体类型如下:
tv_sec是秒的设置;
tv_usec是微秒的设置;
对于select函数的返回值:
当返回值为-1的时候,表示函数出错并会置相应的错误码;
当返回值为0的时候,表示超时返回;
当返回值大于0的时候,表示至少已经有一个事件已经就绪可以处理其数据了;
三. 栗子时间
前面有一篇本人写的博客是基于TCP协议的socket编程,其中一个服务器为了能处理多个连接请求将listen监听和accept处理连接请求分开,每当listen到一个连接请求的时候就fork出一个子进程让子进程去处理,或者使用多线程,这样就不耽误对网络中连接请求的监听了;
但是同样是单进程,可以使用select的IO复用模型来解决对多个连接的数据处理,程序设计如下:
server服务器端:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/select.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define _BACKLOG_ 5//设置监听队列里面允许等待的最大值 int fds[20];//用于集存需要进行处理的IO事件 void usage(const char *argv)//进行命令行参数的差错判断 { printf("%s [ip] [port]\n", argv); exit(0); } int creat_listen_sock(int ip, int port)//创建listen socket { int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0) { perror("socket"); exit(1); } struct sockaddr_in server;//设置本地server端的网络地址信息 server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = ip; if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)//绑定端口号和网络地址信息 { perror("bind"); exit(3); } if(listen(sock, _BACKLOG_) < 0)//进行监听 { perror("listen"); exit(2); } return sock; } int main(int argc, char *argv[]) { if(argc != 3) usage(argv[0]); int port = atoi(argv[2]); int ip = inet_addr(argv[1]); int listen_sock = creat_listen_sock(ip, port);//获取监听端口号 struct sockaddr_in client;//创建对端网络地址信息结构体用于保存对端信息 socklen_t client_len = sizeof(client); size_t fds_num = sizeof(fds)/sizeof(fds[0]); size_t i = 0; for(; i < fds_num; ++i)//将存放文件描述符的数组进行初始化 fds[i] = -1; fds[0] = listen_sock;//首先将listen socket添加进去 fd_set read_fd;//创建读事件文件描述符集 fd_set write_fd;//创建写事件文件描述符集 int max_fd = fds[0];//首先将最大的文件描述符集设定为listen socket while(1) { FD_ZERO(&read_fd);//将两个文件描述符集进行初始化 FD_ZERO(&write_fd); struct timeval timeout = {10, 0};//设定超时时间 size_t i = 0; for(; i < fds_num; ++i)//每次循环都要将数组中的文件描述符进行重新添加设置 { if(fds[i] > 0) { FD_SET(fds[i], &read_fd); if(fds[i] > max_fd) max_fd = fds[i]; } } switch(select(max_fd+1, &read_fd, &write_fd, NULL, &timeout))//进行select等待 { case -1://出错 perror("select"); break; case 0://超时 printf("time out...\n"); break; default://至少有一个IO事件已经就绪 { size_t i = 0; for(; i < fds_num; ++i) { //当为listen socket事件就绪的时候,就表明有新的连接请求 if(FD_ISSET(fds[i], &read_fd) && (fds[i] == listen_sock)) { int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len); if(accept_sock < 0) { perror("accept"); continue; } char *client_ip = inet_ntoa(client.sin_addr); int client_port = ntohs(client.sin_port); printf("connect with a client... [ip]:%s [port]:%d\n", client_ip, client_port); size_t i = 0; for(; i < fds_num; ++i)//将新的连接请求的文件描述符添加进数组保存 { if(fds[i] == -1) { fds[i] = accept_sock; break; } } if(i == fds_num) close(accept_sock); } //除了listen socket就是别的普通进行数据传输的文件描述符 else if(FD_ISSET(fds[i], &read_fd) && (fds[i] > 0)) { char buf[1024]; ssize_t size = read(fds[i], buf, sizeof(buf)-1); if(size < 0) perror("read"); else if(size == 0) {//当client端关闭就关闭相应的文件描述符 printf("client closed...\n"); close(fds[i]); fds[i] = -1; } else { buf[size] = ‘\0‘; printf("client# %s\n", buf); } } else {} } } break; } } return 0; }
因为客户端的程序和前面的TCP的程序一样,这里就不再多写;
上面的程序可以分为如下步骤:
创建监听套接字并绑定本地网络地址信息进行监听;
创建一个全局的数组用于存放已有事件的文件描述符,便于重新进行整理;
创建读、写事件集,这里忽略异常事件集;
循环等待各个事件的就绪,每次都重新初始化事件集和重新添加设置,因为select会将没有就绪的事件清为0;
select完成进行返回值的一个判断:如果是-1,则出错返回;如果是0,则超时返回;如果是大于零的值,则表明至少有一个事件就绪,转到第6步;
将数组中的事件拿出一一进行判断:如果是listen socket就绪表明有新的连接请求,新创建一个文件描述符用于处理数据的传输,并将其添置进数组中;如果是别的文件描述符就绪表明有数据传输过来需要读取,转第7步;
读取数据时,如果判断client端关闭就将数组中相应位置还原回无效值并且关闭相应的socket文件描述符,读取成功输出数据,继续循环;
运行程序:
可以注意到上面的程序中sever端只将所有的连接请求都作为读事件添加进去了,而并没有关心写事件,事实上socket支持全双工的通信,因此,将上面的程序改为server端读取数据的同时将数据再写回给client端,以此来告知client端server端已经成功收到了数据,程序改进如下:
在循环每一次重新整理数组中的文件描述符集的时候将不是listen socket的文件描述符集同时添加进读事件集和写事件集:
FD_ZERO(&read_fd); FD_ZERO(&write_fd); FD_SET(listen_sock, &read_fd);//先将listen socket添加进读事件集 struct timeval timeout = {10, 0}; size_t i = 1;//循环跳过listen socket从1开始 for(; i < fds_num; ++i) { if(fds[i] > 0) { FD_SET(fds[i], &read_fd);//同时添加进读事件集和写事件集 FD_SET(fds[i], &write_fd); if(fds[i] > max_fd) max_fd = fds[i]; } }
而当数据就绪进行读取完毕之后,再将同一个缓冲区中的数据写回client端,这里因为读写事件中使用的是同一个文件描述符,因此,当一个socket的读事件准备就绪的时候,说明写事件同样也是就绪的,而且使用同一个缓冲区中相同的数据:
else if(FD_ISSET(fds[i], &read_fd) && (FD_ISSET(fds[i], &write_fd)) && (fds[i] > 0)) { char buf[1024]; ssize_t size = read(fds[i], buf, sizeof(buf)-1); if(size < 0) perror("read"); else if(size == 0) { printf("client closed...\n"); close(fds[i]); fds[i] = -1; break; } else { buf[size] = ‘\0‘; printf("client# %s\n", buf); } if(FD_ISSET(fds[i], &write_fd)) { size = write(fds[i], buf, strlen(buf)); if(size < 0) perror("write"); } else printf("can not write back...\n"); }
因此,在client端也需要进行读取;
运行程序:
总结如上,虽然select实现IO复用在等待数据的效率看来要比单一的等待高,但是不难发现当需要等待多个事件的时候,是需要不断地进行复制和循环判断的,这也同样增加了时间复杂度增加了系统的开销,而且,作为一个数据类型的fd_set是由上限的,我的当前机器sizeof(fd_set)值为128,而一个字节能添加8个文件描述符,也就是总共只能添加128*8=1024个文件描述符,这个数目还是有些小的,无疑也是一个缺点。
《完》
本文出自 “敲完代码好睡觉zzz” 博客,请务必保留此出处http://2627lounuo.blog.51cto.com/10696599/1783654
原文地址:http://2627lounuo.blog.51cto.com/10696599/1783654