基础知识

  1. TCP

  2. http协议

  3. Linux网络编程

WebServer简介和功能梳理

WebServer,或者网络服务器,是一种专门用来处理互联网或内部网络中请求的服务器。

简单来说,当你在浏览器中输入一个网址时,背后就是WebServer在工作,它负责接收你的请求,处理这个请求,然后把网页的内容发送回你的电脑或手机上。

以Nginx为例,这是一种非常流行的WebServer软件。它之所以广泛使用,主要是因为它处理高并发请求非常高效,即能同时处理成千上万个网络请求。

Nginx常用于提供静态资源如图片、CSS样式文件和JavaScript文件,也可以作为代理服务器,转发对动态资源的请求到其他服务器(这个过程类似于网关)。

WebServer的功能非常强大,它不仅能处理网页内容的分发,还可以进行安全控制、请求转发、负载均衡等。

在这些处理过程中,涉及到的机制包括:

HTTP协议处理:这是Web服务器处理的核心,用于理解和响应浏览器或其他客户端发来的HTTP请求。

内容缓存:为了提高响应速度,Web服务器可以缓存频繁请求的内容,这样在下一次同样的请求到来时可以直接响应,不需要重新生成。

负载均衡:当服务器的请求量非常大时,Web服务器可以将请求分发到多台服务器上,这样可以避免单一服务器过载,提高整体处理能力。

安全功能:例如SSL加密,保证数据传输的安全性。

总的来说,WebServer是网络应用中不可或缺的一部分,它使得网络信息的获取变得快速和高效。

实现一个简单的WebServer

WebServer最核心的功能就是HTTP协议处理,只要能进行HTTP协议处理,就是一个WebServer。

最终目标是实现一个能高效处理HTTP协议的WebServer,其应可以同时处理大量的并发请求,并尽量减少对系统资源的消耗。

先从一个最简单的WebServer开始,后续再逐步扩展,逐渐实现一个高效的、可配置的WebServer。

简单来说,要先实现一个单线程的、只支持处理一个连接的WebServer,这个WebServer 能处理基于HTTP/1.0的简单请求。

具体来说:

这个WebServer应该至少能管理一个网站,一个网站可能有多个网页组成,所以管理的对象应该是一个文件夹。

这个WebServer应该至少监听一个固定的端口,用来接受客户端连接。

