目录前言I/O模型select/poll/epollepoll介绍epoll_createepoll_ctlepoll_waitepoll行为代码实现整体处理逻辑设置epoll ET
接触过 Socket 编程的同学应该都知道一些 I/O 模型的概念,linux 中有阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和 异步 I/O 五种模型。
其他模型的具体概念这里不多介绍,只简单地提一下自己理解的 I/O 多路复用:简单的说就是由一个进程来管理多个 socket,即将多个 socket 放入一个表中,在其中有 socket 可操作时,通知进程来处理, I/O 多路复用的实现方式有 select、poll 和 epoll。
在 linux下,通过文件描述符(file descriptor, 下 fd)来进行 socket 的操作,所以下文均是对 fd 操作。
首先说最开始实现的 select 的问题:
随着网络技术的发展,出现了 poll:poll 相对于 select,使用 pollfd 表(链表实现) 来代替 fd,它没有上限,但受系统内存的限制,它同样使用 fd 遍历的方式,在并发高时效率仍然是一个问题。
最终,epoll 在 Linux 2.6 的内核面世,它使用事件机制,在每一个 fd 上添加事件,当fd 的事件被触发时,会调用回调函数来处理对应的事件,epoll 的优势总之如下:
为了一步到位,也是为了学习最先进的I/O多路复用模型,直接使用了 epoll 机制,接下来介绍一下 epoll 相关基础和自己服务器的实现过程。
epoll 需要引入<sys/epoll.h>文件,首先介绍一下 epoll 系列函数:
int epoll_create(int size);
创建一个 epoll 实例,返回一个指向此 epoll 实例的文件描述符,当 epoll 实例不再使用时,需要使用close()方法来关闭它。
在最初的实现中, size 作为期望打开的最大 fd 数传入,以便系统分配足够大的空间。在最新版本的内核中,系统内核动态分配内存,已不再需要此参数了,但为了避免程序运行在旧内核中会有问题,还是要求此值必须大于0;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
它的结构如下:
typedef uNIOn epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 监听 epoll 事件:
在 epoll_ctl 的 event 参数中,事件 events 有如下可选项:
EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLRDHUP(连接关闭)、EPOLLPRI(紧急数据可读),此外 EPOLLERR(错误),EPOLLHUP(连接挂断)事件会被 epoll 默认一直监听。
除了设置事件外,还可以对监听的行为设置:
当监听事件和行为需求同时设置时,使用运算符 |即可。
使用 epoll 时的服务器受理客户端请求逻辑如下:
1.创建服务器 socket,注册服务器 socket 读事件;
2.客户端连接服务器,触发服务器 socket 可读,服务器创建客户端 socket,注册客户端socket 读事件;
3.客户端发送数据,触发客户端 socket 可读,服务器读取客户端信息,将响应写入 socket;
4.客户端关闭连接,触发客户端 socket 可读,服务器读取客户端信息为空,注销客户端 socket 读事件;
erver_fd = server_start();
epoll_fd = epoll_create(FD_SIZE);
epoll_reGISter(epoll_fd, server_fd, EPOLLIN|EPOLLET);// 这里注册socketEPOLL事件为ET模式
while (1) {
event_num = epoll_wait(epoll_fd, events, MAX_EVENTS, 0);
for (i = 0; i < event_num; i++) {
fd = events[i].data.fd;
// 如果是服务器socket可读,则处理连接请求
if ((fd == server_fd) && (events[i].events == EPOLLIN)){
accept_client(server_fd, epoll_fd);
// 如果是客户端socket可读,则获取请求信息,响应客户端
} else if (events[i].events == EPOLLIN){
deal_client(fd, epoll_fd);
} else if (events[i].events == EPOLLOUT)
// todo 数据过大,缓冲区不足的情况待处理
continue;
}
}
需要注意的是,客户端socket在可读之后也是立刻可写的,我这里直接读取一次请求,然后将响应信息 write 进去,没有考虑读数据时缓冲区满的问题。
这里提出的解决方案为:
1.设置一个客户端 socket 和 buffer 的哈希表;
2.在读入一次信息缓冲区满时 recv 会返回 EAGIN 错误,这时将数据放入 buffer,暂时不响应。
3.后续读事件中读取到数据尾后,再注册 socket 可写事件。
4.在处理可写事件时,读取 buffer 内的全部请求内容,处理完毕后响应给客户端。
5.最后注销 socket 写事件。
上文说过,ET模式是 epoll 的高效模式,事件只会通知一次,但处理良好的情况下会更适用于高并发。它需要 socket 在非阻塞模式下才可用,这里我们实现它。
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
// 获取服务器socket的设置,并添加"不阻塞"选项
flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);
.....
// 这里注册服务器socket EPOLL事件为ET模式
epoll_register(epoll_fd, server_fd, EPOLLIN|EPOLLET);
我将处理事件注掉后使用一次客户端连接请求进行了测试,很清晰地说明了 ET模式下,事件只触发一次的现象,前后对比图如下:
Mac OS X 操作系统的某些部分是基于 FreeBSD 的,FreeBSD 不支持,MAC 也不支持(不过有相似的 kqueue),跑到开发机上开发的,作为一个最基础的 C learner, 靠着printf()和fflush()两个函数来调试的,不过搞了很久总算是完成了,有用 C 的前辈推荐一下调试方式就最好了
以上就是如何用C写一个WEB服务器之I/O多路复用的详细内容,更多关于用C写一个web服务器之I/O多路复用的资料请关注编程网其它相关文章!
--结束END--
本文标题: 如何用C写一个web服务器之I/O多路复用
本文链接: https://www.lsjlt.com/news/126925.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2024-05-13
2024-05-13
2024-05-11
2024-05-11
2024-05-10
2024-05-07
2024-04-30
2024-04-30
2024-04-30
2024-04-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0