linux网络编程(四)多路I/O转接服务器
创始人
2024-01-25 01:41:15
0

文章目录

  • 1.多路I/O转接服务器
  • 2.select 方式的多路I/O转接服务器
  • 3.poll 方式的多路I/O转接服务器
  • 4.epoll 方式的多路I/O转接服务器

1.多路I/O转接服务器

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。

主要使用的方法有三种

2.select 方式的多路I/O转接服务器

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

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

#include 
/* According to earlier standards */
#include 
#include 
#include 
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);nfds: 		监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态readfds:	监控有读数据到达文件描述符集合,传入传出参数writefds:	监控写数据到达文件描述符集合,传入传出参数exceptfds:	监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数timeout:	定时阻塞监控时间,3种情况1.NULL,永远等下去2.设置timeval,等待固定时间3.设置timeval里时间均为0,检查描述字后立即返回,轮询struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */};void FD_CLR(int fd, fd_set *set); 	//把文件描述符集合里fd位清0int FD_ISSET(int fd, fd_set *set); 	//测试文件描述符集合里fd是否置1void FD_SET(int fd, fd_set *set); 	//把文件描述符集合里fd位置1void FD_ZERO(fd_set *set); 			//把文件描述符集合里所有位清0
server
/* server.c */
#include 
#include 
#include 
#include 
#include 
#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666int main(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;struct sockaddr_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, (struct sockaddr *)&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, (struct sockaddr *)&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);return 0;
}
client
/* client.c */
#include 
#include 
#include 
#include 
#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666int main(int argc, char *argv[])
{struct sockaddr_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, (struct sockaddr *)&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);return 0;
}
pselect
pselect原型如下。此模型应用较少,有需要的同学可参考select模型自行编写C/S
#include 
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);struct timespec {long tv_sec; /* seconds */long tv_nsec; /* nanoseconds */};用sigmask替代当前进程的阻塞信号集,调用返回后还原原有阻塞信号集

3.poll 方式的多路I/O转接服务器

#include 
int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd {int fd; /* 文件描述符 */short events; /* 监控的事件 */short revents; /* 监控事件中满足条件返回的事件 */};POLLIN			普通或带外优先数据可读,即POLLRDNORM | POLLRDBANDPOLLRDNORM		数据可读POLLRDBAND		优先级带数据可读POLLPRI 		高优先级可读数据POLLOUT		普通或带外数据可写POLLWRNORM		数据可写POLLWRBAND		优先级带数据可写POLLERR 		发生错误POLLHUP 		发生挂起POLLNVAL 		描述字不是一个打开的文件nfds 			监控数组中有多少文件描述符需要被监控timeout 		毫秒级等待-1:阻塞等,#define INFTIM -1 			Linux中没有定义此宏0:立即返回,不阻塞进程>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值

如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0。
相较于select而言,poll的优势:

  1. 传入、传出事件分离。无需每次调用时,重新设定监听事件。
  2. 文件描述符上限,可突破1024限制。能监控的最大上限数可使用配置文件调整。
server
/* server.c */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024int main(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;struct pollfd client[OPEN_MAX];struct sockaddr_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, (struct sockaddr *)&servaddr, sizeof(servaddr));Listen(listenfd, 20);client[0].fd = listenfd;client[0].events = POLLIN; 					/* 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 & POLLIN) { 		/* 有客户端链接请求 */clilen = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&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 = POLLIN; 		/* 设置刚刚返回的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 & POLLIN) {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");}} else if (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 */}}}return 0;
}
client
/* client.c */
#include 
#include 
#include 
#include 
#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666int main(int argc, char *argv[])
{struct sockaddr_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, (struct sockaddr *)&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);return 0;
}
ppoll
GNU定义了ppoll(非POSIX标准),可以支持设置信号屏蔽字,大家可参考poll模型自行实现C/S。#define _GNU_SOURCE /* See feature_test_macros(7) */
#include 
int ppoll(struct pollfd *fds, nfds_t nfds,const struct timespec *timeout_ts, const sigset_t *sigmask);

