网络编程4:高并发服务器——多路I/O转接服务器设计(select、poll、epoll)

返回:Linux网络编程学习笔记

select、poll、epoll三者优缺点对比

多路I/O函数 优点 缺点
select 1.函数诞生时间早,跨平台性好,windows、linux、macOS、Unix、类Unix均支持 1.监听的文件描述符上限为1024;2.需要添加业务逻辑,来监听满足条件的fd,代码编程难度会提高(业务逻辑是指添加一个1024大小的数组,通过数组来管理满足条件的fd,详见代码)
poll 1.自带数组结构,可将监听和返回事件集合分离;2.可以拓展文件描述符监听上限,突破1024 1.不能跨平台,仅Linux支持;2.无法直接定位满足监听事件的文件描述符,编码难度较大
epoll 1.解决了监听事件难于管理的问题,编程比较简单;2.监听事件上限可以突破1024 1.不能跨平台,仅Linux支持。

 

1. select

1.1 select输入参数说明

       /* According to earlier standards */
       #include 
       #include 
       #include 

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

参数说明:
int nfds                       //监听的最大文件描述符+1,作为监听循环的上限
fd_set *read_fds,        //监视的可读文件句柄集合。
fd_set *write_fds,       //监视的可写文件句柄集合。
fd_set *excepr_fds,      //监视的异常文件句柄集合。
struct timeval *timeout  //本次select()的超时结束时间。

1.2 select函数使用方法

方法一:通过循环判断文件lfd+1到maxfd之间是否有读事件,来操作套接字的读和写。

缺点:效率比较低,在lfd+1到maxfd之间如果有文件关闭了,也会进入FD_ISSET的判断。

网络编程4:高并发服务器——多路I/O转接服务器设计(select、poll、epoll)_第1张图片

方法二:定义数组int client[1024],用来管理lfd+1到maxfd之间需要监听的文件,具体方法如下:

1.当lfd有读事件时会生成cfd,通过for循环查找靠前没有使用的client元素,使client[i]=cfd(未使用的client[i]会置-1);

2.在使用FD_ISSET之前,先判断client[i]是否大于0,大于0的情况才进行判断。

具体的操作如下:

网络编程4:高并发服务器——多路I/O转接服务器设计(select、poll、epoll)_第2张图片

参考链接:linux select函数详解

1.3 使用select函数实现多路I/O转发

详见底部代码

主要疑问:select函数实现的是一个非阻塞忙轮询的逻辑,accept函数为何不会阻塞?

原因很简单,下方代码在使用accept函数之前,首先通过FD_ISSET函数判断了listenfd是有读事件发生的,所以不阻塞。只有在listenfd没有读事件且使用accept函数的情况下,才会阻塞,通过条件判断规避了阻塞的情况。

        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nready < 0)
            perr_exit("select error");

        if (FD_ISSET(listenfd, &rset)) {                        /* 说明有新的客户端链接请求 */

            clie_addr_len = sizeof(clie_addr);
            connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);       /* Accept 不会阻塞 */

            FD_SET(connfd, &allset);                            /* 向监控文件描述符集合allset添加新的文件描述符connfd */

            if (maxfd < connfd)
                maxfd = connfd;

            if (0 == --nready)                                  /* 只有listenfd有事件, 后续的 for 不需执行 */
                continue;
        } 

2. poll

2.1 poll函数输入参数说明

NAME
       poll, ppoll - wait for some event on a file descriptor

SYNOPSIS
       #include 

       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

       #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include 
       #include 

       int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *tmo_p, const sigset_t *sigmask);

DESCRIPTION
       poll()  performs  a  similar  task to select(2): it waits for one of a set of file descriptors to
       become ready to perform I/O.

       The set of file descriptors to be monitored is specified in the fds argument, which is  an  array
       of structures of the following form:

           struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

说明:

(1)struct pollfd结构体介绍

           struct pollfd {
               int   fd;             //待监听文件描述符
               short events;    //待监听文件描述符对应的监听事件,可以为POLLIN、POLLOUT、POLLERR ...
               short revents;   //传入时赋值0。如果满足对应事件返回非0。
           };

