一.为什么要做webserver的项目呢?
串联了C++的相关基础知识,语⾔(C/C++全覆盖,可以扩展⾄C++11/17)+操作系统(含有⼤量的I/O 系统调⽤及其封装,还有 EPOLL 等多路复⽤机制)+计算机⽹络(本身就是⼀个⽹络框架,对⽹络异常的处理)+数据库(注册中⼼的数据库语句、负载均衡等)。
二.线程池线程数怎么确定?
Nthreads=Ncpu*(1+w/c)
公式 Nthreads = Ncpu * (1 + w/c)
是用于估计并发线程数的一个经验公式,其中:
Nthreads
表示估计的并发线程数。Ncpu
表示计算机的CPU核心数。w
表示等待时间(等待任务完成的时间,通常以毫秒为单位)。c
表示计算时间(执行任务所需的时间,通常以毫秒为单位)。
这个公式的目的是根据CPU核心数以及任务的等待时间和计算时间来估算可以并行执行的线程数。这在多线程编程和并行计算中很有用,可以帮助确定合适的线程数,以充分利用计算资源而不过度消耗内存和CPU。(chatgpt)
标准的面试八股文答案:
假设机器有N个CPU,那么对于计算密集型的任务,应该设置线程数为N+1;对于IO密集型的任务,应该设置线程数为2N;对于同时有计算工作和IO工作的任务,应该考虑使用两个线程池,一个处理计算任务,一个处理IO任务,分别对两个线程池按照计算密集型和IO密集型来设置线程数。
IO密集型:如果存在IO,那么肯定w/c>1(阻塞耗时一般是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu*(1+1)=2Ncpu+1。
计算密集型:假设没有等待w=0,则W/C=0. Nthreads=Ncpu+1。在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的效率。(即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。)
如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。
服务器性能IO优化 中发现一个估算公式:
|
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 |
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
|
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目 |
可以得出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
一个系统最快的部分是CPU,决定一个系统吞吐量上限的是CPU。增强CPU处理能力,可以提高系统吞吐量上限。但根据短板效应,真实的系统吞吐量并不能单纯根据CPU来计算。那要提高系统吞吐量,就需要从“系统短板”(比如网络延迟、IO)着手:
- 尽量提高短板操作的并行化比率,比如多线程下载技术
- 增强短板能力,比如用NIO替代IO
第一条可以联系到Amdahl定律,这条定律定义串行系统并行化后的加速比计算公式:
|
加速比=优化前系统耗时 / 优化后系统耗时 |
加速比越大,表明系统并行化的优化效果越好。Addahl定律还给出系统并行度、CPU数目和加速比的关系,加速比为Speedup,系统串行化比率(指串行执行代码所占比率)为F,CPU数目为N:
|
Speedup <= 1 / (F + ( 1 -F)/N) |
当N足够大时,串行化比率F越小,加速比Speedup越大。
是否使用线程池就一定比使用单线程高效呢?
答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:
- 多线程带来线程上下文切换开销,单线程就没有这种开销
- 锁
当然“Redis很快”本质的原因在于:Redis基本都是内存操作,这种情况下单线程可以很高效地利用CPU。而多线程适用场景一般是:存在相当比例的IO和网络操作。
所以即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值。
参考:http://ifeve.com/how-to-calculate-threadpool-size/
三.超过100个用户持续访问,服务器会怎么样?
(chatgpt)
当有超过100个用户持续访问服务器时,服务器可能会面临一系列挑战和潜在问题,这取决于服务器的性能、配置、网络带宽以及访问模式等因素。以下是一些可能发生的情况:
-
服务器性能问题:服务器可能无法处理大量并发请求,导致性能下降。这可能表现为延迟增加、响应时间变长以及服务器负载过高。
-
资源耗尽:服务器的CPU、内存和磁盘等资源可能会耗尽,导致服务器宕机或变得不稳定。这可能需要优化服务器配置或升级硬件来处理更多的并发请求。
-
网络带宽受限:服务器的网络带宽可能会成为瓶颈,导致响应时间增加。这可以通过增加带宽或使用内容分发网络(CDN)来缓解。
-
数据库连接问题:如果应用程序与数据库交互,并且数据库连接数有限,那么数据库连接池可能会耗尽,导致应用程序无法处理请求。需要优化数据库连接和查询,或者增加数据库服务器的容量。
-
缓存失效:如果服务器使用了缓存,而缓存策略不当,可能导致缓存失效,需要重新生成缓存数据,增加了服务器负载。
-
安全问题:服务器可能面临更多的安全威胁,例如DDoS攻击或恶意请求。需要实施安全措施来保护服务器免受攻击。
-
错误处理问题:大量并发请求可能导致应用程序的错误处理能力不足,从而导致应用程序崩溃或不稳定。
为了解决这些问题,可以考虑以下措施:
- 优化服务器性能和配置。
- 增加服务器资源,例如CPU、内存和带宽。
- 使用负载均衡来分散流量。
- 实施缓存策略和合理的缓存失效处理。
- 使用数据库连接池。
- 实施安全措施来防御攻击。
- 进行性能测试和监控,及时发现和解决问题。
- 考虑使用云计算服务,以便根据需要动态扩展服务器资源。
最终,服务器的承受能力取决于服务器硬件、配置和应用程序的设计,因此需要根据具体情况来采取适当的措施。
(网上)
1、服务器过载
由过多流量引起的最常见的服务器问题之一是服务器过载。当服务器过载时,这意味着它收到的请求多于它可以处理的数量,这可能导致响应时间变慢甚至服务器崩溃。当流量突然激增时,例如在热门活动或促销期间,可能会发生这种情况。
为了解决服务器过载问题,您可以实施负载平衡,将传入流量分配到多台服务器以确保没有一台服务器过载。您还可以通过减少不必要的后台进程或升级服务器硬件来优化服务器资源。
2、带宽限制
流量过大引起的另一个服务器问题是带宽限制。带宽是指可以通过网络连接传输的数据量。当服务器接收到大量流量时,它会很快用完可用带宽,从而导致页面加载时间变慢甚至停机。
要解决带宽限制问题,您可以通过升级 Internet 连接或实施内容分发网络 (CDN) 来增加服务器的带宽容量。CDN 将您网站的内容缓存在世界各地的多个服务器上,从而减少提供内容所需的带宽量。
3、数据库过载
过多的流量也会导致数据库过载,当数据库服务器收到的请求超过其处理能力时,就会发生这种情况。这可能导致响应时间变慢甚至数据库崩溃,从而导致数据丢失或损坏。
要解决数据库过载问题,您可以通过减少不必要的查询或实施数据库缓存来优化数据库查询。您还可以升级数据库服务器的硬件或考虑实施数据库集群解决方案,将工作负载分布到多个数据库服务器上。
4、安全漏洞
过多的流量还会增加安全漏洞的风险,例如 DDoS 攻击或暴力攻击。当服务器被来自多个来源的请求淹没,服务器不堪重负并导致其崩溃时,就会发生 DDoS 攻击。当黑客试图通过猜测登录凭据来访问您的服务器时,就会发生暴力攻击。
为了解决安全漏洞,您可以实施防火墙或入侵检测系统来监控和阻止恶意流量。您还可以实施双因素身份验证或强密码策略来降低暴力攻击的风险。
过多的流量会导致许多服务器问题,从而影响您的网站或应用程序的性能和可用性。通过实施负载平衡、带宽优化、数据库优化和安全措施等解决方案,您可以确保您的服务器能够处理用户的需求并提供可靠和安全的用户体验。
四.介绍webserver项目中线程池 io多路复用
webserver里面是用epoll,这题应该是回答epoll的相关知识。
epoll相关函数的定义:
-
epoll_create函数和epoll_create1函数
int epoll_create(int size); int epoll_create1(int flags);
epoll_create 是 epoll 的初始化函数,用于创建一个 epoll 对象。其中,size 表示最多可以监听的文件描述符数量。该函数返回一个整数值,表示创建的 epoll 对象的文件描述符。如果出错,返回值为 -1。
epoll_create1 也用于创建一个epoll对象,并返回一个文件描述符,但它可以通过 flags 参数来设置一些标志位,以改变 epoll 对象的行为。常用的标志位包括:
EPOLL_CLOEXEC
:设置文件描述符的 close-on-exec 标志,即在执行 exec 系统调用时自动关闭文件描述符。EPOLL_NONBLOCK
:设置文件描述符的非阻塞标志,即对该文件描述符的操作都是非阻塞的。
总的来说,epoll_create1 函数比 epoll_create 函数更加灵活,可以通过标志位来设置一些 epoll 对象的属性,而 epoll_create 函数则比较简单,只能创建默认属性的 epoll 对象。在实际编程中,建议优先使用 epoll_create1 函数。
-
epoll_ctl函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl 函数负责把服务端和客户端建立的 socket 连接注册到 eventpoll 对象里。其中,第一个参数 epfd 是 epoll 对象的文件描述符。第二个参数 op 表示操作类型,可以是以下三个值之一:
操作类型 描述 EPOLL_CTL_ADD 在epoll的监视列表中添加一个文件描述符(即参数fd),指定监视的事件类型(参数event),常量值为1。 EPOLL_CTL_DEL 将某监视列表中已经存在的描述符(即参数fd)删除,参数event传NULL,常量值为2。 EPOLL_CTL_MOD 修改监视列表中已经存在的描述符(即参数fd)对应的监视事件类型(参数event),常量值为3。 在centos这个发行版中,EPOLL_CTL_ADD、EPOLL_CTL_MOD和EPOLL_CTL_DEL的定义在/usr/include/sys/epoll.h文件下找到。
第三个参数 fd 是要添加、修改或删除的文件描述符。第四个参数 event 是一个 struct epoll_event 结构体,用于描述要监听的事件类型和数据。该结构体的定义如下:
struct epoll_event {uint32_t events; /* 监听的事件类型 */epoll_data_t data; /* 用户数据 */ };typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64; } epoll_data_t;
其中,events 表示要监听的事件类型,可以是以下几个值之一:
EPOLLIN
:表示文件描述符可读;EPOLLOUT
:表示文件描述符可写;EPOLLERR
:表示文件描述符发生错误;EPOLLHUP
:表示文件描述符被挂起。
data 是一个 epoll_data_t 类型的联合体,用于存储用户数据。它可以是一个指针、一个文件描述符、一个 32 位整数或一个 64 位整数。epoll_ctl 函数返回 0 表示成功,-1 表示失败。
-
epoll_wait函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait 用于等待文件描述符上的事件发生。其中,epfd 是 epoll 对象的文件描述符,events 是一个指向 struct epoll_event 数组的指针,用于存储发生事件的文件描述符和事件类型,maxevents 表示最多等待的事件数量,timeout 表示等待的超时时间,单位是毫秒。epoll_wait 函数返回发生事件的文件描述符数量,如果出错,返回值为 -1。
epoll的底层原理
在理解epoll底层原理之前,需要知道
struct eventpoll
(epoll对象)的作用。 epoll 对象是用于管理多个文件描述符的机制,它可以同时监听多个文件描述符上的事件,包括套接字的可读、可写、错误等事件。尽管 epoll 对象和套接字的功能不同,但它们之间也有联系。在实际应用中,通常会将套接字的文件描述符加入到 epoll 对象中,以便能够监听套接字上的事件。这样,当套接字上有数据可读或可写时,epoll 对象就会通知应用程序进行相应的操作。因此,epoll 对象和套接字是可以结合使用的。
epoll对象的定义如下:
struct eventpoll
{wait_queue_head_t wq; // sys_epoll_wait用到的等待队列struct list_head rdllist; // 接收就绪的描述符都会放到这里struct rb_root rbr; // 每个epoll对象中都有一颗红黑树......
}
- wq: 等待队列。如果当前进程没有数据需要处理,会把当前进程描述符和
回调函数
default_wake_functon (这个回调函数等会要考)构造一个等待队列项,放入当前 wq 对待队列中,等到数据就绪的时候会通过 wq 来找到阻塞在 epoll 对象上的用户进程。 - rdllist: 就绪的描述符的队列。当有的连接就绪的时候(socket监听的事件完成的时候),内核会把就绪的socket的文件描述符放到 rdllist 队列里。这样应用进程只需要判断队列就能找出就绪进程,而不用去遍历整棵树。
- rbr: 一棵红黑树。为了支持对海量连接的高效查找、插入和删除,eventpoll 内部使用了一棵红黑树。通过这棵树来管理用户进程下添加进来的所有 socket 连接。
struct eventpoll 的结构如下图所示:
在创建好epoll对象后,接下来就是将socket的文件描述符和epoll对象关联起来并指定需要监听的事件了。这就是epoll_ctl函数的作用,也就是说epoll_ctl函数可以把socket的文件描述符以及监听的事件添加
(也可以是删除或更新)到epoll对象中。那具体是如何关联的呢?实际上,epoll_ctl函数首先会创建一个epitem对象。epitem结构体如下所示:
struct epitem{
struct rb_node rbn; // 红黑树节点struct epoll_filefd ffd; // socket文件描述符信息struct eventpoll *ep; // 所归属的epoll对象struct list_head pwqlist; // 等待队列
}
这个epitem对象其实就是epoll对象里面的红黑树中的一个节点。epitem对象也是对socket的一种抽象概括,也就是说从epitem对象中能够得到socket的部分关键信息。我们也可以把epitem对象理解为socket对象和epoll对象关联的一个桥梁。当socket上监听的事件没有发生时,socket就会变为阻塞状态。总的来说,epoll_ctl函数主要做了下面这三件事情:
-
创建红黑树节点对象epitem
-
将等待事件添加到socket的等待队列中,通过
pwqlist
(等待队列)设置数据就绪的回调函数为ep_poll_callback。(当事件发生后,软中断处理程序就会调用ep_poll_callback) -
将epitem节点插入到epoll对象的红黑树中
接下来就开始静静的等待事件的发生。显然,这就是epoll_wait函数的作用。epoll_wait 函数的动作比较简单,检查 epoll对象的就绪队列
(里面放的是就绪的文件描述符)是否有数据到达,如果没有就把当前的进程描述符添加到一个等待队列项里,加入到 epoll对象的进程等待队列里,设置等待项回调函数default_wake_function,然后阻塞自己,等待数据到达时通过回调函数被唤醒。
是的,当没有 IO 事件的时候, epoll 也是会阻塞掉当前进程。这个是合理的,因为没有事情可做了占着 CPU 也没啥意义。网上的很多文章有个很不好的习惯,讨论阻塞、非阻塞等概念的时候都不说主语。这会导致你看的云里雾里。拿 epoll 来说,epoll 本身是阻塞的,但一般会把 socket 设置成非阻塞。只有说了主语,这些概念才有意义。
当有数据到达时,通过下面几个步骤唤醒对应的进程处理数据:
- (前面的过程参考select系统调用)。
- 中断处理程序根据数据包里面的IP和端口号就能找到对应的socket对象,将内存中的数据包拷贝到socket的接收队列中(事件被触发) ,再调用socket的等待队列中等待项设置的回调函数ep_poll_callback。
- ep_poll_callback 函数根据等待队列项找到epitem。
- 由于epitem保存了已就绪的socket的文件描述符,并且epitem对象是epoll对象的一个红黑树节点,所以ep_poll_callback函数可以将就绪的socket的文件描述符添加到epoll对象的就绪队列中。
- ep_poll_callback 函数检查epoll对象的等待队列上是否有等待项。
- 如果没有等待项,说明用户进程并未阻塞,此时软中断结束。
- 如果有等待项,则通过调用回调函数 default_wake_func 唤醒这个进程。
- 当进程醒来后,继续从epoll_wait时暂停处的代码继续执行,把epoll对象就绪队列的事件返回给用户进程,让用户进程调用recv把已经到达socket的数据拷贝到用户空间使用。
感想:
研究了整整一天终于看懂了epoll这个系统调用的底层原理。真的感觉这个系统调用的底层原理非常复杂。过不了几天,我感觉就能把这些东西忘的差不多了。为了更方便的回顾,画了epoll对象、epitem对象和socket对象的关系图。
epoll的优缺点
优点:
- 高效处理高并发下的大量连接,有非常有益的性能。(红黑树将存储 epoll 所监听的 FD,高效的数据结构,本身插入和删除性能比较好;通过epoll对象中的就绪队列可以直接知道哪些文件描述符已就绪,减少了遍历文件描述符集的时间开销; mmap 的引入,将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址,减少用户态和内核态之间的数据交换。)
缺点:
- 跨平台性不够好,目前只支持Linux操作系统。MacOS和Windows操作系统不支持该函数。
- 在监听的文件描述符或事件较少的时候,可能select和poll的性能更优。
ET vs LT
基本概念
Edge Triggered (ET) 边沿触发
-
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
-
socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
Level Triggered (LT) 水平触发
-
socket接收缓冲区不为空,有数据可读,则读事件一直触发
-
socket发送缓冲区不满可以继续写入数据,则写事件一直触发
epoll_ctl模式设置
epoll_wait 函数的触发方式可以通过 epoll_ctl 函数的 EPOLL_CTL_ADD 操作来设置。在添加文件描述符到 epoll 对象时,可以通过设置 epoll_event 结构体中的 events 字段来指定触发方式。具体来说:
- 如果将 events 字段设置为
EPOLLIN | EPOLLET
或EPOLLOUT | EPOLLET
,则表示该文件描述符采用边缘触发方式。 - 如果将 events 字段设置为
EPOLLIN
或EPOLLOUT
,则表示该文件描述符采用水平触发方式。
其中,EPOLLIN 表示文件描述符可读,EPOLLOUT 表示文件描述符可写,EPOLLET 表示边缘触发方式。
需要注意的是,边缘触发方式下,epoll_wait 函数只会在文件描述符上发生状态变化时才返回,而水平触发方式下,epoll_wait 函数会在文件描述符上有数据可读或可写时就返回。因此,边缘触发方式下需要更加谨慎地处理事件,否则可能会出现遗漏事件的情况。
应用场景
基于IO多路复用(epoll实现)的Web服务器
基于epoll多路复用的方式写一个并发的Web服务器对于理解epoll多路复用很有帮助。
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define PORT 8080int main(int argc, char const *argv[])
{int client_fd, valread;struct sockaddr_in server_addr;char buffer[1024] = {0};const char *http_request = "GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: close\r\n\r\nHello World!";client_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建socketserver_addr.sin_family = AF_INET; // sin_family用来定义是哪种地址族,AF_INET表示使用IPv4进行通信server_addr.sin_port = htons(PORT); // 指定端口号,htons()将短整型数据转换成网络字节顺序inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); // 将IPv4地址从点分十进制转换为二进制格式// 连接服务器if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){perror("connect failed");exit(EXIT_FAILURE);}send(client_fd, http_request, strlen(http_request), 0); // 发送HTTP请求return 0;
}
服务器代码:
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/socket.h>#define MAX_EVENTS 10 // epoll_wait函数每次最多返回的就绪事件数量
#define BUF_SIZE 1024 // 缓冲区大小int main(int argc, char *argv[])
{int nfds, i, n;char buffer[BUF_SIZE];int server_fd, client_fd, epoll_fd;struct epoll_event ev, events[MAX_EVENTS]; // ev是添加到epoll对象中的事件,events[]用于存储epoll_wait函数返回的就绪事件short port = 8080; // 服务器的监听端口char listen_addr_str[] = "127.0.0.1"; // 服务器的IP地址int server_socket, client_socket; // 定义服务端的socket和客户端的socketstruct sockaddr_in server_addr, client_addr; // 定义服务端和客户端的IPv4的套接字地址结构(定长,16字节)size_t listen_addr = inet_addr(listen_addr_str); // 将点分十进制的IPv4地址转换成网络字节序列的长整型// 对IPv4的套接字地址结构做初始化bzero(&server_addr, sizeof(server_addr)); // 将server_addr结构体的前sizeof(serveraddr)个字节清零,与memset()差不多server_addr.sin_family = AF_INET; // sin_family用来定义是哪种地址族,AF_INET表示使用IPv4进行通信server_addr.sin_port = htons(port); // 指定端口号,htons()将短整型数据转换成网络字节顺序server_addr.sin_addr.s_addr = listen_addr; // 指定服务器的IP地址socklen_t client_len = sizeof(client_addr);server_fd = socket(PF_INET, SOCK_STREAM, 0); // 创建套接字,SOCK_STREAM表示使用TCP协议fcntl(server_fd, F_SETFL, fcntl(server_fd, F_GETFL, 0) | O_NONBLOCK); // 设置非阻塞bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 绑定端口// 监听端口,SOMAXCONN默认值为128,表示TCP服务可以同时接受的连接请求的最大数量listen(server_fd, SOMAXCONN);// 创建 epoll 对象if ((epoll_fd = epoll_create1(0)) < 0){perror("epoll_create1 error");exit(1);}ev.events = EPOLLIN; // 添加事件ev.data.fd = server_fd; // 添加服务器socket到epoll对象中if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) < 0){perror("epoll_ctl error");exit(1);}// 开始循环监听while (1){// 等待事件发生,返回发生事件的文件描述符数量nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds < 0){perror("epoll_wait error");exit(1);}// 处理所有就绪事件for (i = 0; i < nfds; i++){if (events[i].data.fd == server_fd) // 如果是服务器socket有新连接请求{while ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len)) > 0){// 设置客户端socket非阻塞fcntl(client_fd, F_SETFL, fcntl(client_fd, F_GETFL, 0) | O_NONBLOCK);// 添加客户端socket到epoll对象中ev.events = EPOLLIN | EPOLLET; // ET模式,缓冲区状态变化时触发事件ev.data.fd = client_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) < 0){perror("epoll_ctl error");exit(1);}}}else{while ((n = read(events[i].data.fd, buffer, BUF_SIZE)) > 0) // 如果是客户端socket有数据到达{printf("Received: %s\n", buffer); // 输出从客户端接收到的数据char *response = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!";write(events[i].data.fd, response, strlen(response)); // 回复客户端close(events[i].data.fd); // 关闭客户端socket}}}}close(server_fd); // 关闭服务器socketexit(0);
}
运行结果如下:
以上代码的大致流程如下:
Redis的IO多路复用模型简单分析
Redis采用IO多路复用技术,其底层实现原理如下:
-
事件驱动:Redis采用事件驱动的方式处理客户端请求。当有客户端请求到达时,Redis会将请求放入一个事件队列中,然后通过IO多路复用技术来监听事件。当有事件到达时,Redis会从事件队列中取出事件,并根据事件类型进行处理。
-
IO多路复用:Redis采用IO多路复用技术来处理客户端请求。通过IO多路复用技术,Redis可以同时监听多个客户端请求,从而实现高并发的读写操作。
-
非阻塞IO:Redis采用非阻塞IO来处理客户端请求。通过非阻塞IO,Redis可以在等待客户端请求的同时,继续处理其他请求,从而提高系统的吞吐量。
-
事件循环:Redis采用事件循环的方式处理客户端请求。事件循环是指Redis在等待客户端请求的同时,不断地进行事件处理。通过事件循环,Redis可以在保证高并发的同时,保持低延迟和高吞吐量。
Redis的IO多路复用模型是基于epoll实现的。在Linux系统中,有多种IO多路复用模型,包括select、poll和epoll等。Redis最初是基于select模型实现的,但由于select模型在大量连接的情况下性能不佳,因此Redis从2.6版本开始采用epoll模型。
综上所述,Redis的IO多路复用模型是Redis能够实现高性能、高并发、低延迟的关键。通过IO多路复用技术的应用,Redis能够同时监听多个客户端请求,从而实现高并发的读写操作。同时采用非阻塞IO和事件循环的方式处理客户端请求,在保证高并发的同时,保持低延迟和高吞吐量。
问:redis的IO多路复用模型是基于epoll实现,由于epoll系统调用只持支Linux操作系统,为什么windows也能使用redis的IO多路复用模型?
Redis是使用epoll系统调用作为IO多路复用模型的底层实现,而Windows操作系统不支持epoll系统调用。因此,在Windows上使用Redis时,Redis不能直接使用epoll作为底层I/O模型。Redis在Windows上会使用类似epoll的技术实现I/O多路复用。
具体来说,Redis在Windows上使用了IOCP(Input/Output Completion Ports)技术来实现I/O多路复用。IOCP是Windows专有的技术,它是一种高效的I/O调度机制,它可以支持一组I/O操作的异步完成通知。通过IOCP,Redis可以在Windows平台上实现高效的异步I/O,并且可以避免使用epoll等Linux专有的API。
需要注意的是,由于Windows和Linux的底层实现机制不同,导致在Windows上使用IOCP和在Linux上使用epoll并不完全相同,因此,在跨平台开发时需要注意这种差异。
参考链接:https://blog.csdn.net/qq_54015483/article/details/130943574 https://www.xjx100.cn/news/467302.html
标签:线程,socket,epoll,描述符,笔记,学习,Webserver,fd,服务器 From: https://www.cnblogs.com/liuruitao1999/p/17747466.html