1 简介
为进一步提升服务器效率,人们提出了一种被称为I/O多路转接的模型。其中“多路”指代连接到服务器的多个客户端程序,而“转接”则是指在服务器主线与各分支之间设置一个“岗位”,由该岗位实现监控多路连接中数据状态的功能,若某路连接中数据就绪,就通知服务器,使主程序对该路请求作出处理。
与多进程和多线程并发服务器相比,I/O多路转接服务器实现了I/O多路复用,系统不必创建多进程或多线程,也不必维护多个进程或线程,因此大大降低了系统开销。
Linux系统实现I/O多路转接函数有:
- select()
- poll()
- epoll()
2 select函数
图 select通信模型示意图
- 使用select搭建的I/O多路转接服务器是一种基于非阻塞的服务器;
- 当有客户端连接请求到达时,accept会返回一个文件描述符,该文件描述符会被存储到由select监控的文件描述符表中,每个文件描述符对应的文件都可进行I/O操作,因此select可通过监控表中各个文件描述符,来获取对应的客户端I/O状态。
- 若每路程序中都没有数据到达,线程将阻塞在select上;否则select将已就绪客户端程序的数量返回到服务器。
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
struct timeval{long tv_sec;long tv_user;
};
功能: 监视客户端I/O接口的状态
参数说明:
- nfds:设置select监控的文件描述符的范围,需设置为文件描述符最大值加1;
- readfds:可读取数据的文件描述符集,fd_set实质为长整型,这些集合中的每一位都对应一个文件描述符的状态;若集合参数被设置为NULL,表示不关心文件的对应状态;
- writefds:可写入数据的文件描述符集;
- exceptfds:发生异常的文件描述符集;
- timeout:设置select的阻塞时长,其取值有如下几种情况:
– 若timeval=NULL,表示永远等待;
– 若timeval>0,表示等待固定时长;
– 若timeval=0,select将在检查过指定文件描述符后立即返回(轮询)。
Linux系统提供了一系列操作文件描述符集的函数:
函数声明 | 函数功能 |
---|---|
void FD_CLR(int fd,fd_set *set); | 将集合中的文件描述符fd清除(将fd位置为0) |
int FD_ISSET(int fd,fd_set *set); | 测试集合中文件描述符fd是否存在于集合中,若存在则返回非0 |
void FD_SET(int fd,fd_set *set); | 将文件描述符fd添加到集合中(将fd位置为1) |
void FD_ZERO(fd_set *set); | 清除集合中所有的文件描述符(所有位置0) |
返回值说明:
- 大于0:表示已就绪文件描述符的数量,此种情况下某些文件可读写或有错误信息;
- 等于0:表示等待超时,没有可读写或错误的文件;
- -1:表示出错返回,同时errno将被设置。
特别说明:
- select可监控的进程数量是有限的,该数量受到两个因素的限制。
–第一个因素是进程可打开的文件数量;
–第二个因素是select中的集合fd_set的容量。 - 进程可打开文件的上限可通过ulimit –n命令或setrlimit函数设置,但系统所能打开的最大文件数也是有限的;
- select中集合fd_set的容量由宏FD_SETSIZE(定义在linux/posix_types.h中)指定;
- 即便通过重新编译内核的方式修改FD_SETSIZE,也不一定能提升select服务器的性能,因为若select一次监测的进程过多,单轮询便要耗费大量的时间。
【案例 1】使用select模型搭建多路I/O转接服务器。
- 服务器:接收客户端数据,并将接收到的数据转为大写,写回客户端;
- 客户端:向服务器发送数据,并将服务器返回的数据打印到终端。
selectClient.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80 //缓冲数组大小
#define SERV_PORT 8000 //端口号
int main() {struct sockaddr_in tempServAddr;char tempBuf[MAXLINE];int tempSockFd, tempDataLen;tempSockFd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&tempServAddr, sizeof(tempServAddr));tempServAddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &tempServAddr.sin_addr);tempServAddr.sin_port = htons(SERV_PORT);Connect(tempSockFd, (struct sockaddr *)&tempServAddr, sizeof(tempServAddr));while (fgets(tempBuf, MAXLINE, stdin) != NULL) {Write(tempSockFd, tempBuf, strlen(tempBuf));tempDataLen = Read(tempSockFd, tempBuf, MAXLINE);if (tempDataLen == 0){printf("the other side has been closed.\n");} else {Write(STDOUT_FILENO, buf, n);}//of if}//of whileClose(sockfd);return 0;
}//of mainselectServer.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "wrap.h"
#define MAXLINE 80 //缓冲数组大小
#define SERV_PORT 8000 //端口号
int main() {int i, tempMaxI, tempMaxFd, tempListenFd, tempConnFd, tempSockFd;int tempNReady, tempClient[FD_SETSIZE]; //FD_SETSIZE 默认为1024ssize_t tempDataLen;fd_set tempRset, tempAllset;char tempBuf[MAXLINE];char tempStr[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16socklen_t tempCliAddrLen;struct sockaddr_in tempCliAddr, tempServAddr;tempListenFd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&tempServAddr, sizeof(tempServAddr));tempServAddr.sin_family = AF_INET;tempServAddr.sin_addr.s_addr = htonl(INADDR_ANY);tempServAddr.sin_port = htons(SERV_PORT);Bind(tempListenFd, (struct sockaddr *)&tempServAddr, sizeof(tempServAddr));Listen(tempListenFd, 20); //默认最大128tempMaxFd = tempListenFd;tempMaxI = -1;//初始化监控列表for (i = 0; i < FD_SETSIZE; i++){tempClient[i] = -1; //使用-1初始化client[]中元素}//of for iFD_ZERO(&tempAllset);FD_SET(tempListenFd, &tempAllset); //将listenfd添加到文件描述符集中//循环监测处于连接状态进程的文件描述符for (;;) {//使用变量rset获取文件描述符集合tempRset = tempAllset;//记录就绪进程数量tempNReady = select(tempMaxFd + 1, &tempRset, NULL, NULL, NULL);if (tempNReady < 0){perr_exit("select error");}//of ifif (FD_ISSET(tempListenFd, &tempRset)){//有新连接请求到达则进行连接便处理连接请求tempCliAddrLen = sizeof(tempCliAddr);tempConnFd = Accept(tempListenFd, (struct sockaddr *)&tempCliAddr, &tempCliAddrLen);printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &tempCliAddr.sin_addr, tempStr, sizeof(tempStr)), ntohs(tempCliAddr.sin_port));for (i = 0; i < FD_SETSIZE; i++){if (tempClient[i] < 0) {tempClient[i] = tempConnFd ; //将文件描述符connfd保存到client[]中break;}//of if}//of for iif (i == FD_SETSIZE) { //判断连接数是否已达上限fputs("too many clients\n", stderr);exit(1);}//of ifFD_SET(tempConnFd, &tempAllset); //添加新文件描述符到监控信号集中if (tempConnFd > tempMaxFd) { //更新最大文件描述符tempMaxFd = tempConnFd;}//of ifif (i > tempMaxI) { //更新client[]最大下标值tempMaxI = i;}//of if//若无文件描述符就绪,便返回select,继续阻塞监测剩余的文件描述符if (--tempNReady == 0){continue;}//of if}//of if//遍历文件描述符集,处理已就绪的文件描述符for (i = 0; i <= tempMaxI; i++) {if ((tempSockFd = tempClient[i]) < 0){continue;}//of ifif (FD_ISSET(tempSockFd, &tempRset)) {//n=0,client就绪但未读到数据,表示client将关闭连接if ((tempDataLen = Read(tempSockFd, tempBuf, MAXLINE)) == 0) {//关闭服务器端连接Close(tempSockFd);FD_CLR(tempSockFd, &tempAllset); //清除集合中对应的文件描述符tempClient[i] = -1;} else {//处理获取的数据int j;for (j = 0; j < tempDataLen; j++){tempBuf[j] = toupper(tempBuf[j]);}//of for jWrite(sockfd, buf, tempDataLen);}//of ifif (--tempNReady == 0){break;}//of if}//of if}//of for i}//of forClose(tempListenFd);return 0;
}//of main