(2)nfds 监听数组实际的有效监听个数

(3)timeout 超时时长。单位ms(-1 阻塞;0 不阻塞,立即返回;>0 超时时长,单位毫秒)

poll函数返回值:满足对应监听事件的文件描述符总个数。

2.2 poll函数使用方法

poll函数的使用与select类似,使用前需要定义struct pollfd类型结构体,例如监听1024个文件描述符的定义如下:

struct pollfd pfds[1024]; 

在使用poll函数之前,需要先设置pfds的成员,以监听套接字lfd为例,使用第0号元素结构体pfds[0]来监听lfd,监听的事件为读事件POLLIN,revent置0;

使用poll之后,发现有连接事件出现,就通过accept函数创建连接套接字cfd,并将cfd放进pfds里面进行监听;

如果连接套接字上有读事件发生,就调用read和write进行处理,程序的具体流程如下,代码省略。

网络编程4:高并发服务器——多路I/O转接服务器设计(select、poll、epoll)_第3张图片

3. epoll

3.1 epoll的三把斧

(1)epoll_create

NAME
       epoll_create, epoll_create1 - open an epoll file descriptor

SYNOPSIS
       #include 

       int epoll_create(int size);
       int epoll_create1(int flags);

size:创建红黑树的监听节点数量(仅供内核参考,分配对应大小的资源)

epoll_create返回值:成功返回指向新创建的红黑树根节点的fd;失败返回 -1和errno。

(2)epoll_ctl

NAME
       epoll_ctl - control interface for an epoll file descriptor

SYNOPSIS
       #include 

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

epfd:epoll_create创建的红黑树根节点的文件描述符

op:对应如下三种操作

        EPOLL_CTL_ADD:将fd添加到监听红黑树

        EPOLL_CTL_MOD:修改fd在监听红黑树上的监听事件

        EPOLL_CTL_DEL:将fd从监听红黑树上摘下

fd:待监听的文件描述符

event:传入参数,epoll监听事件结构体,events为返回的监听事件类型(可选EPOLLIN、EPOLLOUT、EPOLLER等)

data为一联合体,其泛参成员ptr可用于构建epoll反应堆模型。

           struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

           typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

epoll_ctl:成功返回0,失败返回-1和errno。

(3)epoll_wait

NAME
       epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor

SYNOPSIS
       #include 

       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
       int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);

epfd:epoll_create创建的红黑树根节点的文件描述符

events:数组型传出参数,满足监听条件的文件描述符对应的epoll_event结构体

maxevents:数组元素的总个数

timeout:超时时间,-1 阻塞;0 立即返回,不阻塞;>0 单位ms。

epoll_wait返回值:成功返回有多少个文件描述符就绪,时间到返回0,出错返回-1。

3.2 epoll函数的使用方法

epoll函数的编程难度比较简单,分为三步:

第1步创建监听红黑树树根epollfd;

第2步将listen_socket添加到红黑树上;

第3步实施监听,当返回listen_sock读事件时,创建conn_sock,并添加到监听红黑树,当返回conn_sock对应的读事件时,进行服务器与客户端相关的操作。

使用举例:

           #define MAX_EVENTS 10
           struct epoll_event ev, events[MAX_EVENTS];
           int listen_sock, conn_sock, nfds, epollfd;

           /* Code to set up listening socket, 'listen_sock',
              (socket(), bind(), listen()) omitted */

           epollfd = epoll_create1(0);  //第1步创建监听红黑树树根epollfd
           if (epollfd == -1) {
               perror("epoll_create1");
               exit(EXIT_FAILURE);
           }

           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;  //第2步将listen_socket添加到红黑树上
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }

           for (;;) {  //第3步实施监听,当返回listen_sock读事件时,创建conn_sock,并添加到监听红黑树,当返回conn_sock对应的读事件时,进行服务器与客户端相关的操作
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_wait");
                   exit(EXIT_FAILURE);
               }

               for (n = 0; n < nfds; ++n) {
                   if (events[n].data.fd == listen_sock) {
                       conn_sock = accept(listen_sock,
                                          (struct sockaddr *) &addr, &addrlen);
                       if (conn_sock == -1) {
                           perror("accept");
                           exit(EXIT_FAILURE);
                       }
                       setnonblocking(conn_sock);
                       ev.events = EPOLLIN | EPOLLET;
                       ev.data.fd = conn_sock;
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                                   &ev) == -1) {
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       do_use_fd(events[n].data.fd);
                   }
               }
           }