这个WebServer应该至少支持HTTP/1.0的大部分请求方法。(HTTP/1.0 支持的请求方法包括GET、POST、PUT、DELETE、TRACE。

这个WebServer应该能够返回标准的HTTP响应,告知客户端处理的结果。

扩充

多线程

其实多线程/多进程无非就是pthread_creater/fork而已,但是需要考虑的东西也就更多了,例如临界区/竞态条件race condition, 考虑到这个, 又要用一些进程间的通信原语去解决问题,例如mutex cond/pipe sig shm, 然后使用这些又需要注意很多地方 死锁/临界区,一些内存已经不存在而在其他线程中要被使用

所有线程共享其所属进程的资源,如内存空间、文件描述符和系统资源,这些资源都是由进程统一管理和调度的

每个线程也有一些专属的资源

定时器

参考:https://www.zhihu.com/column/c_1774157245946933248

简单的方案已经用简单的计时器解决了超时控制的问题,但是终究各管各的,而且未来我们的 WebServer 还要扩展更多的功能,可能也要用到计时功能。

一个一个去实现单独的计时器的做法也许可行,但是看起来毕竟很简陋。

我们要打造一个独立于工作线程、稳定、专业计时的工具,主线程还有其他工作要办,于是我们可以建立一个专门的计时线程。

建立计时线程,首先要分析其应当实现的功能。不是所有的线程都需要计时信息,所以计时器不能给所有线程都发通知,这很浪费不可取。因此计时线程要记录,记录要给哪些线程发通知。

需要计时器的通知信息的线程可能会增多,可能会减少,所以要允许它们 既可以订阅,也可以取消订阅。

计时线程并不知道到底会有多少个线程将来要订阅它的通知,所以它还要具备良好的可扩展性,能支持大量的线程订阅其信息。计时线程作为发布者通知到订阅者,一个计时周期已经到了。

因此,双向链表就以其高效的插入、删除操作和可扩展性成为我们建立计时线程理想的数据结构。

小根堆

小根堆(Min-Heap)是一种特殊的完全二叉树,其每个节点的值都不大于其子节点的值。这种属性确保了树的根节点始终是所有元素中的最小值,这就是小根堆的最大优势:能够快速(在常数时间,即O(1)的时间复杂度内)访问到最小元素。

小根堆的主要操作:

插入操作(Insert) - 将新元素添加到堆的末端,然后通过一系列的向上调整(自下而上的调整,也称为上浮操作)保持小根堆的性质。这个操作的时间复杂度是O(log n),其中n是堆中元素的数量。

删除最小元素(Extract Min) - 删除根节点(即最小元素),通常通过将最后一个元素移动到根位置,然后进行自上而下的调整(下沉操作)来恢复堆的性质。这个操作的时间复杂度也是O(log n)。

构建小根堆(Build Min Heap) - 将一个无序数组转化为小根堆的过程。可以通过从最后一个非叶子节点开始,逐个执行下沉操作来实现。这个过程的时间复杂度是O(n)。

小根堆的应用:优先队列 - 小根堆常用于实现优先队列,其中队列的最高优先级对应于最小元素。这使得任务调度、事件管理等应用中,可以快速地处理最紧急的任务或事件。

堆排序 - 利用堆结构进行的一种有效的排序方法,特别是在需要不断调整数据集中最小或最大值时非常高效。

小根堆的真正优势在于它能够提供快速的访问到最小元素的能力,这对于很多需要频繁查询最小值的场景非常关键。同时,它还支持相对高效的插入和删除最小值的操作,使得大量的且动态变化的数据集的管理变得更加高效。

使用小根堆作为连接池的排序结构

线程池

线程池是一种通过预创建线程的方式,来管理和优化多线程程序性能的技术。

在多线程编程中,频繁地创建和销毁线程会带来较大的性能开销,因为每个线程的创建和销毁都涉及到操作系统的资源分配和回收。

线程池通过创建一定数量的线程,并将它们保持在待命状态,可以快速响应新的任务请求。

当一个任务到来时,线程池会从中选择一个空闲的线程去执行任务,执行完毕后,这个线程不会被销毁,而是返回到线程池中,等待执行下一个任务。

这种方式减少了线程创建和销毁的次数,提高了响应速度,降低了资源消耗。

线程池通常包含以下几个主要组件:

工作队列:用于存放待处理的任务。

工作线程:线程池中的线程,负责执行工作队列中的任务。

线程池管理器:负责管理线程池的创建、任务分配、线程同步和销毁等。

muduo

参考资料:https://blog.csdn.net/T_Solotov/article/details/124044175

Muduo库是基于Reactor模式实现的TCP网络编程库

Muduo库有三个核心组件支撑一个reactor实现持续监听一组fd,并根据每个fd上发生的事件调用相应的处理函数。这三个组件分别是Channel类、Poller/EpollPoller类以及EventLoop类。

Channel类

在TCP网络编程中,想要IO多路复用监听某个文件描述符,就要把这个fd和该fd感兴趣的事件通过epoll_ctl注册到IO多路复用模块上

当事件监听器监听到该fd发生了某个事件。事件监听器返回 发生事件的fd集合以及每个fd都发生了什么事件

Channel类则封装了一个 fd 和这个 fd感兴趣事件 以及事件监听器监听到 该fd实际发生的事件

同时Channel类还提供了设置该fd的感兴趣事件,以及将该fd及其感兴趣事件注册到事件监听器或从事件监听器上移除,以及保存了该fd的每种事件对应的处理函数。

Poller/EpollPoller类

负责监听文件描述符事件是否触发以及返回发生事件的文件描述符以及具体事件的模块就是Poller。所以一个Poller对象对应一个事件监听器。在multi-reactor模型中,有多少reactor就有多少Poller。

muduo提供了epoll和poll两种IO多路复用方法来实现事件监听。默认使用epoll来实现。

这个Poller是个抽象虚类,由EpollPoller和PollPoller继承实现,与监听文件描述符和返回监听结果的具体方法也基本上是在这两个派生类中实现。

EpollPoller就是封装了用epoll方法实现的与事件监听有关的各种方法,PollPoller就是封装了poll方法实现的与事件监听有关的各种方法。

epoll是Linux特有的 IO 多路复用机制,相比 select 和 poll,它在处理大数量文件描述符时更为高效。 epoll 使用一个文件描述符来管理事件,通过三个系统调用来操作:epoll_create、epoll_ctl 和 epoll_wait。

EventLoop类

作为一个网络服务器,需要有持续监听、持续获取监听结果、持续处理监听结果对应的事件的能力,也就是我们需要循环的去 调用Poller:poll方法获取实际发生事件的Channel集合,然后调用这些Channel里面保管的不同类型事件的处理函数(调用Channel::HandlerEvent方法)。

EventLoop就是负责实现循环,负责驱动循环的重要模块,EventLoop整合封装了二者并向上提供了更方便的接口来使用。

每个EventLoop对象都唯一绑定了一个线程,这个线程其实就在一直执行loop函数里面的while循环。

这个while循环的逻辑就是调用poll方法获取事件监听器上的监听结果

接下来在loop里面就会**调用监听结果中每一个Channel的处理函数HandlerEvent()**。

每一个Channel的处理函数会根据Channel类中封装的实际发生的事件,执行Channel类中封装的各事件处理函数

比如一个Channel发生了可读事件,可写事件,则这个Channel的HandlerEvent()就会调用提前注册在这个Channel的可读事件和可写事件处理函数,又比如另一个Channel只发生了可读事件,那么HandlerEvent( )就只会调用提前注册在这个Channel中的可读事件处理函数。

总结

EventLoop起到一个驱动循环的功能,Poller负责从事件监听器上获取监听结果。而Channel类则在其中起到了将fd及其相关属性封装的作用,将fd及其感兴趣事件和发生的事件以及不同事件对应的回调函数封装在一起,这样在各个模块中传递更加方便。接着EventLoop调用。

常用命令

查看服务器是否在监听

netstat -tuln | grep 8888

查看具体哪个进程占用了端口

sudo lsof -i :8080

一些错误

错误代码说明

400 Bad Request:客户端发送的请求有语法错误或格式不正确。确保请求符合 HTTP 协议标准

404 Not Found:服务器无法找到请求的资源。检查服务器路径和资源文件是否正确

构建脚本

这个构建过程是将源代码编译成可执行文件的步骤,通常包括配置构建环境、生成构建文件、编译源代码和链接生成最终的可执行文件或库。以下是对这个特定构建过程的详细解释:

1. 设置变量

+ pwd
+ SOURCE_DIR=/home/asus/ly/cpp/WebServer
+ BUILD_DIR=../build
+ BUILD_TYPE=Debug
  • pwd:打印当前工作目录。
  • SOURCE_DIR:源代码目录,位于 /home/asus/ly/cpp/WebServer
  • BUILD_DIR:构建目录,设置为相对于源代码目录的 ../build
  • BUILD_TYPE:构建类型,设置为 Debug,意味着生成的可执行文件包含调试信息,有助于调试。

2. 创建并切换到构建目录

+ mkdir -p ../build/Debug
+ cd ../build/Debug
  • mkdir -p ../build/Debug:创建构建目录及其父目录(如果不存在)。
  • cd ../build/Debug:切换到构建目录。

3. 生成构建文件

+ cmake -DCMAKE_BUILD_TYPE=Debug /home/asus/ly/cpp/WebServer
  • 使用 cmake 命令生成构建文件。
  • -DCMAKE_BUILD_TYPE=Debug:指定构建类型为 Debug
  • /home/asus/ly/cpp/WebServer:源代码目录。

CMake 将读取源代码目录中的 CMakeLists.txt 文件,根据配置生成适合目标平台的构建文件(例如 Makefile)。

4. 编译源代码

+ make
  • make:根据 CMake 生成的构建文件,编译源代码。

编译输出

CMake 配置

-- The CXX compiler identification is GNU 11.3.0
-- Detecting CXX compiler ABI info
...
-- Build files have been written to: /home/asus/ly/cpp/build/Debug
  • CMake 识别出使用的 C++ 编译器是 GNU 11.3.0。
  • 检测并确认编译器的功能和特性。
  • 生成构建文件并写入构建目录。

编译和链接

[  4%] Building CXX object WebServer/base/CMakeFiles/libserver_base.dir/AsyncLogging.cpp.o
...
[100%] Built target HTTPClient
  • make 逐步编译各个源文件,并显示编译进度。
  • 每一行输出表示正在编译一个源文件,例如 AsyncLogging.cpp,生成对应的目标文件(.o 文件)。
  • 目标文件链接成静态库 libserver_base.a 或可执行文件 WebServerHTTPClient

警告信息

In file included from /home/asus/ly/cpp/WebServer/WebServer/Channel.cpp:3:
...
warning: ‘Channel::lastEvents_’ will be initialized after [-Wreorder]
  • 编译过程中出现了变量初始化顺序的警告,提示代码中的变量初始化顺序与类中声明的顺序不一致。
  • 尽管这些警告不会阻止编译成功,但修复这些警告有助于提高代码质量和稳定性。

总结

这个构建过程是典型的 CMake 和 Make 编译流程,主要步骤包括:

  1. 设置环境变量和构建目录。
  2. 使用 CMake 生成构建系统(如 Makefile)。
  3. 使用 Make 编译和链接源代码,生成可执行文件或库。

这个流程自动化了编译过程,确保代码可以在不同平台上进行一致的构建。

总体结构分析

服务端具体功能分析

主函数

//创建事件循环对象mainLoop,然后创建服务器对象myHTTPServer,传入事件循环、线程数和端口号。
//调用myHTTPServer.start()启动服务器,然后调用mainLoop.loop()进入事件循环。
EventLoop mainLoop;
Server myHTTPServer(&mainLoop, threadNum, port);
myHTTPServer.start();
mainLoop.loop();

并发模型

EventLoop类分析与总结

EventLoop 类是一个核心组件,用于实现基于事件驱动的编程模型。它通过 epoll 提供的高效 I/O 多路复用机制管理事件循环。

具体功能和作用

事件循环管理
  • loop() 方法启动事件循环,不断监听和处理 I/O 事件,直到 quit() 方法被调用。
  • quit() 方法用于退出事件循环。
事件通知机制
  • 使用 eventfd 创建的文件描述符 wakeupFd_,配合 epoll,实现线程间的事件通知。
  • wakeup() 方法向 wakeupFd_ 写入数据,以唤醒阻塞的事件循环。
  • handleRead() 方法从 wakeupFd_ 读取数据,处理唤醒事件。
任务调度
  • 支持异步任务的调度和执行,通过 runInLoop()queueInLoop() 方法实现。
  • runInLoop() 方法在事件循环线程中直接执行任务,如果在其他线程调用,则将任务放入队列。
  • queueInLoop() 方法将任务添加到待处理队列,并在需要时唤醒事件循环。
  • doPendingFunctors() 方法在每次循环迭代中处理所有待执行的任务。

总结

EventLoop 类通过事件循环、高效的 I/O 多路复用和线程间通知机制,实现了一个高性能、线程安全的事件驱动模型。

Channel类分析

Channel 类是一个重要的封装类,用于将文件描述符与其对应的事件处理器绑定在一起。它的主要作用是:

  1. 管理文件描述符的事件:如读事件、写事件和连接事件。

  2. 绑定事件处理函数:为每个文件描述符注册相应的事件处理函数。

  3. EventLoop协同工作:在事件发生时,调用相应的处理函数。

作用

Channel 类在项目中主要负责文件描述符的事件管理和处理。它将文件描述符与事件处理函数关联起来,并在事件发生时调用相应的处理函数。Channel 类与EventLoopEpoll协同工作,形成事件驱动的框架。EventLoop 负责事件循环和事件调度,Epoll 负责高效地监视文件描述符上的事件,而Channel 则负责将文件描述符与事件处理函数绑定,并在事件发生时调用处理函数。

EPOLLIN 表示读事件,即有数据可读;EPOLLET 表示边缘触发模式。边缘触发模式可以减少不必要的事件通知,提高性能。在高并发服务器中,边缘触发模式可以减少系统调用次数,提升效率。

总结

Channel 类是事件驱动框架中的核心组件之一,它将文件描述符与事件处理函数绑定,并在事件发生时调用相应的处理函数。通过与EventLoopEpoll协同工作,Channel 实现了高效的事件管理和处理。

Epoll类分析(poller)

Epoll 类是基于 Linux epoll 接口实现的 I/O 多路复用管理器。

它的主要功能是管理多个文件描述符上的 I/O 事件,并将这些事件分发给相应的处理函数。通过使用 epoll 提供的高效事件通知机制,Epoll 类可以在高并发场景下有效地管理和调度大量 I/O 请求。

具体功能

  1. 文件描述符管理

    • epoll_add(SP_Channel request, int timeout):将新的文件描述符添加到 epoll 监视列表中,并设置超时时间。
    • epoll_mod(SP_Channel request, int timeout):修改已存在的文件描述符的事件类型。
    • epoll_del(SP_Channel request):从 epoll 监视列表中删除文件描述符。
  2. 事件循环

    • poll():等待文件描述符上有事件发生,返回有事件的描述符列表。
  3. 事件处理

    • getEventsRequest(int events_num):根据事件数量,将发生事件的描述符转换为 Channel 对象,并返回待处理的请求数据。
  4. 定时器管理

    • add_timer(SP_Channel request_data, int timeout):为文件描述符设置定时器,超时后自动处理。
    • handleExpired():处理所有过期的定时器事件。

Epoll类在EventLoop中的作用

Epoll 类在 EventLoop 中主要承担以下角色:

  • 事件循环管理器:负责等待和分发 I/O 事件。
  • 事件分发器:将 epoll 返回的事件分发给相应的 Channel 进行处理。
  • 定时器管理:处理超时事件,确保事件及时处理。

EventLoop 使用 Epoll 类实例 poller_ 管理 I/O 事件。addToPoller()updatePoller()removeFromPoller() 方法分别用于向 epoll 中添加、更新和移除事件。loop() 方法中调用 poller_->poll() 获取触发的事件并处理,每个事件通过相应的 Channel 对象调用其绑定的事件处理函数。

定时器(Timer类分析,如上)

TimerNodeTimerManager 类共同实现了一个定时器机制,用于管理 HTTP 请求的超时处理。

主要功能

  1. 管理HTTP请求的生命周期:通过定时器管理 HTTP 请求的有效期,当请求超时未处理时自动清理。

  2. 延迟删除机制:通过延迟删除机制优化性能,避免频繁创建和销毁 HTTP 请求对象。

  3. 高效的时间管理:使用优先队列(小根堆)管理定时器节点,确保高效处理定时事件。

线程池

线程池的主要目的是提高并发性和效率,通过使用多个线程来执行任务,而不是依赖单个线程来处理所有任务。

线程池实现了基本的线程管理和任务队列管理功能,能够创建、添加任务、销毁和执行任务。通过使用互斥锁和条件变量,确保线程之间的同步和任务的安全分发。

EventLoopThreadPool类分析

事件循环线程池 (EventLoopThreadPool),用于管理和分配多个事件循环线程 (EventLoopThread)。它的主要功能是创建、启动和管理这些事件循环线程,以实现负载均衡和高效的事件处理。

管理一组线程,以便在并发执行任务时提高效率。线程池的设计可以避免频繁创建和销毁线程的开销,并且可以通过复用线程来处理多个任务。

作用

  1. 提高并发处理能力:线程池能够管理多个线程并行执行任务,提高系统的并发处理能力。
  2. 减少线程创建和销毁的开销:通过复用线程,减少了频繁创建和销毁线程的开销。
  3. 任务队列管理:任务可以按需添加到队列中,线程池中的线程会自动从队列中获取任务并执行,简化了任务的调度和管理。
  4. 线程安全:使用互斥锁和条件变量确保任务队列操作的线程安全,避免竞争条件。

启动线程池

void EventLoopThreadPool::start() {
baseLoop_->assertInLoopThread(); // 确认该函数在主线程中调用
started_ = true; // 标记线程池已启动

for (int i = 0; i < numThreads_; ++i) {
std::shared_ptr<EventLoopThread> t(new EventLoopThread()); // 创建新的 EventLoopThread 对象
threads_.push_back(t); // 将线程对象存储到 threads_ 中
loops_.push_back(t->startLoop()); // 启动线程并获取其 EventLoop 对象,将其存储到 loops_ 中
}
}
  • 确保在主线程中调用。
  • 标记线程池已启动。
  • 创建指定数量的 EventLoopThread 线程,启动每个线程并将其 EventLoop 对象存储在 loops_ 数组中。

轮询的方式(round-robin)

轮询(Round-Robin)是一种常见的调度算法,用于均匀地分配任务或资源。

轮询的优势
  • 负载均衡:通过轮询方式可以确保任务均匀分配到每个线程,避免某个线程过载。
  • 简单实现:轮询算法简单且容易实现,不需要复杂的逻辑。
  • 公平性:每个 EventLoop 都有平等的机会处理任务,确保资源利用均衡。

轮询调度是一种简单的调度算法,通过循环方式均匀地分配任务或资源,确保每个任务或资源有平等的机会被处理。在 getNextLoop() 方法中,它用于依次选择 EventLoop,以实现负载均衡。

使用轮询调度可以确保任务均匀分配到每个 EventLoop,避免某个线程过载,保持系统负载均衡和资源利用均衡。

getNextLoop() 方法通过维护一个索引 next_,每次调用时返回 loops_ 中的当前 EventLoop,并更新 next_ 指向下一个 EventLoop。当 next_ 达到 numThreads_ 时,重置为 0,从而实现循环。

优势是简单易实现,确保任务均匀分配;劣势是在负载不均的情况下,可能无法最优化性能,因为它不考虑每个任务的实际负载和处理时间。

总结

这个 EventLoopThreadPool 类的主要功能是:

  1. 管理多个 EventLoopThread 对象。
  2. 启动这些线程并获取它们对应的 EventLoop 对象。
  3. 通过 getNextLoop 方法实现负载均衡,轮询地返回下一个 EventLoop 对象,用于处理新事件。

这在多线程事件驱动编程中,可以高效地分发和处理事件,提升系统的并发性能。

EventLoopThread类分析

EventLoopThread 类管理一个单独的线程,在该线程中运行一个 EventLoop 对象。它提供了启动线程和获取 EventLoop 对象的方法,以便在多线程环境中处理事件。
其主要功能是启动一个新的线程,在该线程中运行事件循环,并提供线程安全的机制以确保事件循环正确启动。

EventLoopThread 类的主要功能是:

  1. 创建一个新的事件循环线程并在该线程中运行事件循环。

  2. 提供 startLoop 方法启动线程并获取事件循环对象,确保事件循环正确启动。

  3. 使用互斥锁和条件变量实现线程同步,确保在事件循环对象 loop_ 初始化完成前,调用线程处于等待状态。

  4. 在析构函数中安全退出事件循环并等待线程结束。

Server类分析与总结

Server 类是一个简单的 HTTP 服务器,它的主要功能是接受新连接并处理请求。

Server 类的主要功能和作用是:

  1. 初始化服务器:创建监听套接字,设置为非阻塞模式,处理 SIGPIPE 信号。

  2. 启动服务器:启动线程池,设置监听 Channel 的事件和处理函数,并将 Channel 添加到事件循环中。

  3. 处理新连接:接受新连接,设置新连接的属性,将新连接交给线程池中的某个事件循环进行处理。

主要流程

  1. 接受新连接:当有新连接到来时,调用 accept 函数接受连接。
  2. 获取线程池中的下一个事件循环:从线程池中获取一个事件循环,用于处理该新连接。
  3. 设置新连接属性:将新连接的文件描述符设置为非阻塞模式,并禁用 Nagle 算法。
  4. 创建 HttpData 对象:为新连接创建一个 HttpData 对象,并将其与 Channel 关联。
  5. 添加到事件循环:将 HttpData 对象的 newEvent 方法添加到事件循环的任务队列中,等待异步处理。

总结

Server` 类是一个简单的 HTTP 服务器,主要负责初始化服务器,启动线程池,接受新连接,并将新连接交给线程池中的某个事件循环进行处理。

HttpData

  1. 封装连接处理逻辑HttpData 对象封装了与 HTTP 请求和响应相关的所有逻辑。这样可以将连接处理的各个部分(如读取请求、解析请求、生成响应、发送响应等)组织在一起,使代码更清晰和模块化。

  2. 管理连接状态:每个连接都有其状态(如读取请求、发送响应等),HttpData 对象可以维护和管理这些状态。同时,HttpData 对象还可以保存一些与连接相关的资源,如文件描述符、缓冲区等。

  3. **关联 Channel**:每个连接都需要一个 Channel 来监控其 I/O 事件,HttpData 对象可以持有并管理这个 Channel。通过 Channel,可以方便地注册和处理该连接的读写事件。

  4. 生命周期管理:通过智能指针来管理 HttpData 对象,可以方便地控制其生命周期,避免内存泄漏。当连接关闭或超时后,HttpData 对象会自动销毁,释放相关资源。

  5. 异步任务处理:在多线程环境中,HttpData 对象可以被安全地传递给其他线程处理。在 Server::handNewConn 中,将 HttpData 对象添加到线程池中的事件循环,可以实现异步处理,从而提高服务器的并发能力。

Nagle 算法

禁用 Nagle 算法的目的是为了降低网络延迟,提高实时通信的效率。以下是详细解释:

什么是 Nagle 算法

Nagle 算法是一种用于优化网络数据传输的算法,主要用于减少小包传输的数量,从而减少网络拥塞。它的工作原理是,将小数据包积累起来,直到有足够多的数据或者收到一个确认包(ACK)后再进行发送。

禁用 Nagle 算法的原因

在某些网络应用中,特别是实时性要求高的应用,禁用 Nagle 算法可以带来以下好处:

  1. 降低延迟

    • 实时通信应用(如在线游戏、即时通讯、视频会议等)通常需要尽快将数据发送给对方,而不是等待数据积累到一定量再发送。如果启用了 Nagle 算法,数据包可能会被延迟发送,从而增加网络延迟。
  2. 提高数据传输的实时性

    • 对于需要实时响应的应用,禁用 Nagle 算法可以确保数据包立即发送,提高数据传输的实时性。这样可以使客户端和服务器之间的交互更加迅速,响应更加及时。
  3. 改善小包传输性能

    • 某些应用场景下,数据包通常较小且频繁发送。在这种情况下,禁用 Nagle 算法可以避免数据包在发送缓冲区中积累,从而提高传输效率和响应速度。

应用场景

禁用 Nagle 算法通常适用于以下场景:

  • 在线游戏:游戏中的动作和状态变化需要尽快传递给服务器和其他玩家,以保持游戏的同步性和流畅性。
  • 即时通讯:聊天消息和状态更新需要立即发送和接收,以提供良好的用户体验。
  • 实时音视频通信:语音和视频数据需要以最低延迟发送,以保证通信的实时性和清晰度。

条件变量

条件变量(Condition Variable)是用于多线程编程中的一种同步原语,它允许线程在某个条件不满足时进入等待状态,并在条件满足时被唤醒,从而实现线程之间的协调和同步。条件变量通常与互斥锁(Mutex)一起使用,以保证对共享资源的访问是线程安全的。

条件变量的基本操作

条件变量主要有以下几个基本操作:

  1. 等待操作(Wait)

    • 当某个条件不满足时,线程可以调用条件变量的等待操作进入等待状态。等待操作通常是原子的,即线程在释放互斥锁并进入等待状态的过程是不可中断的。
    • 例如:pthread_cond_wait(&cond, &mutex);
    • 在调用pthread_cond_wait之前,线程必须持有与条件变量相关联的互斥锁。调用pthread_cond_wait后,线程会释放互斥锁并进入等待状态,直到被唤醒。
  2. 唤醒操作(Signal/Broadcast)

    • 当条件满足时,线程可以调用条件变量的唤醒操作唤醒一个或多个等待该条件的线程。
    • 例如:pthread_cond_signal(&cond);(唤醒一个等待线程)
    • 例如:pthread_cond_broadcast(&cond);(唤醒所有等待线程)
    • 唤醒操作会通知等待在条件变量上的一个或多个线程,通知它们条件已经满足,可以继续执行。

条件变量的使用场景

条件变量通常用于以下场景:

  1. 生产者-消费者模型

    • 在生产者-消费者模型中,生产者线程向缓冲区添加数据,消费者线程从缓冲区取数据。条件变量可以用于实现生产者和消费者之间的同步。
    • 当缓冲区为空时,消费者线程进入等待状态;当缓冲区不为空时,生产者线程通知等待的消费者线程。
  2. 任务队列

    • 在线程池或任务队列中,工作线程从任务队列中取任务执行。当任务队列为空时,工作线程进入等待状态;当有新任务添加到队列时,通知等待的工作线程继续执行。
  3. 事件等待

    • 当某个事件需要等待另一个事件的完成时,可以使用条件变量。例如,一个线程等待另一个线程完成初始化工作后再继续执行。

线程复用(如上,线程池)

线程池中的线程复用是通过创建一组线程并将它们保持在等待状态,当有任务需要执行时,线程池将任务分配给这些线程来执行。线程完成任务后,不会终止,而是继续等待下一个任务,这样就实现了线程的复用。

以下是线程池中线程复用的实现机制:

线程池的核心组成部分

  1. 线程池管理器

    • 负责创建和管理线程池中的线程。
    • 维护一个任务队列,当有新任务提交时,将任务放入队列中。
  2. 工作线程

    • 线程池中的线程通常称为工作线程。
    • 工作线程在创建后进入一个循环,不断从任务队列中取任务执行。
  3. 任务队列

    • 存储需要执行的任务。
    • 任务队列通常是一个线程安全的队列,使用互斥锁和条件变量来实现线程安全的访问。

线程池中的线程复用过程

  1. 线程池初始化

    • 创建指定数量的工作线程,并将它们放入等待状态。
  2. 提交任务

    • 当有新任务提交时,将任务放入任务队列中,并通知等待的工作线程有新任务可执行。
  3. 工作线程执行任务

    • 工作线程从任务队列中取任务执行,执行完成后继续等待下一个任务。
  4. 线程复用

    • 线程完成任务后不退出,而是继续等待任务队列中的新任务,这样实现了线程的复用。

客户端分析 HTTPClient

一个简单的客户端程序,用于向服务器发送 HTTP 请求并接收响应。

  1. 创建套接字并连接到服务器:程序使用不同的 HTTP 请求数据与服务器进行通信。
  2. 非阻塞套接字:通过设置套接字为非阻塞模式,避免读写操作阻塞程序的执行。
  3. 发送 HTTP 请求:程序分别发送三种不同的 HTTP 请求数据,观察服务器的响应。
  4. 接收服务器响应:读取服务器返回的数据并打印输出。

其他

argc 参数 和 argv 参数

在C/C++程序中,main函数是程序的入口点,int main(int argc, char *argv[]) 是其标准签名之一,其中包含两个参数:argc 和 argv。

argc 参数
argc 是一个整数,表示命令行参数的个数。argc 的值至少为1,因为第一个参数总是程序的名称。

argv 参数
argv 是一个指向字符指针的指针数组(通常解释为字符串数组)。它包含了命令行输入的参数,每个参数是一个C风格的字符串。argv[0] 是程序的名称或路径,而 argv[1] 到 argv[argc-1] 是命令行传递的其他参数。

选项字符串的语法

选项字符串中的每个字符表示一个选项。如果一个字符后跟一个冒号(:),则表示该选项需要一个参数。如果没有冒号,则表示该选项不需要参数。

const char *str = “t:l:p:”; 的含义
在这句代码中,选项字符串 str 被设置为 “t:l:p:”,这意味着 getopt 函数将会解析三个选项:

t:需要一个参数。
l:需要一个参数。
p:需要一个参数。