当前位置: 首页 > news >正文

TinyWebserver的复现与改进(4):主线程的具体实现

GitHub - yzfzzz/MyWebServer: Linux高并发服务器项目,参考了TinyWebServer,将在此基础上进行性能改进与功能增加。为方便读者学习,附带详细注释和博客!

webserver

TinyWebserver的复现与改进(1):服务器环境的搭建与测试-CSDN博客

TinyWebserver的复现与改进(2):项目的整体框架-CSDN博客

TinyWebserver的复现与改进(3):线程同步机制类封装及线程池实现-CSDN博客

Reactor模式

今天我们将采用主从Reactor多线程模式,这是是大多数高性能服务器采用的模式

主从Reactor多线程模式要求主线程(I/O处理单元)只需负责:

  1. 监听文件描述符上是否有事件发生,
  2. 有的话就立即将该事件通知工作线程(逻辑单元),
  3. 将 socket 可读可写事件放入请求队列,交给工作线程处理。

除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。

Reactor模式的工作流程

使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:

  1. 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。 (即把listenfd、和已连接的客户端socketfd加入到epoll模型中)
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读。
  3. 当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
  4. 正在堵塞的某个工作线程被解除堵塞,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件。
  5. 当主线程调用 epoll_wait 等待 socket 可写。
  6. 当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
  7. 堵塞的某个工作线程被解除堵塞,它往 socket 上写入服务器处理客户请求的结果。

image-20240812174758935

代码实现

main函数流程图如图所示:

image-20240812181143205

初始化

创建线程池
// 定义一个线程池指针
threadpool<http_conn>* pool = NULL;
try {// 开辟一个线程池pool = new threadpool<http_conn>;
}catch(...)
{// 若异常则退出return 1;
}

使用new创建一个http_conn类型的线程池,返回的指针由pool接收。由于使用了 try……catch(...)语句,因此如果遇到异常则退出

创建客户端集合
// 开辟一块连续的http_conn数组,保存所有正在连接的客户端信息
http_conn* users = new http_conn[MAX_FD];

创建一个大小为 MAX_FD 的 http_conn 数组,当接收一个新 socket_fd 时,将会在 索引 i = socket的数组处(即 users[socket_fd]),初始化一个 http_conn 对象。这个对象保存着该客户端的所有信息。

创建监听
// 设置监听
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = 0;
struct sockaddr_in address;
address.sin_addr.s_addr = INADDR_ANY;	// 表示本机的所有IP都可以连接客户端
address.sin_family = AF_INET;  // 使用ipv4
address.sin_port = htons(port);// 设置端口复用
int reuse = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));// 绑定
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
if(ret == -1)
{
perror("bind");
exit(-1);
}// 开始监听
ret = listen(listenfd, 5);
if(ret == -1)
{
perror("listen");
exit(-1);
}
  1. socket 创建一个套接字,AF_INET表示使用 ipv4,SOCK_STREAM表示使用流式协议,和后面的0搭配表示使用的是TCP协议

  2. 但此时的socket,没有主机的端口号和ip信息,我们需要使用结构体address存储主机的相关信息:如IP(address.sin_addr.s_addr = INADDR_ANY)、ipv4(address.sin_family = AF_INET)、端口(address.sin_port = htons(port)),其中 htons() 表示 主机字节序 => 网络字节序 ,因为不同的机器在内存中的存放字节的顺序不同,我们需要统一标准

  3. 假设用户先运行./server.out,关闭后再快速运行./server.out,会报错:该端口被占用。这是因为结束./server,服务端处于FIN_WAIT状态,可以认为TCP通讯的TIME_WAIT时期需要等一小段时间。为了解决这个问题,常用setsockopt()表示开启端口复用功能。

  4. bind 将fd 和本地的IP和端口进行绑定,然后开始监听

epoll初始化
// 将listend添加到epoll模型中
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
addfd(epollfd, listenfd, false);
http_conn::m_epollfd = epollfd;

1.epoll_event用于检测文件描述符发生了什么事情(如读事件、写事件等)

2.addfd表示将listenfd加入epollfd中,这是一个自定义函数

epoll轮询

int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER,-1);

如果没有数据发送给服务器,将一直堵塞再此处

