多路I/O转接服务器
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种:select、poll、epoll
一、select多路IO转接
让内核去监听客户端连接(lfd),当有客户端进行连接时 它会让server去调用accetp(当有连接时才去立即调用,而不是一直阻塞等待)得到一个用于通信的cfd,最后让内核监管着lfd和所有cfd
即(原理):借助内核, select 来监听, 客户端连接、数据通信事件。
函数解析
1. 底层原理:
文件描述符表:前三个默认被系统占用
fd_set集合:传入的是文件描述符,传出所有监听集合(读、写、异常)中满足对应事件的总数
fd_set集合的本质:位图(二进制位存放文件描述符的状态),默认都为0,若发生变化就置1
2. 语法:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeva l *timeout);
参数:
- nfds:监听的所有文件描述符中,最大文件描述符+1;
- readfds:读 文件描述符监听集合。传入传出参数
- writefds:写 文件描述符监听集合。传入传出参数,通常传NULL
- exceptfds:异常 文件描述符监听集合。传入传出参数,通常传NULL
- timeout:大于0表示设置监听时长,NULL表示阻塞监听,0表示非阻塞监听 while轮询
返回值:
- 大于0:所有监听集合(读、写、异常)中满足对应事件的总数
- 0:没有满足监听条件的文件描述符
- -1:error
3. 监听集合对应函数:
1.void FD_ZERO(fd_set *set); ---清空一个文件描述符集合
fd_set rset; FD_ZERO(&rset); //将rset集合清空
2.void FD_SET(int fd, fd_set *set); ---将待监听的文件描述符添加到监听集合中
FD_SET(3,&rset);FD_SET(5,&rset); //将文件描述符3和5加到rset集合中
3.void FD_CLR(int fd, fd_set *set); ---将一个文件描述符从监听集合中移除
4.int FD_ISSET(int fd, fd_set *set); ---判断一个文件描述符是否在该集合中
4. 思路分析:
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>#include "wrap.h"#define SERV_PORT 6666int main(int argc, char *argv[])
{int listenfd, connfd; // connect fdchar 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;//定义读集合,备份集合allsetint ret,maxfd = 0,n;maxfd = listenfd;//最大文件描述符FD_ZERO(&allset);//清空监听集合FD_SET(listenfd,&allset);//将监听fd添加到监听集合中while(1){rset = allset;//备份select(maxfd+1,&rset,NULL,NULL,NULL);//使用select监听if (ret < 0){perr_exit("select error");}if (FD_ISSET(listenfd,&rset))//listenfd满足监听的读事件{clie_addr_len = sizeof(clie_addr);connfd = Accept(listenfd,(struct sockaddr *)&clie_addr,&clie_addr_len);//建立连接---不会阻塞FD_SET(connfd,&allset);//将新产生的fd添加到监听集合中监听数据读事件if (maxfd < connfd)//修改maxfdmaxfd = connfd;if (ret = 1)//说明select只返回一个,并且是listenfd,后续执行无需执行continue; }for (int i = listenfd+1;i <= maxfd;i++)//处理 满足读事件的fd{if (FD_ISSET(i,&rset))//找到满足读事件的fd{n = read(i,buf,sizeof(buf));if (n == 0)//检测到客户端已经关闭连接{Close(i);FD_CLR(i,&allset);//将关闭的fd移除出监听集合}else if (n == -1){perr_exit("read error");}for (int j = 0;j<n;j++){buf[j] = toupper(buf[j]);}write(i,buf,n);write(STDOUT_FILENO,buf,n);}}}Close(listenfd);return 0;
}
select优缺点:
优点:跨平台。win、linux、macOS、Unix、类Unix、mips
缺点:监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
*二、poll多路IO转接(半成品):在实际开发过程中用处不大,了解即可,重点是epoll
函数解析:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:监听的文件描述符【数组】
struct pollfd
{
int fd:待监听的文件描述符
short events:待监听的文件描述符对应的监听事件取值:POLLIN、POLLOUT、POLLERR
short revnets:传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
}nfds: 监听数组的,实际有效监听个数。
timeout: > 0: 超时时长。单位:毫秒。
-1: 阻塞等待
0: 不阻塞
返回值:
返回满足对应监听事件的文件描述符 总个数。
思路分析:
代码实现:
/* 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"#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;
}
read 函数返回值:
> 0:实际读到的字节数=0: socket中,表示对端关闭。close()
-1: 如果 errno == EINTR 被异常终端。 需要重启。
如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。 需要,再次读。
如果 errno == ECONNRESET 说明连接被 重置。 需要 close(),移除监听队列。
错误。
poll优缺点:
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux无法直接定位满足监听事件的文件描述符, 编码难度较大。