4. 程序附件

4.1 使用select方法一实现服务器端

说明:编译时需要添加wrap.h和wrap.c

#include 
#include 
#include 
#include 
#include 
#include 

#include "wrap.h"

#define SERV_PORT 6666

int main(int argc, char *argv[])
{
    int i, j, n, nready;

    int maxfd = 0;

    int listenfd, connfd;

    char buf[BUFSIZ];         /* #define INET_ADDRSTRLEN 16 */

    struct sockaddr_in clie_addr, serv_addr;
    socklen_t clie_addr_len;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);  
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family= AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port= htons(SERV_PORT);
    Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    Listen(listenfd, 128);
    

    fd_set rset, allset;                            /* rset 读事件文件描述符集合 allset用来暂存 */

    maxfd = listenfd;

    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);                                  /* 构造select监控文件描述符集 */

    while (1) {   
        rset = allset;                                          /* 每次循环时都从新设置select监控信号集 */
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nready < 0)
            perr_exit("select error");

        if (FD_ISSET(listenfd, &rset)) {                        /* 说明有新的客户端链接请求 */

            clie_addr_len = sizeof(clie_addr);
            connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);       /* Accept 不会阻塞 */

            FD_SET(connfd, &allset);                            /* 向监控文件描述符集合allset添加新的文件描述符connfd */

            if (maxfd < connfd)
                maxfd = connfd;

            if (0 == --nready)                                  /* 只有listenfd有事件, 后续的 for 不需执行 */
                continue;
        } 

        for (i = listenfd+1; i <= maxfd; i++) {                 /* 检测哪个clients 有数据就绪 */

            if (FD_ISSET(i, &rset)) {

                if ((n = Read(i, buf, sizeof(buf))) == 0) {    /* 当client关闭链接时,服务器端也关闭对应链接 */
                    Close(i);
                    FD_CLR(i, &allset);                        /* 解除select对此文件描述符的监控 */

                } else if (n > 0) {

                    for (j = 0; j < n; j++)
                        buf[j] = toupper(buf[j]);
                    Write(i, buf, n);
                }
            }
        }
    }

    Close(listenfd);

    return 0;
}

4.2 使用select方法二实现服务器端

使用client[1024]数组来打理需要监听的文件

说明:编译时需要添加wrap.h和wrap.c

#include 
#include 
#include 
#include 
#include 
#include 

#include "wrap.h"

#define SERV_PORT 6666