事件处理

有新客户端连接
 // 有新的客户端连接
if(sockfd == listenfd)
{struct sockaddr_in client_address;socklen_t client_addresslen = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addresslen);if(connfd < 0)
{printf("errno is %d\n", errno);continue;
}if(http_conn::m_user_count >= MAX_FD)
{close(connfd);continue;
}users[connfd].init(connfd, client_address);
}
可读
// 有读事件发生(可读)
else if(events[i].events & EPOLLIN)
{// 有读事件发生if(users[sockfd].read()){// 读的到数据pool->append(users+sockfd);}else{// 读不到数据users[sockfd].close_conn();}
}
可写
// 有写事件发生(可写)
else if(events[i].events & EPOLLOUT)
{if(!users[sockfd].write()){users[sockfd].close_conn();}
}

结束

close(epollfd);
close(listenfd);
delete [] users;
delete pool;
return 0;

关闭所有的fd

完整代码

#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include "threadpool.hpp"
#include "locker.h"
#include "http_conn.h"
#include <signal.h>
#include <assert.h>  #define MAX_FD 65536  // 最大的文件描述符
#define MAX_EVENT_NUMBER 10000 // 监听的最大事件数/* 函数指针的声明: 类型说明符 (*函数名) (参数)void(handler)(int) 声明了一个名为 handler 的函数指针,它指向一个接受一个 int 参数并返回 void 的函数
*/
void addsig(int sig, void(handler)(int))
{// sigaction的输入参数struct sigaction sa;// 指定sa内存区域的前n个字节都设置为某个特定的值('\0'),用于对新分配的内存进行初始化memset(&sa, '\0', sizeof(sa));// 写入函数指针,指向的函数就是信号捕捉到之后的处理函数sa.sa_handler = handler;// 设置临时阻塞信号集sigfillset(&sa.sa_mask);assert(sigaction(sig, &sa, NULL) != -1);
}int main(int argc, char* argv[])
{if(argc <= 1){// 要求输入格式为 ./a.out 10000  其中10000是端口号 printf("usage: %s port_number\n", basename(argv[0]));return 1;}// 端口号 string -> intint port = atoi(argv[1]);// 如果向一个没有读端的管道写数据,不用终止进程addsig(SIGPIPE, SIG_IGN);   // SIG_IGN: 忽略信号,这里指的是忽略信号 ·  SIGPIPE// 定义一个线程池指针threadpool<http_conn>* pool = NULL;try {// 开辟一个线程池pool = new threadpool<http_conn>;}catch(...){// 若异常则退出return 1;}// 开辟一块连续的http_conn数组,保存所有正在连接的客户端信息http_conn* users = new http_conn[MAX_FD];// 设置监听int listenfd = socket(AF_INET, SOCK_STREAM, 0);int ret = 0;struct sockaddr_in address;address.sin_addr.s_addr = INADDR_ANY;address.sin_family = AF_INET;address.sin_port = htons(port);// 设置端口复用int reuse = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));// 绑定ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));if(ret == -1){perror("bind");exit(-1);}// 开始监听ret = listen(listenfd, 5);if(ret == -1){perror("listen");exit(-1);}// 将listend添加到epoll模型中epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);addfd(epollfd, listenfd, false);http_conn::m_epollfd = epollfd;while(1){// epoll轮询,等待有数据发送int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER,-1);if((number < 0) && (errno != EINTR)){printf("epoll failture\n");break;}for(int i = 0; i < number; i++){int sockfd = events[i].data.fd;// 有新的客户端连接if(sockfd == listenfd){struct sockaddr_in client_address;socklen_t client_addresslen = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addresslen);if(connfd < 0){printf("errno is %d\n", errno);continue;}if(http_conn::m_user_count >= MAX_FD){close(connfd);continue;}users[connfd].init(connfd, client_address);}// 若对方异常端开或错误else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){users[sockfd].close_conn();}// 有读事件发生(可读)else if(events[i].events & EPOLLIN){// 有读事件发生if(users[sockfd].read()){// 读的到数据pool->append(users+sockfd);}else{// 读不到数据users[sockfd].close_conn();}}// 有写事件发生(可写)else if(events[i].events & EPOLLOUT){if(!users[sockfd].write()){users[sockfd].close_conn();}}}}close(epollfd);close(listenfd);delete [] users;delete pool;return 0;
}

相关文章:

TinyWebserver的复现与改进(4):主线程的具体实现

GitHub - yzfzzz/MyWebServer: Linux高并发服务器项目&#xff0c;参考了TinyWebServer&#xff0c;将在此基础上进行性能改进与功能增加。为方便读者学习&#xff0c;附带详细注释和博客&#xff01; TinyWebserver的复现与改进&#xff08;1&#xff09;&#xff1a;服务器环…...

DaemonSet 不能帮助我们做什么事情?

DaemonSet 不能帮助我们做什么事情&#xff1f; A. 保证集群内每一个(或者一些)节点都运行一组相同的Pod B. 跟踪集群节点状态&#xff0c;保证新加入的节点自动创建对应的Pod C. 跟踪集群节点状态&#xff0c;保证移除的节点删除对应的Pod D. 能够设置Pod重试次数&#xff0c;…...

开源模型应用落地-LangChain高阶-记忆组件-RedisChatMessageHistory正确使用(八)

一、前言 LangChain 的记忆组件发挥着至关重要的作用,其旨在协助大语言模型(LLM)有效地留存历史对话信息。通过这一功能,使得大语言模型在对话过程中能够更出色地维持上下文的连贯性和一致性,进而能够像人类的记忆运作方式那样,进行更为自然、流畅且智能化的交互。 它仿佛…...

解决Openwrt 串口默认是没有密码的方法

将串口登录加入密码方法如下&#xff1a; 步骤一&#xff1a;配置busybox的登录&#xff0c;可以在.config文件中添加如下 CONFIG_BUSYBOX_CONFIG_LOGINy 添加后&#xff0c;需要重新编译busybox。 步骤二&#xff1a;修改target/linux/ramips/base-files/etc/inittab文件 将…...

【vue讲解:v-model 之 lazy、number、trim、与后端交互、小电影案例】

2 v-model 之 lazy、number、trim lazy&#xff1a;等待input框的数据绑定时区焦点之后再变化 number&#xff1a;数字开头&#xff0c;只保留数字&#xff0c;后面的字母不保留&#xff1b;字母开头&#xff0c;都保留 trim&#xff1a;去除首位的空格<!DOCTYPE html> …...

ECCV 2024 | 南洋理工三维数字人生成新范式:结构扩散模型

该论文作者均来自于新加坡南洋理工大学 S-Lab 团队&#xff0c;包括博士后胡涛&#xff0c;博士生洪方舟&#xff0c;以及计算与数据学院刘子纬教授&#xff08;《麻省理工科技评论》亚太地区 35 岁以下创新者&#xff09;。S-Lab 近年来在顶级会议如 CVPR, ICCV, ECCV, NeurIP…...

2024.8.13-算法学习(原创+转载)

一、什么是张量并行&#xff08;Tensor Parallelism&#xff09; &#xff1f; 张量并行&#xff08;Tensor Parallelism&#xff09; 是一种分布式矩阵算法。 随着模型越来越大&#xff0c;模型内的矩阵也越来越大。一个大矩阵的乘法可以拆分成多个小矩阵的运算&#xff0c;…...

beautifulsoup的简单使用

文章目录 beautifulsoup一. beautifulsoup的简单使用1、安装2、如何使用3、对象的种类 二、beautifulsoup的遍历文档树2.1 子节点.contents 和 .children descendants2.2 节点内容.string.text 2.3 多个内容.strings**.stripped_strings** 2.4 父节点.parent.parents 三、beaut…...

【Python】Jupyter Notebook的安装及简单使用

Jupyter Notebook的安装及简单使用1、安装2、language设置为中文3、Jupyter Notebook启动4、Jupyter Notebook的常用快捷方式5、将Notebook笔记转为其他文件格式保存 Jupyter Notebook的安装及简单使用 不安装AnaCoda&#xff0c;但需要使用Jupyter Notebook 1、安装 pip inst…...

中国自动驾驶出租车冲击网约车市场

近年来&#xff0c;中国的自动驾驶技术迅速发展&#xff0c;对传统网约车市场构成了越来越大的冲击。随着科技巨头百度旗下的萝卜快跑等公司加速推广无人驾驶出租车&#xff0c;这一趋势引发了广泛的讨论和担忧。 自动驾驶技术的迅猛发展 中国自动驾驶行业正处于快速发展阶段&…...

解决浏览器书签同步问题,极空间部署开源免费的跨平台书签同步工具『xBrowserSync』

解决浏览器书签同步问题&#xff0c;极空间部署开源免费的跨平台书签同步工具『xBrowserSync』 哈喽小伙伴们好&#xff0c;我是Stark-C~ 作为一个喜欢折腾的数码党&#xff0c;我平时上网冲浪使用的浏览器绝不会只限于一种&#xff0c;就比如说我在上班的地方只会用到Edge浏…...

14个SpringBoot优化小妙招

今天我们来分享一下平时用SpringBoot开发时候的一些优化小妙招&#xff0c;用好这些优化小妙招让我们开发的系统架构、系统代码、开发流程、测试流程、运维监控看起来就跟写诗一样优雅&#xff0c;让我们每个人手头负责的代码和工程都要很漂亮~~~ 这里的优化小妙招很多不是说直…...

Elasticsearch 度量(Metric)聚合详解及示例

Elasticsearch 提供了强大的聚合功能&#xff0c;允许用户对数据进行深入的统计分析。度量&#xff08;Metric&#xff09;聚合是其中一种&#xff0c;它用于对数值型数据进行计算&#xff0c;如求和、平均值、最大值、最小值等。本文将详细介绍 Elasticsearch 的度量聚合&…...

基于 jsp 的健身俱乐部会员系统设计与实现

点击下载源码 基于 jsp 的健身俱乐部会员系统设计与实现 摘 要 目前我国虽然己经开发出了应用计算机操作的健身俱乐部管理系统&#xff0c;但管理软件&#xff0c;管理方法和管理思想三者往往相脱节。造成我国健身俱乐部信息管理系统极端化的缺陷。在国外健身俱乐部已经有了一…...

苍穹外卖项目DAY01

苍穹外卖项目Day01 1、软件开发整体介绍 1.1、软件开发流程 1.2、角色分工 项目经理&#xff1a;对整个项目负责&#xff0c;任务分配、把控进度产品经理&#xff1a;进行需求调研&#xff0c;输出需求调研文档、产品原型等UI设计师&#xff1a;根据产品原型输出界面效果图架…...

SpringBoot(Ⅰ)——HelloWorld和基本打包部署+Pom依赖概述+@SpringBootApplication注解+自动装配原理+约定大于配置

前言 如果SSM学的比较好&#xff0c;那么SpringBoot说白了就两件事:约定大于配置和自动装配 SpringBoot不会提供任何的功能拓展&#xff0c;完全依赖我们手动添加 所以SpringBoot的本质是一个依赖脚手架&#xff0c;可以快速集成配置各种依赖 1.1 SpringBoot相关依赖 创建…...

[Unity]关闭URP的SRP,开启GPU Instancing。

1. 对应材质的gpu instancing勾选上。 2. 游戏初始化时动态关闭SRP&#xff0c;或者在Graphics里全局关闭。动态关闭的代码如下&#xff1a; GraphicsSettings.useScriptableRenderPipelineBatching false; 模型合批的一些规则&#xff1a; 1. 模型一致。 2. 材质一致。 …...

04创建型设计模式——建造者模式

一、建造者模式简介 建造者模式&#xff08;Builder Pattern&#xff09;又被称为生成器模式。它旨在构建一个复杂对象的各个部分&#xff0c;而不需要指定该对象的具体类。该模式特别适用于对象的构建过程复杂且需要多个步骤的情况。建造者模式是一种对象创建型模式之一&…...

前端开发中的代码规范

引言 在前端开发中&#xff0c;遵循良好的代码规范是非常重要的。这不仅能提高代码的可读性和可维护性&#xff0c;还能帮助团队成员更好地协作。本文将介绍一些前端开发中常用的代码规范&#xff0c;并探讨它们的重要性。 1. 代码规范的重要性 1.1 可读性 良好的代码规范可…...

WHAT - 远程控制机制

目录 1. 客户端-服务器架构2. 连接建立3. 数据传输4. 通信协议5. 安全性6. 远程控制软件示例7. 操作流程示例 远程控制别人的电脑涉及到技术和安全多个方面。其基本机制通常包括以下几个方面&#xff1a; 1. 客户端-服务器架构 远程控制软件通常采用客户端-服务器架构&#x…...

苹果手机录音功能在哪里?3招轻松打开手机录音

无论是记录重要的会议内容、捕捉生活中的美好瞬间&#xff0c;还是进行语言学习&#xff0c;苹果手机的录音功能都能提供极大的便利。那么&#xff0c;苹果手机录音功能在哪里呢&#xff1f;本文将为您揭示苹果手机录音功能的藏身之处&#xff0c;并通过3个简单步骤&#xff0c…...

RCE之突破长度限制

我们在写webshell时通常会遇到过滤&#xff0c;但除了过滤之外还可能会有长度限制&#xff0c;这里就简单说一下关于RCE突破长度限制的技巧 突破16位 例如&#xff1a;PHP Eval函数参数限制在16个字符的情况下 &#xff0c;如何拿到Webshell&#xff1f; <?php $param …...

Arduino控制带编码器的直流电机速度

Arduino DC Motor Speed Control with Encoder, Arduino DC Motor Encoder 作者 How to control dc motor with encoder:DC Motor with Encoder Arduino, Circuit Diagram:Driving the Motor with Encoder and Arduino:Control DC motor using Encoder feedback loop: How …...

LangChain与Elasticsearch向量数据库的完美结合

在过去的一年中&#xff0c;生成式 AI (Generative AI) 领域取得了显著的进展。许多新的服务和工具应运而生。其中&#xff0c;LangChain 已成为构建大语言模型 (LLM) 应用程序&#xff08;例如检索增强生成 (RAG) 系统&#xff09;最受欢迎的框架之一。该框架极大地简化了原型…...

element时间段选择器或时间选择器 只设置默认起始时间或者结束时间,不显示问题

element时间段选择器或时间选择器 只设置默认起始时间或者结束时间&#xff0c;不显示问题 <div v-for"(item,index) in [a,b]":key"item"><el-date-pickerv-if"b"v-model"value1[item]"type"datetimerange"value-…...

Vue 3 中,组件间传值有多种方式

在 Vue 3 中&#xff0c;组件间传值有多种方式&#xff0c;以下是几种常见的方式 父组件向子组件传值&#xff08;通过 props&#xff09;&#xff1a;以下是几个父组件向子组件传值的示例&#xff1a;示例 1&#xff1a;传递字符串示例 2&#xff1a;传递数字示例 3&#xff1…...

前置(3):npm 和npx异同点

npm&#xff08;Node Package Manager&#xff09;和npx&#xff08;Node Package Execute&#xff09;是两个密切相关但用途不同的命令行工具&#xff0c;它们都是Node.js生态系统中的重要组成部分。 npm 用途&#xff1a;npm是Node.js的包管理器&#xff0c;主要用于安装、…...

笔记(day17)集合概述、List、Set、比较器

集合Collection 一.概述 ​ 集合可以理解为数据结构的封装,根据不同的特性及操作性能进行分类 二.继承体系 三.Collection中常用方法 ​ collection是集合中的父类,所以collection中的方法是所有集合中都有的 ​ 集合中只能保存引用类型(Object),无法保存基本类型 ​ Colle…...

C语言从头学45——I/O函数(二)

本文继续学习I/O函数&#xff0c;并延续前文的编号。 (三)、sscanf() 函数 sscanf() 函数与scanf() 有些相似&#xff0c;不同之处sscanf() 是从已有的字符串里面获取数据&#xff1b;这个函数也是定义在stdio.h中。 功能&#xff1a;处理已经输入到计算机中的字…...

Python爬虫——爬取bilibili中的视频

爬取bilibili中的视频 本次爬取&#xff0c;还是运用的是requests方法 首先进入bilibili官网中&#xff0c;选取你想要爬取的视频&#xff0c;进入视频播放页面&#xff0c;按F12&#xff0c;将网络中的名称栏向上拉找到第一个并点击&#xff0c;可以在标头中&#xff0c;找到…...