IO多路复用--select
1 IO多路复用的基本概念
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
2 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: 集合中所有文件描述符的范围,即所有文件描述符的最大值加1,描述字0、1、2...nfds-1均将被测试;
readfds: 指向fd_set结构的指针,这个集合中包括需要监控读事件的文件描述符,若可读则返回;当然也可以置空(NULL),表示并不关心;
writefds, exceptfds: 同readfds;
timeout: select的超时时间,这个参数至关重要,它可以使select处于三种状态:
- 若将NULL以形参传入,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
- 若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
- timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
返回值:
- >0:就绪描述字的正数目;
- -1:出错;
- 0 : 超时;
fd_set :
void FD_ZERO (fd_set *fdset); // clear all bits in fdset void FD_SET (int fd,fd_set *fdset); // turn on the bit for fd in fdset void FD_CLR (int fd,fd_set *fdset); // turn off the bit for fd in fdset int FD_ISSET(int fd,fd_set *fdset); // is the bit for fd on in fdset
struct timeval:
timeval结构用于指定这段时间的秒数和微秒数,
struct timeval { long tv_sec; //seconds long tv_usec; //microseconds };
select() may update the timeout argument to indicate how much time was left. pselect() does not change this argument.
3 select函数的典型应用
下面介绍一个给予select的echo server;client直接通过telnet连接server:telnet host port,主要包括以下几个
基本功能:
(1) client 发送quit,则server主动断开与客服端的连接;
(2) clinet发送其他data到server,则server会原样返回;
注意点:
(1) 由于通过telnet与server通信,telnet client发送过来的字符串的结束符为"\r\n" ,所以判断client的断开信号时,需判断是否为"quit\r\n"
(2) 同样打印客服端的数据时不需要再添加换行符,因为data form telnet client自带的就有;
/* Handle multiple socket connections with select and fd_set on Linux */ #include <stdio.h> #include <string.h> //strlen #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros #define TRUE 1 #define FALSE 0 #define PORT 8888 #define CLIENT_NUM 2 #define BLOCK_NUM 2 #define BUFFER_SIZE 1024 int main(int argc , char *argv[]) { int opt = TRUE; int master_socket , addrlen , new_socket , client_socket[CLIENT_NUM] , activity, i , valread , sd; int max_sd; struct sockaddr_in address; char buffer[BUFFER_SIZE+1]; //data buffer of 1K struct timeval tv; char quit[7]="quit\r\n"; //for data from telnet client, the data end with "\r\n" //set of socket descriptors fd_set readfds; //a message retuan to client when it connect to the server. char *message = "ECHO Daemon v1.0 \r\n"; //initialise all client_socket[] to 0 so not checked for (i = 0; i < CLIENT_NUM; i++) { client_socket[i] = 0; } printf("size of buffer:%lu\n",sizeof(buffer)); memset(buffer,0,sizeof(buffer)*sizeof(buffer[0])); //create a master socket for listening if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } /*一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。*/ if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0 ) { perror("setsockopt"); exit(EXIT_FAILURE); } //type of socket created address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons( PORT ); //bind the socket to localhost port 8888 if (bind(master_socket, (struct sockaddr *)&address, sizeof(address))<0) { perror("bind failed"); exit(EXIT_FAILURE); } printf("Listener on port %d \n", PORT); //try to specify maximum of BLOCK_NUM pending connections for the master socket if (listen(master_socket, BLOCK_NUM) < 0) { perror("listen"); exit(EXIT_FAILURE); } //accept the incoming connection addrlen = sizeof(address); puts("Waiting for connections ..."); /********************************call select in a infinite loop*******************************/ while(TRUE) { /*每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核修改啦*/ FD_ZERO(&readfds); /*添加监听套接字*/ FD_SET(master_socket, &readfds); max_sd = master_socket; tv.tv_sec = 30; tv.tv_usec = 0; /*添加客户端套接字*/ for ( i = 0 ; i < CLIENT_NUM ; i++) { //socket descriptor sd = client_socket[i]; //if valid socket descriptor then add to read list if(sd > 0) FD_SET( sd , &readfds); //highest file descriptor number, need it for the select function if(sd > max_sd) max_sd = sd; } printf("\nselect begin, timeout left:%lds-%ldms.\n",tv.tv_sec,tv.tv_usec); //wait for an activity on one of the sockets, timeout is tv; activity = select( max_sd + 1 , &readfds , NULL , NULL , &tv); //return value < 0, error happend; if ((activity < 0) && (errno!=EINTR)) { printf("select error"); } //return value = 0, timeout; else if(activity == 0){ //no event happend in specific time, timeout; printf("select return, activity:%d, timeout left:%lds-%ldms.\n", activity, tv.tv_sec, tv.tv_usec); continue; } //If something happened on the master socket , then its an incoming connection if (FD_ISSET(master_socket, &readfds)) { printf("select return, timeout left:%lds-%ldms.\n",tv.tv_sec,tv.tv_usec); if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) { perror("accept"); exit(EXIT_FAILURE); } //inform user of socket number - used in send and receive commands printf("New connection , socket fd is %d , ip is : %s , port : %d \n" , new_socket , inet_ntoa(address.sin_addr) , ntohs(address.sin_port)); //send new connection greeting message if( send(new_socket, message, strlen(message), 0) != strlen(message) ) { perror("send"); } puts("Welcome message sent successfully"); //add new socket to array of sockets for (i = 0; i < CLIENT_NUM; i++) { //if position is empty if( client_socket[i] == 0 ) { client_socket[i] = new_socket; printf("Adding to list of sockets as %d\n" , i); break; } } } //else there is some IO operation on some other socket :) for (i = 0; i < CLIENT_NUM; i++) { sd = client_socket[i]; printf("check client--%d:fd--%d\n",i,sd); if (FD_ISSET( sd , &readfds)) { printf("select return, timeout left:%lds-%ldms.\n",tv.tv_sec,tv.tv_usec); //Check if it was for closing , and also read the incoming message valread = read( sd , buffer, 1024); printf("read form client,size:%d\n",valread); if (valread <= 0) { //Somebody disconnected , get his details and print getpeername(sd , (struct sockaddr*)&address , (socklen_t*)&addrlen); printf("Host disconnected , ip %s , port %d \n" , inet_ntoa(address.sin_addr) , ntohs(address.sin_port)); //Close the socket and mark as 0 in list for reuse close( sd ); client_socket[i] = 0; } //get message "quit", close the connection; else if(valread == strlen(quit) && memcmp(buffer,quit,strlen(quit))==0) { printf("client of fd:%d will be closed.\n", sd); client_socket[i] = 0; close(sd); } //Echo back the message that came in else { printf("data come from client:%s",buffer); send(sd , buffer , strlen(buffer) , 0 ); memset(buffer,0,valread); //printf("sleep 5s\n"); //sleep(5); } } } } return 0; }
运行结果:
[root@localhost LibEvent_practice]# ./echo-server-select size of buffer:1025 Listener on port 8888 Waiting for connections ... select begin, timeout left:30s-0ms. select return, timeout left:25s-213990ms.
//clinet1 connect to server, select return, the time left to timeout is "timeout left"; New connection , socket fd is 4 , ip is : 10.204.214.232 , port : 45042 //detail info of client Welcome message sent successfully Adding to list of sockets as 0 check client--0:fd--4 //client fd info saved in client_socket[CLIENT_NUM] check client--1:fd--0 select begin, timeout left:30s-0ms. select return, timeout left:20s-751072ms. //client2 connect to server New connection , socket fd is 5 , ip is : 10.204.214.248 , port : 48117 Welcome message sent successfully Adding to list of sockets as 1 check client--0:fd--4 check client--1:fd--5 select begin, timeout left:30s-0ms. check client--0:fd--4 select return, timeout left:25s-940254ms.
//client1 fd可读,读取数据并回传给client read form client,size:21 data come from client:aaaaaaaaaaaaaaaaaaa check client--1:fd--5 select begin, timeout left:30s-0ms. check client--0:fd--4 check client--1:fd--5 select return, timeout left:23s-593254ms.
//client2 fd可读,读取数据并回传,可以看到dataSize=7,可以看到的数据为bbbbb,实际来自client的数据为"bbbbb\r\n",打印出"bbbbb"然后换行; read form client,size:7 data come from client:bbbbb select begin, timeout left:30s-0ms. check client--0:fd--4 select return, timeout left:19s-478711ms. read form client,size:6 client of fd:4 will be closed.
//读取到来自client的"quit\r\n"字符串,主动关闭client check client--1:fd--5 select begin, timeout left:30s-0ms. select return, activity:0, timeout left:0s-0ms. select begin, timeout left:30s-0ms. ^C