1、多路IO转接服务器
服务器和客户端建立连接示意图
2、select函数
Server服务器利用select()
进行监听需要建立连接的Client,Server事先创建好lfd并交给select()函数进行监听,若有客户端需要建立连接,就反馈给服务器 ,服务器调用accpet()函数返回cfd文件描述符给select()函数。
select多路IO转换,原理:借助内核, select 来监听,客户端连接、数据通信事件。
①fd_set rset:创建一个名为rset的文件描述符集合
②void FD_ZERO(fd_set *set):清空一个文件描述符集合 。FD_ZERO(&rset);
③void FD_SET(int fd, fd_set *set):将待监听的文件描述符,添加到监听集合中。FD_SET(3, &rset);FD_SET(5, &rset);FD_SET(6, &rset); 将3、5、6号文件描述符添加到监听集合中。
④void FD_CLR(int fd, fd_set *set):将一个文件描述符从监听集合中移除。FD_CLR(4, &rest);
⑤int FD_ISSET(int fd, fd_set *set):判断一个文件描述符是否在监听集合中。FD_ISSET(4, &rest);返回值,在返回1,不在返回0
int select
(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select()参数及返回值
:
nfds
:监听的所有文件描述符中,最大文件描述符+1
readfds
:读 文件描述符监听集合传入传出参数
writefds :写 文件描述符监听集合传入传出参数通常NULL
exceptfds:异常 文件描述符监听集合 传入传出参数 通常NULL
timeout:>0,设置监听超时时长;NULL: 阻塞监听;0: 非阻塞监听,轮询
返回值:>0 所有监听集合中,满足对应事件的总数;0 无满足监听条件的文件描述符;-1 errno
select的优缺点
缺点:
①监听上限受文件描述符限制,最大1024。
②检测满足条件的fd. 自己添加业务逻辑提高小。 提高了编码难度
优点:跨平台
,win、linux、macOS、Unix。
#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 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;
}
3、poll函数
poll 是一种 I/O 多路复用机制,允许程序监控多个文件描述符的状态,以便在它们变得可读、可写或有异常时做出反应。
poll 函数通过监视一个文件描述符数组中的所有文件描述符,等待它们的状态发生变化。当指定的事件发生时,poll 函数返回,程序可以对这些事件做出响应。
int poll(struct pollfd * fds, nfd_s nfds, int timeout);
poll()参数及返回值:
① fds: 监听的文件描述符【数组】,传入传出
struct pollfd结构体成员变量
int fd:待监听的文件描述符
short events:待监听的文件描述符对应的监听事件,取值:POLLIN、POLLOUT、POLLERR
short revents传入时,给0。 如果满足对应事件的话,返回非0;非0-----POLLIN、POLLOUT、POLLERR
② nfds: 监听数组的,实际有效监听个数
③ timeout>0: 超时时长。 单位:毫秒 ;-1: 阻塞等待 ;0:不阻塞
④ 返回值:返回满足对应监听事件的文件描述符总个数
poll优缺点
优点:自带数组结构,可以将监听事件集合 和返回事件集合 分离;拓展 监听上限 超出1024限制
缺点:不能跨平台。适用于Linux;无法直接定位满足监听事件的文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
int main() {
struct pollfd fds[2];
int timeout_msecs = 5000; // 5 seconds timeout
// 初始化第一个文件描述符(例如 stdin)
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN; // 监听可读事件
// 初始化第二个文件描述符(例如某个套接字)
fds[1].fd = /* 某个套接字的文件描述符 */;
fds[1].events = POLLIN; // 监听可读事件
// 调用 poll 进行事件监测
int retval = poll(fds, 2, timeout_msecs);
if (retval == -1) {
perror("poll()");
exit(EXIT_FAILURE);
}
if (retval == 0) {
printf("Timeout occurred! No data available.\n");
} else {
// 处理发生的事件
if (fds[0].revents & POLLIN) {
printf("Data available on stdin.\n");
}
if (fds[1].revents & POLLIN) {
printf("Data available on socket.\n");
}
}
return 0;
}
4、epoll函数
int epoll_create
(int size):创建一个监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考)
返回值:指向新创建的红黑树的根节点的fd;失败: -1 errno
int epoll_ctl
(int epfd, int op, int fd, struct epoll_event * event):操作监听红黑树
epoll_ctl()参数及返回值
①epfd : epoll_create 函数的返回值 epfd文件描述符
② op: 对该监听红黑树所做的操作
EPOLL_CTL_ADD添加fd到 监听红黑树上
EPOLL_CTL_MOD修改fd在 监听红黑树上的监听事件
EPOLL_CTL_DEL将一个fd 从监听红黑树上摘下(取消监听)
③fd: 待监听的fd
④ event: 本质是struct epoll_event 结构体地址。
成员
events:EPOLLIN,EPOLLOUT,EPOLLERR
data联合体:int fd(对应监听事件的fd),void *ptr,uint32_t u32,uint64_t u64
⑤ 返回值成功0;失败 -1 errno
int epoll_wait
(int epfd, struct epoll_event * events, int maxevents, int timeout): 阻塞监听
epoll_wait()参数及返回值
①epfd : epoll_create 函数的返回值 epfd文件描述符
②events: 传出参数,看作数组, 满足监听条件的那些 fd结构体
③maxevents:数组 元素的总个数。 直接写1024 struct epoll_event evnets[1024]
④timeout >0: 超时时长。 单位:毫秒
-1: 阻塞等待
0: 不阻塞
⑤返回值 >0: 满足监听的 总个数。 可以用作循环上限
0:没有fd满足监听事件
-1:失败 errno
5、epoll事件模型
ET模式(边沿触发)
缓冲区剩余未读尽的数据不会导致epoll_wait 返回,新的事件才会触发。
struct eopll_event event,event.events = EPOLLIN | EPOLLE。
只有数据到来才触发,不管缓存区是否还有数据。
LT模式(水平触发)
默认采用模式,缓冲区剩余未读尽的数据会导致epoll_wait 返回。
水平触发只要有数据都会触发。
结论:
epoll 的 ET模式, 高效模式。但是只支持 非阻塞模式。
struct epoll_event event,event.events = EPOLLIN | EPOLLET。
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event)。
int flg = fcntl(cfd, F_GETFL),flg | = O_NONBLOCK,fcntl(cfd, F_SETEL, flg)。