int main(int argc, char *argv[])
{
    int i, j, n, maxi;

    int nready, client[FD_SETSIZE];                 /* 自定义数组client, 防止遍历1024个文件描述符  FD_SETSIZE默认为1024 */
    int maxfd, listenfd, connfd, sockfd;
    char buf[BUFSIZ], str[INET_ADDRSTRLEN];         /* #define INET_ADDRSTRLEN 16 */

    struct sockaddr_in clie_addr, serv_addr;
    socklen_t clie_addr_len;
    fd_set rset, allset;                            /* rset 读事件文件描述符集合 allset用来暂存 */

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family= AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port= htons(SERV_PORT);

    Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    Listen(listenfd, 128);

    maxfd = listenfd;                                           /* 起初 listenfd 即为最大文件描述符 */

    maxi = -1;                                                  /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;                                         /* 用-1初始化client[] */

    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);                                  /* 构造select监控文件描述符集 */

    while (1) {   
        rset = allset;                                          /* 每次循环时都从新设置select监控信号集 */

        nready = select(maxfd+1, &rset, NULL, NULL, NULL);  //2  1--lfd  1--connfd
        if (nready < 0)
            perr_exit("select error");

        if (FD_ISSET(listenfd, &rset)) {                        /* 说明有新的客户端链接请求 */

            clie_addr_len = sizeof(clie_addr);
            connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);       /* Accept 不会阻塞 */
            printf("received from %s at PORT %d\n",
                    inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
                    ntohs(clie_addr.sin_port));

            for (i = 0; i < FD_SETSIZE; i++)
                if (client[i] < 0) {                            /* 找client[]中没有使用的位置 */
                    client[i] = connfd;                         /* 保存accept返回的文件描述符到client[]里 */
                    break;
                }

            if (i == FD_SETSIZE) {                              /* 达到select能监控的文件个数上限 1024 */
                fputs("too many clients\n", stderr);
                exit(1);
            }

            FD_SET(connfd, &allset);                            /* 向监控文件描述符集合allset添加新的文件描述符connfd */

            if (connfd > maxfd)
                maxfd = connfd;                                 /* select第一个参数需要 */

            if (i > maxi)
                maxi = i;                                       /* 保证maxi存的总是client[]最后一个元素下标 */

            if (--nready == 0)
                continue;
        } 

        for (i = 0; i <= maxi; i++) {                               /* 检测哪个clients 有数据就绪 */

            if ((sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {

                if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {    /* 当client关闭链接时,服务器端也关闭对应链接 */
                    Close(sockfd);
                    FD_CLR(sockfd, &allset);                        /* 解除select对此文件描述符的监控 */
                    client[i] = -1;
                } else if (n > 0) {
                    for (j = 0; j < n; j++)
                        buf[j] = toupper(buf[j]);
                    Write(sockfd, buf, n);
                    Write(STDOUT_FILENO, buf, n);
                }
                if (--nready == 0)
                    break;                                          /* 跳出for, 但还在while中 */
            }
        }
    }
    Close(listenfd);
    return 0;
}

wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);

#endif

wrap.c

#include 
#include 
#include 
#include 
#include 

void perr_exit(const char *s)
{
	perror(s);
	exit(-1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
	int n;

again:
	if ((n = accept(fd, sa, salenptr)) < 0) {
		if ((errno == ECONNABORTED) || (errno == EINTR))
			goto again;
		else
			perr_exit("accept error");
	}
	return n;
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
    int n;

	if ((n = bind(fd, sa, salen)) < 0)
		perr_exit("bind error");

    return n;
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    int n;

	if ((n = connect(fd, sa, salen)) < 0)
		perr_exit("connect error");

    return n;
}

int Listen(int fd, int backlog)
{
    int n;

	if ((n = listen(fd, backlog)) < 0)
		perr_exit("listen error");

    return n;
}

int Socket(int family, int type, int protocol)
{
	int n;

	if ((n = socket(family, type, protocol)) < 0)
		perr_exit("socket error");

	return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = read(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = write(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}

int Close(int fd)
{
    int n;
	if ((n = close(fd)) == -1)
		perr_exit("close error");

    return n;
}

/*参三: 应该读取的字节数*/
ssize_t Readn(int fd, void *vptr, size_t n)
{
	size_t  nleft;              //usigned int 剩余未读取的字节数
	ssize_t nread;              //int 实际读到的字节数
	char   *ptr;

	ptr = vptr;
	nleft = n;

	while (nleft > 0) {
		if ((nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;
			else
				return -1;
		} else if (nread == 0)
			break;

		nleft -= nread;
		ptr += nread;
	}
	return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
	size_t nleft;
	ssize_t nwritten;
	const char *ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
			if (nwritten < 0 && errno == EINTR)
				nwritten = 0;
			else
				return -1;
		}

		nleft -= nwritten;
		ptr += nwritten;
	}
	return n;
}

static ssize_t my_read(int fd, char *ptr)
{
	static int read_cnt;
	static char *read_ptr;
	static char read_buf[100];

	if (read_cnt <= 0) {
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return -1;
		} else if (read_cnt == 0)
			return 0;
		read_ptr = read_buf;
	}
	read_cnt--;
	*ptr = *read_ptr++;

	return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t n, rc;
	char    c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c  == '\n')
				break;
		} else if (rc == 0) {
			*ptr = 0;
			return n - 1;
		} else
			return -1;
	}
	*ptr  = 0;

	return n;
}

 

你可能感兴趣的:(linux网络编程,多路I/O)