0


计算机网络编程 | 多路I/O转接服务器

在这里插入图片描述

欢迎关注博主 Mindtechnist 或加入【Linux C/C++/Python社区】一起学习和分享Linux、C、C++、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和技术。


多路I/O转接服务器


专栏:《网络编程》


多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种,下面一一介绍并给出代码实现。

select

  • select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。
  • 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。
#include<sys/select.h>/* According to earlier standards */#include<sys/time.h>#include<sys/types.h>#include<unistd.h>intselect(int nfds, fd_set *readfds, fd_set *writefds,
            fd_set *exceptfds,structtimeval*timeout);

nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
①NULL,永远等下去
②设置timeval,等待固定时间
③设置timeval里时间均为0,检查描述字后立即返回,轮询

structtimeval{long tv_sec;/* seconds */long tv_usec;/* microseconds */};voidFD_CLR(int fd, fd_set *set);//把文件描述符集合里fd清0intFD_ISSET(int fd, fd_set *set);//测试文件描述符集合里fd是否置1voidFD_SET(int fd, fd_set *set);//把文件描述符集合里fd位置1voidFD_ZERO(fd_set *set);//把文件描述符集合里所有位清0

server实现

/* server.c */#include<stdio.h>#include<stdlib.h>#include<string.h>#include<netinet/in.h>#include<arpa/inet.h>#include"wrap.h"#defineMAXLINE80#defineSERV_PORT6666intmain(int argc,char*argv[]){int i, maxi, maxfd, listenfd, connfd, sockfd;int nready, client[FD_SETSIZE];/* FD_SETSIZE 默认为 1024 */ssize_t n;
    fd_set rset, allset;char buf[MAXLINE];char str[INET_ADDRSTRLEN];/* #define INET_ADDRSTRLEN 16 */socklen_t cliaddr_len;structsockaddr_in cliaddr, servaddr;

    listenfd =Socket(AF_INET, SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
servaddr.sin_port =htons(SERV_PORT);Bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));Listen(listenfd,20);/* 默认最大128 */

maxfd = listenfd;/* 初始化 */
maxi =-1;/* client[]的下标 */for(i =0; i < FD_SETSIZE; i++)
    client[i]=-1;/* 用-1初始化client[] */FD_ZERO(&allset);FD_SET(listenfd,&allset);/* 构造select监控文件描述符集 */for(;;){
    rset = allset;/* 每次循环时都从新设置select监控信号集 */
    nready =select(maxfd+1,&rset,NULL,NULL,NULL);if(nready <0)perr_exit("select error");if(FD_ISSET(listenfd,&rset)){/* new client connection */
        cliaddr_len =sizeof(cliaddr);
        connfd =Accept(listenfd,(structsockaddr*)&cliaddr,&cliaddr_len);printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr, str,sizeof(str)),ntohs(cliaddr.sin_port));for(i =0; i < FD_SETSIZE; i++){if(client[i]<0){
                client[i]= connfd;/* 保存accept返回的文件描述符到client[]里 */break;}}/* 达到select能监控的文件个数上限 1024 */if(i == FD_SETSIZE){fputs("too many clients\n",stderr);exit(1);}FD_SET(connfd,&allset);/* 添加一个新的文件描述符到监控信号集里 */if(connfd > maxfd)
            maxfd = connfd;/* select第一个参数需要 */if(i > maxi)
            maxi = i;/* 更新client[]最大下标值 */if(--nready ==0)continue;/* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,
                                        负责处理未处理完的就绪文件描述符 */}for(i =0; i <= maxi; i++){/* 检测哪个clients 有数据就绪 */if((sockfd = client[i])<0)continue;if(FD_ISSET(sockfd,&rset)){if((n =Read(sockfd, buf, MAXLINE))==0){Close(sockfd);/* 当client关闭链接时,服务器端也关闭对应链接 */FD_CLR(sockfd,&allset);/* 解除select监控此文件描述符 */
                    client[i]=-1;}else{int j;for(j =0; j < n; j++)
                        buf[j]=toupper(buf[j]);Write(sockfd, buf, n);}if(--nready ==0)break;}}}close(listenfd);return0;}

client实现

/* client.c */#include<stdio.h>#include<string.h>#include<unistd.h>#include<netinet/in.h>#include"wrap.h"#defineMAXLINE80#defineSERV_PORT6666intmain(int argc,char*argv[]){structsockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;

    sockfd =Socket(AF_INET, SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
    servaddr.sin_port =htons(SERV_PORT);Connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr));while(fgets(buf, MAXLINE,stdin)!=NULL){Write(sockfd, buf,strlen(buf));
        n =Read(sockfd, buf, MAXLINE);if(n ==0)printf("the other side has been closed.\n");elseWrite(STDOUT_FILENO, buf, n);}Close(sockfd);return0;}

pselect
pselect原型如下。此模型应用较少。

#include<sys/select.h>intpselect(int nfds, fd_set *readfds, fd_set *writefds,
            fd_set *exceptfds,conststructtimespec*timeout,constsigset_t*sigmask);structtimespec{long tv_sec;/* seconds */long tv_nsec;/* nanoseconds */};

