多路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 6666
int main(int argc, char *argv[])
{
int listenfd, connfd; // connect fd
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;//定义读集合,备份集合allset
int 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)//修改maxfd
maxfd = 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 1024
int 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无法直接定位满足监听事件的文件描述符, 编码难度较大。