TinywebServer知识点(1)
TinywebServer知识点(1)
原项目地址(点击跳转)
博主添加注释后项目地址(点击跳转)
一、套接字相关
1.为什么服务端套接字ip绑定为0.0.0.0
- 监听所有网络接口:绑定到
0.0.0.0
意味着服务器将监听所有可用的网络接口。不论客户端通过哪个IP地址与服务器通信,服务器都能接收到请求。这对于具有多个网络接口或希望接受来自不同子网请求的服务器非常有用 - 简化配置:通过绑定
0.0.0.0
,开发者不需要明确指定某一个IP地址,这样可以避免在配置时出错,也方便了服务器在不同环境中的部署。例如,从开发环境到生产环境,可能会有不同的IP地址,通过绑定0.0.0.0
可以减少配置的修改 - 提高灵活性:绑定
0.0.0.0
使得服务器具有更高的灵活性,可以同时处理来自不同网络的请求,例如本地网络、公司内部网络或互联网
上面提到的网络接口,指代的是网卡设备,每个网络接口代表一个可以接收和发送网络流量的硬件设备或虚拟设备:
- 物理网卡设备:这些是服务器上实际存在的硬件设备,如以太网卡、无线网卡等。每个物理网卡通常都有一个唯一的MAC地址和一个分配的IP地址
- 虚拟网络接口:在一些服务器上,除了物理网卡之外,还可以存在虚拟网络接口。常见的虚拟接口包括:
- Loopback接口(127.0.0.1):用于本地回环测试。
- 虚拟网卡:如VPN连接创建的虚拟接口或容器化环境(如Docker)中创建的网络接口
当服务器套接字绑定到0.0.0.0
时,它会监听来自所有这些网络接口的连接请求。无论客户端通过哪个接口(例如,通过一个特定的以太网卡IP地址或通过无线网卡IP地址)发送请求,服务器都能接收到并处理这些请求
(1)实例
假设服务器有两个物理网卡:
- eth0:分配了IP地址
192.168.1.10
- eth1:分配了IP地址
10.0.0.10
同时还有一个本地回环接口:
- lo:分配了IP地址
127.0.0.1
如果服务器套接字绑定到0.0.0.0
,它将监听所有这些接口。无论客户端是连接到192.168.1.10:8080
,10.0.0.10:8080
,还是127.0.0.1:8080
,服务器都能接收并处理这些连接。
2.webServer项目中,客户端的套接字绑定的端口号,需要与服务端的一致吗
在Web服务器项目中,客户端套接字绑定的端口号通常不需要与服务端的一致。实际上,客户端通常不需要显式绑定端口号,而是让操作系统自动分配一个临时的、随机的端口号。下面是更详细的解释:
服务端端口绑定:
- 服务端需要绑定到一个固定的IP地址和端口号,这样客户端才能知道如何连接到服务端。
- 例如,HTTP服务器可能会绑定到IP地址
0.0.0.0
和端口号80
,这样客户端可以通过http://<server-ip>:80
访问服务器
客户端端口绑定:
- 客户端不需要绑定到一个特定的端口号。客户端发起连接请求时,操作系统会自动分配一个临时端口号用于此次连接。
- 客户端只需知道服务端的IP地址和端口号,发起连接请求时,操作系统会选择一个空闲的临时端口号。
(1)错误理解
在之前我个人的理解搞错了,就是客户端向服务端发送请求时,需要知道服务端的IP地址和端口号,以便通过connect
函数向服务端发起连接请求,以至于我以为,那是客户端套接字绑定的ip以及端口号,那个实际是服务端的ip以及端口号,如下代码:
server.cpp
1 |
|
client.cpp
1 |
|
在上述client.cpp
中,如下一段,即是对服务端的ip以及端口进行绑定的设置
1 | // 对需要连接的服务端ip以及端口进行设置 |
3.什么是IO复用
IO复用是一种允许程序同时监控多个文件描述符的机制,从而使得程序可以在某个或多个文件描述符准备好进行IO操作(读或写)时得到通知并进行处理。IO复用的主要目的在于提高程序的并发性能(可以提高服务器的吞吐能力),特别是在处理大量网络连接时,不必为每个连接创建一个线程或进程
(1)IO复用实现方法
select:
select
是最早的IO复用机制,能够同时监控多个文件描述符的读、写和异常事件- 缺点是每次调用
select
都需要重新设置文件描述符集合,且文件描述符数量有限(通常为1024)
poll:
poll
与select
类似,但使用了不同的数据结构,可以支持任意数量的文件描述符- 缺点是每次调用
poll
都需要遍历文件描述符集合,效率在高并发情况下不如epoll
epoll(Linux特有):
epoll
是Linux特有的高效IO复用机制,适用于处理大量并发连接epoll
使用事件通知机制,避免了select
和poll
的遍历开销,支持“水平触发”和“边缘触发”模式epoll
适用于高并发、高性能网络服务器
(2)IO复用工作原理
select:
- 初始化文件描述符集合
- 调用
select
函数,阻塞等待某个文件描述符变为可读、可写或有异常 - 当
select
返回时,检查文件描述符集合,处理已准备好的文件描述符
poll:
- 初始化
pollfd
结构体数组,指定要监控的文件描述符及其事件。 - 调用
poll
函数,阻塞等待某个文件描述符变为可读、可写或有异常。 - 当
poll
返回时,遍历pollfd
数组,处理已准备好的文件描述符
epoll:
- 创建一个
epoll
实例,使用epoll_create
或epoll_create1
。 - 将文件描述符添加到
epoll
实例中,使用epoll_ctl
函数。 - 调用
epoll_wait
函数,阻塞等待文件描述符集合中的某个或多个文件描述符变为可读、可写或有异常。 - 当
epoll_wait
返回时,处理已准备好的文件描述符
4.UNIX五种基本的IO模型
(1)阻塞IO
在阻塞I/O模型中,当一个进程执行I/O操作时,它会被阻塞,直到I/O操作完成。通常,阻塞I/O会导致进程挂起,直到数据准备好
特点
- 简单直观
- 每个I/O操作都会阻塞调用进程,直到操作完成
- 适用于简单的程序和低并发场景
示例代码
1 | int sockfd = socket(AF_INET, SOCK_STREAM, 0); |
(2)非阻塞IO
在非阻塞I/O模型中,进程执行I/O操作时不会被阻塞。如果操作无法立即完成,系统会立即返回一个错误。进程可以反复尝试操作直到成功(其实是一种轮询(polling)操作)
特点
- 进程不会被阻塞,可以继续执行其他任务
- 需要不断轮询以检查I/O状态,可能导致CPU空转
示例代码
1 | int sockfd = socket(AF_INET, SOCK_STREAM, 0); |
(3)IO复用
I/O复用模型使用select
、poll
或epoll
系统调用,使得一个进程可以同时监控多个文件描述符。当任意一个文件描述符准备好时,进程可以执行I/O操作
select
、poll
等,使系统阻塞在select或poll调用上,而不是真正的IO系统调用(如recvfrom
),等待select
返回可读才调用IO系统,其优势就在于可以等待多个描述符就位
(4)信号驱动式IO
信号驱动I/O模型通过信号来通知应用程序文件描述符准备就绪。当I/O操作可以执行时,内核会发送一个信号给应用程序,应用程序可以在信号处理函数中执行I/O操作
(5)异步IO
在异步I/O模型中,I/O操作的执行是非阻塞的。当应用程序发起一个I/O请求时,内核会立即返回,并在后台继续处理该请求。完成I/O操作后,内核会通知应用程序,通常通过回调函数、信号或事件机制。这样,应用程序可以在等待I/O完成的同时继续执行其他任务。
5.事件处理模式
(1)Reactor
要求主线程(I/O
处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元),将 socket
可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成
使用同步 I/O
(以epoll_wait
为例)实现的 Reactor
模式的工作流程是:
- 主线程往
epoll
内核事件表中注册socket
上的读就绪事件 - 主线程调用
epoll_wait
等待socket
上有数据可读 - 当
socket
上有数据可读时,epoll_wait
通知主线程。主线程则将socket
可读事件放入请求队列 - 睡眠在请求队列上的某个工作线程被唤醒,它从
socket
读取数据,并处理客户请求,然后往epoll
内核事件表中注册该socket
上的写就绪事件 - 当主线程调用
epoll_wait
等待socket
可写 - 当
socket
可写时,epoll_wait
通知主线程。主线程将socket
可写事件放入请求队列 - 睡眠在请求队列上的某个工作线程被唤醒,它往
socket
上写入服务器处理客户请求的结果
Reactor模式工作流程:
主线程,只负责监听epoll
实例中的文件描述符是否有事件发生,工作子线程需要负责I/O
数据读写,以及业务逻辑
(2)Proactor
Proactor
模式将所有I/O
操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻辑。使用异步I/O
模型(以 aio_read
和 aio_write
为例)实现的Proactor
模式的工作流程是:
- 主线程调用
aio_read
函数向内核注册socket
上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例) - 主线程继续处理其他逻辑
- 当
socket
上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用 - 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求后,调用
aio_write
函数向内核注册socket
上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序 - 主线程继续处理其他逻辑
- 当用户缓冲区的数据被写入
socket
之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕 - 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭
socket
Proactor模式工作流程:
主线程负责循环监听epoll
实例上的文件描述符事件,并且发起异步I/O
操作(如文件读写、网络数据发送接收等)和指定完成处理器(Completion Handlers
),设置完成处理器是关键步骤,因为这些处理器定义了当I/O
操作完成时将执行的回调函数或操作;内核负责I/O
数据的读写,当I/O
操作完成时,内核将通知应用程序。这种通知通常是通过预设的回调函数或事件机制实现的;工作线程负责业务的处理逻辑(数据处理、数据验证、数据转换等业务逻辑)
(3)使用同步IO模拟Proactor
使用同步I/O
方式模拟出 Proactor
模式,原理是:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一”完成事件“。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理
使用同步I/O
模型(以 epoll_wait
为例)模拟出的Proactor
模式的工作流程如下:
- 主线程往
epoll
内核事件表中注册socket
上的读就绪事件 - 主线程调用
epoll_wait
等待socket
上有数据可读 - 当
socket
上有数据可读时,epoll_wait
通知主线程。主线程从socket
循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列 - 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往
epoll
内核事件表中注册socket
上的写就绪事件 - 主线程调用
epoll_wait
等待socket
可写 - 当
socket
可写时,epoll_wait
通知主线程。主线程往socket
上写入服务器处理客户请求的结果
同步 I/O 模拟 Proactor 模式的工作流程:
主线程负责epoll
实例中的文件描述符监听,以及IO
的读写操作;而工作线程仅仅负责业务处理逻辑
6.同步IO与异步IO
(1)同步IO
同步I/O指的是应用程序发起一个I/O操作后,必须等待该操作完成才能继续执行其他任务。根据阻塞与否,分为阻塞同步I/O和非阻塞同步I/O
- 阻塞同步I/O
- 阻塞同步I/O是最传统的I/O模型,调用线程会被阻塞,直到I/O操作完成。
- I/O操作会阻塞调用线程,导致资源浪费和性能降低
- 非阻塞同步I/O
- 非阻塞同步I/O调用不会阻塞进程,但需要应用程序主动轮询检查I/O操作是否完成
- 需要轮询I/O状态,增加了代码复杂度和CPU开销
(2)异步IO
异步I/O指的是应用程序发起I/O操作后,不需要等待操作完成,可以立即继续执行其他任务。当I/O操作完成时(I/O操作的完成是由内核去做的,内核将数据准备好,并且将数据从内核空间拷贝到用户空间,进而通过相关机制通知应用程序,数据准备好了),应用程序会得到通知,并进行相应的处理。异步I/O通常由操作系统或I/O库来实现
当遇到IO操作时,代码(应用程序)只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果
实现较复杂,依赖操作系统或I/O库的支持
异步IO,当IO操作完成了,应用程序如何得到通知呢:
- 回调函数
- 回调函数是异步I/O中最常见的通知机制。当I/O操作完成时,操作系统或I/O库会调用预先注册的回调函数
- 信号
- 在某些系统中,可以使用信号来通知异步I/O操作的完成。信号是一种异步通知机制,当I/O操作完成时,内核向进程发送一个信号,进程可以在信号处理程序中处理该I/O操作
- 事件驱动
- 事件驱动模型通常用于图形用户界面(GUI)应用程序和某些高性能服务器中。当I/O操作完成时,系统会将事件加入事件队列,应用程序的事件循环(event loop)会处理这些事件
7.TinywebServer为何使用epoll
- 对于select和poll来说,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态;epoll则将整个文件描述符集合维护在内核态(在内核种维护一个事件表),每次添加/修改/删除文件描述符的时候都需要执行一个系统调用
epoll_ctl()
,内核会更新这个事件表。系统调用的开销是很大的,而且在有很多短期活跃连接的情况下,epoll可能会慢于select和poll由于这些大量的系统调用开销 - select使用线性表描述文件描述符集合,文件描述符有上限;poll使用链表来描述;epoll底层通过红黑树来描述,并且维护一个ready list,将事件表中已经就绪的事件添加到这里,在使用epoll_wait调用时,仅观察这个list中有没有数据即可
- select和poll的最大开销来自内核判断是否有文件描述符就绪这一过程:每次执行select或poll调用时,它们会采用遍历的方式,遍历整个文件描述符集合去判断各个文件描述符是否有活动;epoll则不需要去以这种方式检查,当有活动产生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理
epoll
的设计减少了需要复制的数据量,因为只有发生事件的文件描述符信息会被复制回用户空间,与select
或poll
相比,这大大减少了不必要的数据传输和处理。此外,因为事件表是在内核中维护的,所以不需要每次调用时都传递大量数据结构,也就减少了系统调用的开销- 当监控的文件描述符发生了注册的事件(如可读、可写等),内核将这些事件复制到一个用户空间的缓冲区中。这个过程是通过
epoll_wait()
调用完成的。在epoll_wait()
调用中,内核检查事件表,查找发生了事件的文件描述符,然后将这些事件的信息复制到用户空间提供的内存中 - select和poll都只能工作在相对低效的LT模式下,而epoll同时支持LT和ET模式。
- 综上,当监测的fd数量较小,且各个fd都很活跃的情况下,建议使用select和poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使用epoll会明显提升性能
8.epoll的工作模式
这一部分知识点,看**爱编程的大丙
**
(1)水平工作模式LT(电平触发)
epoll缺省的工作模式,当IO事件就绪时,内核会一直通知,直到该IO事件被处理
并且同时支持block
和no-block socket
。在这种做法中,内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行IO
操作了。如果我们不作任何操作,内核还是会继续通知使用者(文件描述符已经就绪,如果不作操作,就会一直通知,知道对文件描述符进行IO操作)
水平模式触发的特点:
- 重复通知:只要文件描述符仍然处于可读写状态,
epoll
会不断地通知应用程序,无论是否已经对该描述符执行过读写操作。这意味着,如果数据可读,应用程序如果没有读取,epoll_wait
将会再次返回该文件描述符 - 简化的事件处理:由于
epoll
在描述符准备好时会不断通知,应用程序的逻辑可以比较简单,无需担心错过事件。这也意味着应用程序在每次epoll_wait
调用后都必须适当处理事件,否则会陷入重复通知的循环 - 兼容性:水平触发模式的行为类似于传统的
select
和poll
系统调用,使得从这些系统调用迁移到epoll
的过程中,逻辑转换较为直接
使用场景:
- 不确定是否能一次性处理完所有数据:例如,当你希望接收缓冲区非空时就读取一些数据,但不想(或不需要)一次性将其清空
- 多个线程处理同一文件描述符:在这种情况下,水平触发可以减少因竞态条件造成的复杂性,因为多个线程可能都会被告知文件描述符准备好了
(2)边沿工作模式ET(边沿触发)
边沿模式可以简称为ET
模式,ET(edge-triggered)
是高速工作方式,只支持no-block socket
。在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll
通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)
。如果我们对这个文件描述符做IO
操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。**ET
模式在很大程度上减少了epoll
事件被重复触发的次数,因此效率要比LT
模式高**
当IO事件就绪时,内核只会通知一次,如果在这次没有及时处理,该IO事件就丢失了
边沿触发模式特点:
- 单次通知:当文件描述符从非就绪状态变为就绪状态时,
epoll
会通知应用程序一次。一旦通知发生,即使文件描述符仍然处于就绪状态,epoll
不会再次通知应用程序,除非状态再次发生变化 - 高效率:边沿触发模式可以减少系统调用的次数,因为它只在状态变化时发出通知。这减少了应用程序不必要的检查和轮询,特别是在高负载情况下
- 复杂的事件处理:使用边沿触发模式要求应用程序必须能够处理所有的数据,直到资源耗尽(例如读取直到遇到
EAGAIN
错误)。这需要应用程序具有更复杂的逻辑来确保数据的完全处理
使用场景:
- 高并发服务器:在高负载环境下,边沿触发可以显著减少不必要的事件处理,提高服务器效率。
- 应用程序能够一次处理所有数据:应用程序需要设计为能够处理尽可能多的数据,直到没有更多数据可读(或可写),以避免遗漏数据。
注意:
在使用ET模式时,必须要保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);并且每次调用read
和write
的时候都必须等到它们返回EWOULDBLOCK
(确保所有数据都已读完或写完)。
9.epoll事件类型
events描述事件类型,其中epoll事件类型有以下几种
EPOLLIN
:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)EPOLLOUT
:表示对应的文件描述符可以写EPOLLPRI
:表示对应的文件描述符有紧急的数据可读EPOLLERR
:表示对应的文件描述符发生错误EPOLLHUP
:表示对应的文件描述符被挂断;EPOLLET
:将EPOLL
设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的EPOLLONESHOT
:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里EPOLLET
: 边缘触发模式EPOLLRDHUP
:表示读关闭,对端关闭,不是所有的内核版本都支持