4.epoll 方式的多路I/O转接服务器

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

cat /proc/sys/fs/file-max

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

sudo vi /etc/security/limits.conf

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

* soft nofile 65536
* hard nofile 100000

在这里插入图片描述1.创建一个epoll句柄,参数size用来告诉

内核监听的文件描述符的个数,跟内存大小有关。

#include 
int epoll_create(int size)		size:监听数目(内核参考值)返回值:成功:非负文件描述符;失败:-1,设置相应的errno

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

	#include int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)epfd:	为epoll_creat的句柄op:		表示动作,用3个宏来表示:EPOLL_CTL_ADD (注册新的fd到epfd),EPOLL_CTL_MOD (修改已经注册的fd的监听事件),EPOLL_CTL_DEL (从epfd删除一个fd);event:	告诉内核需要监听的事件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;EPOLLIN :	表示对应的文件描述符可以读(包括对端SOCKET正常关闭)EPOLLOUT:	表示对应的文件描述符可以写EPOLLPRI:	表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)EPOLLERR:	表示对应的文件描述符发生错误EPOLLHUP:	表示对应的文件描述符被挂断;EPOLLET: 	将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里返回值:成功:0;失败:-1,设置相应的errno

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

	#include int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)events:		用来存内核得到事件的集合,可简单看作数组。maxevents:	告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,timeout:	是超时时间-1:	阻塞0:	立即返回,非阻塞>0:	指定毫秒返回值:	成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
server
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024int main(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];struct sockaddr_in cliaddr, servaddr;struct epoll_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, (struct sockaddr *) &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, (struct sockaddr *)&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);return 0;
}
client
/* client.c */
#include 
#include 
#include 
#include 
#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666int main(int argc, char *argv[])
{struct sockaddr_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, (struct sockaddr *)&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);return 0;
}

相关内容

热门资讯

玩家的惊喜!中至麻将讨赏天天都... 您好:中至麻将讨赏这款游戏是可以开挂的,确实是有挂的,很多玩家在中至麻将讨赏这款游戏中打牌都会发现很...
高科技小工具,博乐呼伦贝尔麻将... 您好:博乐呼伦贝尔麻将可以开挂,确实是有挂的,很多玩家在博乐呼伦贝尔麻将中打牌都会发现很多用户的牌特...
实用靠谱!天酷大厅专用控制想要... a实用靠谱!天酷大厅专用控制想要的牌-开挂神器让你风生水起亲,天酷大厅这款游戏可以开挂的,确实是有挂...
高科技小工具,极速官方游戏辅助... 高科技小工具,极速官方游戏辅助插件提升好牌几率(透视选牌)核心提示:1.通过添加客服微信需要了解加薇...
从输钱到包赢!安徽九张牌设置好... 您好:安徽九张牌可以开挂,确实是有挂的,很多玩家在安徽九张牌中打牌都会发现很多用户的牌特别好,总是好...
如何才能开挂!聚闲麻将辅助安装... 聚闲麻将神器是一款可以让一直输的玩家,快速成为一个“必胜”的ai辅助神器,聚闲麻将可以一键让你轻松成...
运气爆破!欢乐十三张有没有办法... 您好:欢乐十三张可以开挂,确实是有挂的,很多玩家在欢乐十三张中打牌都会发现很多用户的牌特别好,总是好...
如何才能开挂!小闲巴渝棋牌开挂... 小闲巴渝棋牌神器是一款可以让一直输的玩家,快速成为一个“必胜”的ai辅助神器,小闲巴渝棋牌可以一键让...
出奇制胜!新圣游斗牛怎么识别别... 您好:新圣游斗牛这款游戏是可以开挂的,确实是有挂的,很多玩家在新圣游斗牛这款游戏中打牌都会发现很多用...
真实有效!闽游福建麻将开挂神器... 真实有效!闽游福建麻将开挂神器要什么牌来什么牌-2025已升级核心提示:1.通过添加客服微信需要了解...