用sigmask替代当前进程的阻塞信号集,调用返回后还原原有阻塞信号集。

poll

#include<poll.h>intpoll(structpollfd*fds,nfds_t nfds,int timeout);structpollfd{int fd;/* 文件描述符 */short events;/* 监控的事件 */short revents;/* 监控事件中满足条件返回的事件 */};

POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
POLLRDNORM 数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级可读数据
POLLOUT 普通或带外数据可写
POLLWRNORM 数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件

nfds 监控数组中有多少文件描述符需要被监控

timeout 毫秒级等待
-1:阻塞等,#define INFTIM -1 , Linux中没有定义此宏。
0:立即返回,不阻塞进程。

0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值。

如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0。

server实现

/* server.c */#include<stdio.h>#include<stdlib.h>#include<string.h>#include<netinet/in.h>#include<arpa/inet.h>#include<poll.h>#include<errno.h>#include"wrap.h"#defineMAXLINE80#defineSERV_PORT6666#defineOPEN_MAX1024intmain(int argc,char*argv[]){int i, j, maxi, listenfd, connfd, sockfd;int nready;ssize_t n;char buf[MAXLINE], str[INET_ADDRSTRLEN];socklen_t clilen;structpollfd client[OPEN_MAX];structsockaddr_in cliaddr, servaddr;

    listenfd =Socket(AF_INET, SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
    servaddr.sin_port =htons(SERV_PORT);Bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));Listen(listenfd,20);

    client[0].fd = listenfd;
    client[0].events = POLLRDNORM;/* listenfd监听普通读事件 */for(i =1; i < OPEN_MAX; i++)
        client[i].fd =-1;/* 用-1初始化client[]里剩下元素 */
    maxi =0;/* client[]数组有效元素中最大元素下标 */for(;;){
        nready =poll(client, maxi+1,-1);/* 阻塞 */if(client[0].revents & POLLRDNORM){/* 有客户端链接请求 */
            clilen =sizeof(cliaddr);
            connfd =Accept(listenfd,(structsockaddr*)&cliaddr,&clilen);printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr, str,sizeof(str)),ntohs(cliaddr.sin_port));for(i =1; i < OPEN_MAX; i++){if(client[i].fd <0){
                    client[i].fd = connfd;/* 找到client[]中空闲的位置,存放accept返回的connfd */break;}}if(i == OPEN_MAX)perr_exit("too many clients");

            client[i].events = POLLRDNORM;/* 设置刚刚返回的connfd,监控读事件 */if(i > maxi)
                maxi = i;/* 更新client[]中最大元素下标 */if(--nready <=0)continue;/* 没有更多就绪事件时,继续回到poll阻塞 */}for(i =1; i <= maxi; i++){/* 检测client[] */if((sockfd = client[i].fd)<0)continue;if(client[i].revents &(POLLRDNORM | POLLERR)){if((n =Read(sockfd, buf, MAXLINE))<0){if(errno == ECONNRESET){/* 当收到 RST标志时 *//* connection reset by client */printf("client[%d] aborted connection\n", i);Close(sockfd);
                        client[i].fd =-1;}else{perr_exit("read error");}}elseif(n ==0){/* connection closed by client */printf("client[%d] closed connection\n", i);Close(sockfd);
                    client[i].fd =-1;}else{for(j =0; j < n; j++)
                        buf[j]=toupper(buf[j]);Writen(sockfd, buf, n);}if(--nready <=0)break;/* no more readable descriptors */}}}return0;}

client实现

/* client.c */#include<stdio.h>#include<string.h>#include<unistd.h>#include<netinet/in.h>#include"wrap.h"#defineMAXLINE80#defineSERV_PORT6666intmain(int argc,char*argv[]){structsockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;

    sockfd =Socket(AF_INET, SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
    servaddr.sin_port =htons(SERV_PORT);Connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr));while(fgets(buf, MAXLINE,stdin)!=NULL){Write(sockfd, buf,strlen(buf));
        n =Read(sockfd, buf, MAXLINE);if(n ==0)printf("the other side has been closed.\n");elseWrite(STDOUT_FILENO, buf, n);}Close(sockfd);return0;}

ppoll
GNU定义了ppoll(非POSIX标准),可以支持设置信号屏蔽字。

#define_GNU_SOURCE/* See feature_test_macros(7) */#include<poll.h>intppoll(structpollfd*fds,nfds_t nfds,conststructtimespec*timeout_ts,constsigset_t*sigmask);

epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
可以使用cat命令查看一个进程可以打开的socket描述符上限。

cat /proc/sys/fs/file-max

如有需要,可以通过修改配置文件的方式修改该上限值。

sudovi /etc/security/limits.conf

在文件尾部写入以下配置,soft软限制,hard硬限制。

* soft nofile 65536
* hard nofile 100000

基础API
创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。

#include<sys/epoll.h>intepoll_create(int size)//size:监听数目

