网络IO模型之多路复用

一.select函数

Linux中可以用select函数统一监视多个文件描述符。

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd, 
          fd_set* readset, 
          fd_set* writeset, 
          fd_set* exceptset, 
          const struct timeval* timeout);

参数:
maxfd:监视对象文件描述符数量。(最大的文件描述符值加1)
readset: 将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值。
writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值。
exceptset: 将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值。
timeout: 调用select后,为防止陷入无限阻塞状态,传递超时信息。
返回值:错误返回-1,超时返回0。因关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。

select函数实现I/O复用服务器

//server.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/select.h>

#define BUF_SIZE 100

void error_handing(char* buf);

int main(int argc, char* argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;

    socklen_t adr_sz;

    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE];
    if(argc != 2)
    {
        printf("Usage: %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handing("bind() error");
    if(listen(serv_sock, 5) == -1)
        error_handing("listen() error");

    FD_ZERO(&reads);
    FD_SET(serv_sock, &reads); //将服务端套接字注册入fd_set,即添加了服务器端套接字为监视对象
    fd_max = serv_sock;

 /* 超时不能while外面设置!
    因为调用select后,结构体timeval的成员tv_sec和tv_usec的值将被替换为超时前剩余时间.
    调用select函数前,每次都需要初始化timeval结构体变量.
    timeout.tv_sec = 5;
    timeout.tv_usec = 5000;
  */
    while(1)
    {
        cpy_reads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;

        //无限循环调用select 监视可读事件
        if((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1)
        {
            perror("select error");break;
        }
        if(fd_num == 0)
            continue;

        for(i = 0; i < fd_max + 1; i++)
        {
            if(FD_ISSET(i, &cpy_reads))
            {
                /*发生状态变化时,首先验证服务器端套接字中是否有变化.
                ①若是服务端套接字变化,接受连接请求。
                ②若是新客户端连接,注册与客户端连接的套接字文件描述符.
                */
                if(i == serv_sock)
                {
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                    FD_SET(clnt_sock, &reads);
                    if(fd_max < clnt_sock)
                        fd_max = clnt_sock;
                    printf("connected client: %d \n", clnt_sock);
                }
                else    
                {
                    str_len = read(i, buf, BUF_SIZE);
                    if(str_len == 0)    //读取数据完毕关闭套接字
                    {
                        FD_CLR(i, &reads);//从reads中删除相关信息
                        close(i);
                        printf("closed client: %d \n", i);
                    }
                    else
                    {
                        write(i, buf, str_len);//执行回声服务  即echo
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

void error_handing(char* buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

select的缺陷
(1)select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数

(2)解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力

二.poll函数

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,
根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,
而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

函数格式如下所示:

#include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

参数
1.fd文件描述符集合

struct pollfd{
    int fd;         /*文件描述符,如果小于0。这个文件描述符将会被进程忽略*/
    short events;   /*注册的事件,即感兴趣的事件。可以是一个事件,也可以是多个事件的按位或*/
    short revents; /*实际发生的事件,由内核来赋值。需要与某种类型的事件按位与来确定某种事件是否发生*/
};

2.要监视的描述符的数目。
3.超时时间

poll实现:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main(int argc, char* argv[])
{
	struct pollfd fds[2];//文件描述符
	fds[0].fd = 0;
	fds[0].events = POLLIN;//对读事件感兴趣
	fds[0].revents = 0;//输出型
 
	fds[1].fd = 1;
	fds[1].events = POLLOUT;//对写事件感兴趣
	fds[1].revents = 0;
 
	char buf[1024];
	int done = 0;
	int i = 0;
 
	int timeout = 1000;//1000ms 1秒
 
	while (!done)
	{
		int ret = poll(fds, sizeof(fds) / sizeof(fds[0]), timeout);
		switch (ret)
		{
		case -1:
			perror("poll");
			exit(2);
			break;
		case 0:
			printf("timeout...");
		default:
                        //有事件就绪,要判断哪个文件描述符的事件所以要遍历fds
                       for (i = 0; i < sizeof(fds) / sizeof(fds[0]); ++i)
			{
                                //是否可读 fds[i]关心的是读,读事件发生 
                               if (fds[i].fd == 0 && (fds[i].revents&POLLIN))
				{
					memset(buf, '\0', sizeof(buf));
					ssize_t _s = read(0, buf, sizeof(buf) - 1);
					if (_s > 0)
					{
						buf[_s - 1] = '\0';
						if (strncmp(buf, "quit", 4) == 0)
						{
							close(fds[i].fd);
							exit(0);
						}
						printf("echo#.%s\n", buf);
					}
				}
				if (fds[i].fd == 1 && (fds[1].revents&POLLOUT))
				{
					memset(buf, '\0', sizeof(buf));
					strcpy(buf, "Hello!");
					printf("echo#.%s\n", buf);
					sleep(3);
				}
			}
			break;
		}
	}
	return 0;
}

poll总结:
1.相比于select机制,poll机制采用链表来进行文件描述符的存储,因此它并没有最大连接数的限制。
2.大量的 fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
3. poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

三.epoll函数

在linux新的内核中,有了一种替换它的机制,就是epoll。

epoll接口

#include <sys/epoll.h>

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

1.epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。
2.epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
3.epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。

工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:
  当epoll_ wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:
  当epoll_ wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
  epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

epoll服务端例子

#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

using namespace std;

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

void setnonblocking(int sock)
{
    int opts;
    opts=fcntl(sock,F_GETFL);
    if(opts<0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts|O_NONBLOCK;
    if(fcntl(sock,F_SETFL,opts)<0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}

int main(int argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;


    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
        return 1;
    }



    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

    struct epoll_event ev,events[20];
    //生成用于处理accept的epoll专用的文件描述符

    epfd=epoll_create(256);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    //把socket设置为非阻塞方式

    //setnonblocking(listenfd);

    //设置与要处理的事件相关的文件描述符

    ev.data.fd=listenfd;
    //设置要处理的事件类型

    ev.events=EPOLLIN|EPOLLET;
    //ev.events=EPOLLIN;

    //注册epoll事件

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);

    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
        //等待epoll事件的发生

        nfds=epoll_wait(epfd,events,20,500);
        //处理所发生的所有事件

        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。

            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                //setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                //设置用于读操作的文件描述符

                ev.data.fd=connfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN;

                //注册ev

                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。

            {
                cout << "EPOLLIN" << endl;
                if ( (sockfd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    } else
                        std::cout<<"readline error"<<std::endl;
                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                line[n] = '/0';
                cout << "read " << line << endl;
                //设置用于写操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的写操作事件

                ev.events=EPOLLOUT|EPOLLET;
                //修改sockfd上要处理的事件为EPOLLOUT

                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

            }
            else if(events[i].events&EPOLLOUT) // 如果有数据发送

            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //设置用于读操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //修改sockfd上要处理的事件为EPOLIN

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值