我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何用C写一个web服务器之I/O多路复用

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何用C写一个web服务器之I/O多路复用

前言

I/O模型

接触过 socket 编程的同学应该都知道一些 I/O 模型的概念,linux 中有阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和 异步 I/O 五种模型。

其他模型的具体概念这里不多介绍,只简单地提一下自己理解的 I/O 多路复用:简单的说就是由一个进程来管理多个 socket,即将多个 socket 放入一个表中,在其中有 socket 可操作时,通知进程来处理, I/O 多路复用的实现方式有 select、poll 和 epoll。

select/poll/epoll

在 linux下,通过文件描述符(file descriptor, 下 fd)来进行 socket 的操作,所以下文均是对 fd 操作。

首先说最开始实现的 select 的问题:

  • select 打开的 fd 最大数目有限制,一般为1024,在当前计算系统的并发量前显然有点不适用了。
  • select 在收到有 fd 可操作的通知时,是无法得知具体是哪个 fd 的,需要线性扫描 fd 表,效率较低。
  • 当有 fd 可操作时,fd 会将 fd 表复制到内核来遍历,消耗也较大。

随着网络技术的发展,出现了 poll:poll 相对于 select,使用 pollfd 表(链表实现) 来代替 fd,它没有上限,但受系统内存的限制,它同样使用 fd 遍历的方式,在并发高时效率仍然是一个问题。

最终,epoll 在 Linux 2.6 的内核面世,它使用事件机制,在每一个 fd 上添加事件,当fd 的事件被触发时,会调用回调函数来处理对应的事件,epoll 的优势总之如下:

  • 只关心活跃的 fd,精确定位,改变了poll的时间效率 O(n) 到 O(1);
  • fd 数量限制是系统能打开的最大文件数,会受系统内存和每个 fd 消耗内存的影响,以当前的系统硬件配置,并发数量绝对不是问题。
  • 内核使用内存映射,大量 fd 向内核态的传输不再是问题。

为了一步到位,也是为了学习最先进的I/O多路复用模型,直接使用了 epoll 机制,接下来介绍一下 epoll 相关基础和自己服务器的实现过程。

epoll介绍

epoll 需要引入<sys/epoll.h>文件,首先介绍一下 epoll 系列函数:

epoll_create

int epoll_create(int size);

创建一个 epoll 实例,返回一个指向此 epoll 实例的文件描述符,当 epoll 实例不再使用时,需要使用close()方法来关闭它。

在最初的实现中, size 作为期望打开的最大 fd 数传入,以便系统分配足够大的空间。在最新版本的内核中,系统内核动态分配内存,已不再需要此参数了,但为了避免程序运行在旧内核中会有问题,还是要求此值必须大于0;

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  • epfd 是通过 epoll_create 返回的文件描述符
  • op 则是文件描述符监听事件的操作方式,EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL 分别表示添加、修改和删除一个监听事件。
  • fd 为要监听的文件描述符。
  • 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;        
};

epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 监听 epoll 事件:

  • events 是 epoll 事件数组,epoll 事件的结构上面已经介绍过。
  • maxevents 是一次监听获取到的最大事件数目。
  • timeout 是一次监听中获取不到事件的最长等待时间,设置成 -1 会一直阻塞等待,0 则会立即返回。

epoll行为

在 epoll_ctl 的 event 参数中,事件 events 有如下可选项:

EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLRDHUP(连接关闭)、EPOLLPRI(紧急数据可读),此外 EPOLLERR(错误),EPOLLHUP(连接挂断)事件会被 epoll 默认一直监听。

除了设置事件外,还可以对监听的行为设置:

  • level trigger:此行为被 epoll 默认支持,不必设置。在 epoll_wait 得到一个事件时,如果应用程序不处理此事件,在 level trigger 模式下,epoll_wait 会持续触发此事件,直到事件被程序处理;
  • EPOLLET(edge trigger):在 edge trigger 模式下,事件只会被 epoll_wait 触发一次,如果用户不处理此事件,不会在下次 epoll_wait 再次触发。在处理得当的情况下,此模式无疑是高效的。需要注意的是此模式需求 socket 处理非阻塞模式,下面会实现此模式。
  • EPOLLONESHOT:在单次命中模式下,对同一个文件描述符来说,同类型的事件只会被触发一次,若想重复触发,需要重新给文件描述符注册事件。
  • EPOLLWAKEUP:3.5版本加入,如果设置了单次命中和ET模式,而且进程有休眠唤醒能力,当事件被挂起和处理时,此选项确保系统不进入暂停或休眠状态。 事件被 epoll_wait 调起后,直到下次 epoll_wait 再次调起此事件、文件描述符被关闭,事件被注销或修改,都会被认为是处于处理中状态。
  • EPOLLEXCLUSIVE:4.5版本加入,为一个关联到目标文件描述符的 epoll 句柄设置独占唤醒模式。如果目标文件描述符被关联到多个 epoll 句柄,当有唤醒事件发生时,默认所有 epoll 句柄都会被唤醒。而都设置此标识后,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 写事件。

设置epoll ET(edge trigger)模式

上文说过,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多路复用的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何用C写一个web服务器之I/O多路复用

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

怎么用C写一个web服务器之I/O多路复用

小编给大家分享一下怎么用C写一个web服务器之I/O多路复用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!前言I/O模型接触过 socket 编程的同学应该都知道
2023-06-15

怎么用C写一个web服务器之基础功能

这篇文章给大家分享的是有关怎么用C写一个web服务器之基础功能的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。服务器架构目标架构以 nginx 的思想来考虑本服务器架构,初步考虑如下图:当然 php 进程也可以替换
2023-06-15

怎么用C写一个web服务器之GCC项目编译

这篇文章给大家分享的是有关怎么用C写一个web服务器之GCC项目编译的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。前言本想着接下来大概实现一下 CGI 协议,但是实现过程中被一个问题卡住了:C进程与php进程的交
2023-06-15

如何用C#编写一个Windows服务程序

今天小编给大家分享一下如何用C#编写一个Windows服务程序的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。1.添加引用Wi
2023-07-05

如何使用Node.js构建一个简单Web服务器

Node.js是一个用于后端服务的JavaScript运行环境,它允许开发者使用同一种语言编写服务器端和客户端应用程序。与其他后端技术相比,Node.js具有更高的处理能力和更好的可扩展性。在这篇文章中,我们将介绍如何使用Node.js构建一个简单但强大的Web服务器。第一步 - 安装 Node.js首先,你需要在你的计算机上安装 Node.js。你可以从Node.js官方网站
2023-05-14

如何使用Python实现一个简易版Web服务器

今天小编给大家分享一下如何使用Python实现一个简易版Web服务器的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。一、简介我
2023-07-05

编程热搜

目录