控制某个epoll监控的文件描述符上的事件:注册、修改、删除。

#include<sys/epoll.h>intepoll_ctl(int epfd,int op,int fd,structepoll_event*event)

epfd: 为epoll_creat的句柄。
op: 表示动作,用3个宏来表示:
-EPOLL_CTL_ADD (注册新的fd到epfd),
-EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
-EPOLL_CTL_DEL (从epfd删除一个fd);
event: 告诉内核需要监听的事件。

structepoll_event{
    __uint32_t events;/* Epoll events */epoll_data_t data;/* User data variable */};typedefunion epoll_data {void*ptr;int fd;uint32_t u32;uint64_t u64;}epoll_data_t;

EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
EPOLLOUT: 表示对应的文件描述符可以写。
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
EPOLLERR: 表示对应的文件描述符发生错误。
EPOLLHUP: 表示对应的文件描述符被挂断。
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

等待所监控文件描述符上有事件的产生,类似于select()调用。

#include<sys/epoll.h>intepoll_wait(int epfd,structepoll_event*events,int maxevents,int timeout);

events: 用来存内核得到事件的集合。
maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。
timeout: 是超时时间
-1: 阻塞
0: 立即返回,非阻塞

0: 指定毫秒
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

server

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<netinet/in.h>#include<arpa/inet.h>#include<sys/epoll.h>#include<errno.h>#include"wrap.h"#defineMAXLINE80#defineSERV_PORT6666#defineOPEN_MAX1024intmain(int argc,char*argv[]){int i, j, maxi, listenfd, connfd, sockfd;int nready, efd, res;ssize_t n;char buf[MAXLINE], str[INET_ADDRSTRLEN];socklen_t clilen;int client[OPEN_MAX];structsockaddr_in cliaddr, servaddr;structepoll_event tep, ep[OPEN_MAX];

    listenfd =Socket(AF_INET, SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
    servaddr.sin_port =htons(SERV_PORT);Bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));Listen(listenfd,20);for(i =0; i < OPEN_MAX; i++)
        client[i]=-1;
    maxi =-1;

    efd =epoll_create(OPEN_MAX);if(efd ==-1)perr_exit("epoll_create");

    tep.events = EPOLLIN; tep.data.fd = listenfd;

    res =epoll_ctl(efd, EPOLL_CTL_ADD, listenfd,&tep);if(res ==-1)perr_exit("epoll_ctl");while(1){
        nready =epoll_wait(efd, ep, OPEN_MAX,-1);/* 阻塞监听 */if(nready ==-1)perr_exit("epoll_wait");for(i =0; i < nready; i++){if(!(ep[i].events & EPOLLIN))continue;if(ep[i].data.fd == listenfd){
                clilen =sizeof(cliaddr);
                connfd =Accept(listenfd,(structsockaddr*)&cliaddr,&clilen);printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr, str,sizeof(str)),ntohs(cliaddr.sin_port));for(j =0; j < OPEN_MAX; j++){if(client[j]<0){
                        client[j]= connfd;/* save descriptor */break;}}if(j == OPEN_MAX)perr_exit("too many clients");if(j > maxi)
                    maxi = j;/* max index in client[] array */

                tep.events = EPOLLIN; 
                tep.data.fd = connfd;
                res =epoll_ctl(efd, EPOLL_CTL_ADD, connfd,&tep);if(res ==-1)perr_exit("epoll_ctl");}else{
                sockfd = ep[i].data.fd;
                n =Read(sockfd, buf, MAXLINE);if(n ==0){for(j =0; j <= maxi; j++){if(client[j]== sockfd){
                            client[j]=-1;break;}}
                    res =epoll_ctl(efd, EPOLL_CTL_DEL, sockfd,NULL);if(res ==-1)perr_exit("epoll_ctl");Close(sockfd);printf("client[%d] closed connection\n", j);}else{for(j =0; j < n; j++)
                        buf[j]=toupper(buf[j]);Writen(sockfd, buf, n);}}}}close(listenfd);close(efd);return0;}

client

/* client.c */#include<stdio.h>#include<string.h>#include<unistd.h>#include<netinet/in.h>#include"wrap.h"#defineMAXLINE80#defineSERV_PORT6666intmain(int argc,char*argv[]){structsockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;

    sockfd =Socket(AF_INET, SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
    servaddr.sin_port =htons(SERV_PORT);Connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr));while(fgets(buf, MAXLINE,stdin)!=NULL){Write(sockfd, buf,strlen(buf));
        n =Read(sockfd, buf, MAXLINE);if(n ==0)printf("the other side has been closed.\n");elseWrite(STDOUT_FILENO, buf, n);}Close(sockfd);return0;}

在这里插入图片描述
在这里插入图片描述



本文转载自: https://blog.csdn.net/qq_43471489/article/details/130514602
版权归原作者 Mindtechnist 所有, 如有侵权,请联系我们删除。

“计算机网络编程 | 多路I/O转接服务器”的评论:

还没有评论