1. I/O复用
我们学习了I/o复用的基本知识,了解到目前支持I/O复用的系统调用有select、pselect、poll、epoll。而epoll技术以其独特的优势被越来越多的应用到各大企业服务器。(后面将有poll & epoll单独学习笔记)
基本概念
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
2. select技术
select()函数确定一个或多个套接口的状态,本函数用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息,用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。
2.1函数原型
该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:
#include <sys/select.h> #include <sys/time.h> int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout) 返回值:就绪描述符的数目,超时返回0,出错返回-1函数参数介绍如下:
(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。因为文件描述符是从0开始的。
(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
有关select更加详细的讲解请参考《Unix网络编程 -- 卷一》第六章 Page127 ~ 142;
2.2 select原理流程图
3. TCP回射程序实例
本例是基本套接字编程(1) -- tcp篇中回射程序的改写,其中server端采用select技术,实现I/O复用,可同时为多个客户程序服务!
3.1 server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <time.h> #include <sys/socket.h> #include <sys/select.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <fcntl.h> #define PORT 8888 #define MAX_LINE 2048 #define LISTENQ 20 int main(int argc , char **argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready , client[FD_SETSIZE]; ssize_t n, ret; fd_set rset , allset; char buf[MAX_LINE]; socklen_t clilen; struct sockaddr_in servaddr , cliaddr; /*(1) 得到监听描述符*/ listenfd = socket(AF_INET , SOCK_STREAM , 0); /*(2) 绑定套接字*/ bzero(&servaddr , sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(PORT); bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)); /*(3) 监听*/ listen(listenfd , LISTENQ); /*(4) 设置select*/ maxfd = listenfd; maxi = -1; for(i=0 ; i<FD_SETSIZE ; ++i) { client[i] = -1; }//for FD_ZERO(&allset); FD_SET(listenfd , &allset); /*(5) 进入服务器接收请求死循环*/ while(1) { rset = allset; nready = select(maxfd+1 , &rset , NULL , NULL , NULL); if(FD_ISSET(listenfd , &rset)) { /*接收客户端的请求*/ clilen = sizeof(cliaddr); printf("\naccpet connection~\n"); if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0) { perror("accept error.\n"); exit(1); }//if printf("accpet a new client: %s:%d\n", inet_ntoa(cliaddr.sin_addr) , cliaddr.sin_port); /*将客户链接套接字描述符添加到数组*/ for(i=0 ; i<FD_SETSIZE ; ++i) { if(client[i] < 0) { client[i] = connfd; break; }//if }//for if(FD_SETSIZE == i) { perror("too many connection.\n"); exit(1); }//if FD_SET(connfd , &allset); if(connfd > maxfd) maxfd = connfd; if(i > maxi) maxi = i; if(--nready < 0) continue; }//if for(i=0; i<=maxi ; ++i) { if((sockfd = client[i]) < 0) continue; if(FD_ISSET(sockfd , &rset)) { /*处理客户请求*/ printf("\nreading the socket~~~ \n"); bzero(buf , MAX_LINE); if((n = read(sockfd , buf , MAX_LINE)) <= 0) { close(sockfd); FD_CLR(sockfd , &allset); client[i] = -1; }//if else{ printf("clint[%d] send message: %s\n", i , buf); if((ret = write(sockfd , buf , n)) != n) { printf("error writing to the sockfd!\n"); break; }//if }//else if(--nready <= 0) break; }//if }//for }//while }
3.2 client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <time.h> #include <sys/socket.h> #include <sys/select.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <fcntl.h> #define PORT 8888 #define MAX_LINE 2048 int max(int a , int b) { return a > b ? a : b; } /*readline函数实现*/ ssize_t readline(int fd, char *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = read(fd, &c,1)) == 1) { *ptr++ = c; if (c == '\n') break; /* newline is stored, like fgets() */ } else if (rc == 0) { *ptr = 0; return(n - 1); /* EOF, n - 1 bytes were read */ } else return(-1); /* error, errno set by read() */ } *ptr = 0; /* null terminate like fgets() */ return(n); } /*普通客户端消息处理函数*/ void str_cli(int sockfd) { /*发送和接收缓冲区*/ char sendline[MAX_LINE] , recvline[MAX_LINE]; while(fgets(sendline , MAX_LINE , stdin) != NULL) { write(sockfd , sendline , strlen(sendline)); bzero(recvline , MAX_LINE); if(readline(sockfd , recvline , MAX_LINE) == 0) { perror("server terminated prematurely"); exit(1); }//if if(fputs(recvline , stdout) == EOF) { perror("fputs error"); exit(1); }//if bzero(sendline , MAX_LINE); }//while } /*采用select的客户端消息处理函数*/ void str_cli2(FILE* fp , int sockfd) { int maxfd; fd_set rset; /*发送和接收缓冲区*/ char sendline[MAX_LINE] , recvline[MAX_LINE]; FD_ZERO(&rset); while(1) { /*将文件描述符和套接字描述符添加到rset描述符集*/ FD_SET(fileno(fp) , &rset); FD_SET(sockfd , &rset); maxfd = max(fileno(fp) , sockfd) + 1; select(maxfd , &rset , NULL , NULL , NULL); if(FD_ISSET(fileno(fp) , &rset)) { if(fgets(sendline , MAX_LINE , fp) == NULL) { printf("read nothing~\n"); close(sockfd); /*all done*/ return ; }//if sendline[strlen(sendline) - 1] = '\0'; write(sockfd , sendline , strlen(sendline)); }//if if(FD_ISSET(sockfd , &rset)) { if(readline(sockfd , recvline , MAX_LINE) == 0) { perror("handleMsg: server terminated prematurely.\n"); exit(1); }//if if(fputs(recvline , stdout) == EOF) { perror("fputs error"); exit(1); }//if }//if }//while } int main(int argc , char **argv) { /*声明套接字和链接服务器地址*/ int sockfd; struct sockaddr_in servaddr; /*判断是否为合法输入*/ if(argc != 2) { perror("usage:tcpcli <IPaddress>"); exit(1); }//if /*(1) 创建套接字*/ if((sockfd = socket(AF_INET , SOCK_STREAM , 0)) == -1) { perror("socket error"); exit(1); }//if /*(2) 设置链接服务器地址结构*/ bzero(&servaddr , sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); if(inet_pton(AF_INET , argv[1] , &servaddr.sin_addr) < 0) { printf("inet_pton error for %s\n",argv[1]); exit(1); }//if /*(3) 发送链接服务器请求*/ if(connect(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0) { perror("connect error"); exit(1); }//if /*调用普通消息处理函数*/ str_cli(sockfd); /*调用采用select技术的消息处理函数*/ //str_cli2(stdin , sockfd); exit(0); }
3.3 运行结果
服务器监听终端:
两个客户链接服务器:
注:以上部分理论内容来自参考博客,多谢原博主!