C++轻量级Web服务器TinyWebServer源码分析之http篇

news2025/1/23 7:28:26

文章目录

  • http篇简介
  • 一、服务器接收http请求和http报文解析
    • 解析报文整体流程
    • 从状态机逻辑
    • 主状态机逻辑
  • 二、服务器响应请求报文

http篇简介

http类这篇个人觉得是最难同时也是最繁琐的一篇,本篇在基础知识方面,包括epoll、HTTP报文格式、状态码和有限状态机,不做赘述,所有源码分析的篇章基础知识会做单开一篇或者读者可以看小白一文看懂社长服务器来补基础知识。真的佩服那个叫社长的男人,我读代码且困难,他是自己手敲的啊!

http报文处理流程
1、浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。

2、工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。

3、解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。

http类头文件源码如下:

class http_conn
{
public:
    static const int FILENAME_LEN = 200;//设置读取文件的名称m_real_file大小
    static const int READ_BUFFER_SIZE = 2048;//设置读缓冲区m_read_buf大小
    static const int WRITE_BUFFER_SIZE = 1024;//设置写缓冲区m_write_buf大小
    //报文的请求方法,本项目只用到GET和POST
    enum METHOD{GET=0,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATH};
    //主状态机的状态
    enum CHECK_STATE{CHECK_STATE_REQUESTLINE=0,CHECK_STATE_HEADER,CHECK_STATE_CONTENT};
    //报文解析的结果
    enum HTTP_CODE{NO_REQUEST,GET_REQUEST,BAD_REQUEST,NO_RESOURCE,FORBIDDEN_REQUEST,FILE_REQUEST,INTERNAL_ERROR,CLOSED_CONNECTION};
    //从状态机的状态  
    enum LINE_STATUS{LINE_OK=0,LINE_BAD,LINE_OPEN};
public:
    http_conn() {}
    ~http_conn() {}

public:
	//初始化套接字地址,函数内部会调用私有方法init
    void init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname);
    void close_conn(bool real_close = true);    //关闭http连接
    void process();
    bool read_once();//读取浏览器端发来的全部数据
    bool write();//响应报文写入函数
    sockaddr_in *get_address(){return &m_address;}//获取套接字地址
    void initmysql_result(connection_pool *connPool);//同步线程初始化数据库读取表
    int timer_flag;
    int improv;
//原文中使用线程池初始化数据库表的函数initresultFileCGI()头文件中没找到,但是具体实现文件中找到了;

private:
    void init();
    HTTP_CODE process_read();//从m_read_buf读取,并处理请求报文
    bool process_write(HTTP_CODE ret);//向m_write_buf写入响应报文数据
    HTTP_CODE parse_request_line(char *text);//主状态机解析报文中的请求行数据
    HTTP_CODE parse_headers(char *text);//主状态机解析报文中的请求头数据
    HTTP_CODE parse_content(char *text); //主状态机解析报文中的请求内容
    
    HTTP_CODE do_request();//生成响应报文
    char *get_line() { return m_read_buf + m_start_line; };//m_start_line是已经解析的字符,get_line用于将指针向后偏移,指向未处理的字符
    LINE_STATUS parse_line();//从状态机读取一行,分析是请求报文的哪一部分
    void unmap();//释放共享内存空间资源

//根据响应报文格式,生成对应8个部分,以下函数均由do_request调用
    bool add_response(const char *format, ...);
    bool add_content(const char *content);
    bool add_status_line(int status, const char *title);
    bool add_headers(int content_length);
    bool add_content_type();
    bool add_content_length(int content_length);
    bool add_linger();
    bool add_blank_line();

public:
    static int m_epollfd;
    static int m_user_count;
    MYSQL *mysql;
    int m_state;  //读为0, 写为1

private:
    int m_sockfd;
    sockaddr_in m_address;
    char m_read_buf[READ_BUFFER_SIZE];//存储读取的请求报文数据
    long m_read_idx;//缓冲区中m_read_buf中数据的最后一个字节的下一个位置
    long m_checked_idx;//m_read_buf读取的位置m_checked_idx
    int m_start_line;//m_read_buf中已经解析的字符个数
    char m_write_buf[WRITE_BUFFER_SIZE];//存储发出的响应报文数据
    int m_write_idx;//指示buffer中的长度
    
    CHECK_STATE m_check_state;//主状态机的状态
    METHOD m_method;//请求方法

//以下为解析请求报文中对应的6个变量//存储读取文件的名称
    char m_real_file[FILENAME_LEN];
    char *m_url;
    char *m_version;
    char *m_host;
    long m_content_length;
    bool m_linger;


    char *m_file_address;//读取服务器上的文件地址
    struct stat m_file_stat;
    struct iovec m_iv[2];//io向量机制iovec
    int m_iv_count;
    int cgi;        //是否启用的POST
    char *m_string; //存储请求头数据
    int bytes_to_send;//剩余发送字节数
    int bytes_have_send;//已发送字节数
    char *doc_root;//网站根目录,文件夹内存放请求的资源和跳转的html文件

    map<string, string> m_users;
    int m_TRIGMode;
    int m_close_log;

    char sql_user[100];
    char sql_passwd[100];
    char sql_name[100];
};

一、服务器接收http请求和http报文解析

浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。

各子线程通过process函数对任务进行处理,调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务。

以下主从状态机解析报文流程图和解释来自社长原文:
在这里插入图片描述
主状态机
三种状态,标识解析位置。

  • CHECK_STATE_REQUESTLINE,解析请求行

  • CHECK_STATE_HEADER,解析请求头

  • CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求

从状态机
三种状态,标识解析一行的读取状态。

  • LINE_OK,完整读取一行

  • LINE_BAD,报文语法有误

  • LINE_OPEN,读取的行不完整

对于主从状态机解析的结果定义为HTTP_CODE,分别为

  • NO_REQUEST//请求不完整,需要继续读取请求报文数据

  • GET_REQUEST//获得了完整的HTTP请求

  • BAD_REQUEST //HTTP请求报文有语法错误

  • INTERNAL_ERROR//服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

解析报文整体流程

process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。
在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析。
判断条件

  • 主状态机转移到CHECK_STATE_CONTENT,该条件涉及解析消息体

  • 从状态机转移到LINE_OK,该条件涉及解析请求行和请求头部

  • 两者为或关系,当条件为真则继续循环,否则退出

循环体

  • 从状态机读取数据

  • 调用get_line函数,通过m_start_line将从状态机读取数据间接赋给text

  • 主状态机解析text

从状态机逻辑

从状态机负责读取buffer中的数据,将每行数据末尾的\r\n置为\0\0,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析。
在这里插入图片描述

主状态机逻辑

主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。

CHECK_STATE_REQUESTLINE

  • 主状态机的初始状态,调用parse_request_line函数解析请求行

  • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号

  • 解析完成后主状态机的状态变为CHECK_STATE_HEADER

解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。

CHECK_STATE_HEADER

  • 调用parse_headers函数解析请求头部信息

  • 判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。

  • 若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。

  • connection字段判断是keep-alive还是close,决定是长连接还是短连接

  • content-length字段,这里用于读取post请求的消息体长度

解析完消息体后,报文的完整解析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT,也就是说,符合循环入口条件,还会再次进入循环,这并不是我们所希望的。

为此,增加了&& line_status==LINE_OK,并在完成消息体解析后,将line_status变量更改为LINE_OPEN,此时可以跳出循环,完成报文解析任务。

CHECK_STATE_CONTENT

  • 仅用于解析POST请求,调用parse_content函数解析消息体

  • 用于保存post请求消息体,为后面的登录和注册做准备

下面是源码函数截图(代码太多了…):
initmysql_result函数是对数据库的操作封装
而 addfd 、removefd 、modfd 、setnonblocking
这几个函数都是epoll常用操作的封装,具体作用如社长源码中注释所示:
在这里插入图片描述

read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。
在这里插入图片描述

二、服务器响应请求报文

浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,进入相应的逻辑和模块。

其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_once和http_conn::write完成数据的读取与发送。

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/413953.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python 小型项目大全 71~75

七十一、声音模拟 原文&#xff1a;http://inventwithpython.com/bigbookpython/project71.html 类似于西蒙电子玩具&#xff0c;这款识记游戏使用第三方playsound模块&#xff0c;播放四种不同的声音&#xff0c;分别对应键盘上的A、S、D、F键。当你成功地重复游戏给你的图案时…

驱动保护 -- 通过PID保护指定进程

一、设计界面 1、添加一个编辑框输入要保护的进程PID&#xff0c;并添加两个按钮&#xff0c;一个保护进程&#xff0c;一个解除保护 2、右击编辑框&#xff0c;添加变量 二、驱动层代码实现 1、声明一个受保护的进程PID数组 static UINT32 受保护的进程PID[256] { 0 }; 2…

Python Web开发技巧II

Postman安置Cookie 对于大型项目而已&#xff0c;所携带的cookie往往都不止一个&#xff0c;而是一堆&#xff0c;甚至特别特别长&#xff0c;postman文档提供的cookie操作是全局的&#xff0c;但需要一个一个打&#xff08;折磨&#xff09;&#xff0c;唯一的优点就是作用域…

tocbot生成文章目录

学习链接 github上的tocbot npmjs上的tocbot 效果图 使用步骤 1. 安装tocbot npm install tocbot --save2. vue组件中使用引入tocbot 只需要引入tocbot&#xff0c;然后调用tocbot.init(…)&#xff0c;指定提取的文章内容所在的dom&#xff0c;以及要把生成的目录放到哪个…

4月第1周榜单丨飞瓜数据B站UP主排行榜(哔哩哔哩平台)发布!

飞瓜轻数发布2023年4月3日-4月9日飞瓜数据UP主排行榜&#xff08;B站平台&#xff09;&#xff0c;通过充电数、涨粉数、成长指数三个维度来体现UP主账号成长的情况&#xff0c;为用户提供B站号综合价值的数据参考&#xff0c;根据UP主成长情况用户能够快速找到运营能力强的B站…

Flink (十一) --------- Table API 和 SQL

目录一、快速上手1. 需要引入的依赖2. 一个简单示例二、基本 API1. 程序架构2. 创建表环境3. 创建表4. 表的查询5. 输出表6. 表和流的转换三、流处理中的表1. 动态表和持续查询2. 将流转换成动态表3. 用 SQL 持续查询3. 将动态表转换为流四、时间属性和窗口1. 事件时间2. 处理时…

PDF怎么转换成word格式?这三个方法转换效率很高

在日常办公生活中&#xff0c;我们经常需要对文件进行格式转换&#xff0c;尤其是将PDF文件转换为可编辑的Word文件。虽然在Office软件中将Word文件转换为PDF很容易&#xff0c;但是将PDF文件转换为Word文件却需要一些技巧。在保证安全性能的前提下&#xff0c;如何将PDF文件转…

回溯算法编程题集合(leetcode)

给定一个整数数组 nums 和一个正整数 k&#xff0c;找出是否有可能把这个数组分成 k 个非空子集&#xff0c;其总和都相等。 示例 1&#xff1a; 输入&#xff1a; nums [4, 3, 2, 3, 5, 2, 1], k 4 输出&#xff1a; True 说明&#xff1a; 有可能将其分成 4 个子集&#x…

【springboot】ApplicationListener用法及源码

用法 方法一&#xff1a;继承ApplicationListener 传递事件 继承于ApplicationEvent public class ForDebuggerEvent extends ApplicationEvent {public ForDebuggerEvent(Object source) {super(source);}public void executeEvent() {System.out.println("ForDebugge…

第四章 网络层

网络层服务 网络层需要实现的两项重要功能&#xff1a; 转发&#xff1b;当通过一条输入链路接收到一个分组后&#xff0c;路由器需要决策通过哪条输出链路将分组发送出去&#xff0c;并将分组从输入接口转移到输出接口路由选择&#xff1b;当分组从源主机流向目的主机时&…

24.SSM-SpringMVC延续--拦截器

目录 一、拦截器。 &#xff08;1&#xff09;拦截器的作用。 &#xff08;2&#xff09;拦截器与过滤器的区别。 &#xff08;3&#xff09;拦截器的方法。 &#xff08;4&#xff09;拦截器的快速入门。 &#xff08;5&#xff09;多拦截器。 &#xff08;5.1&#xf…

libui的简单使用

libui是一个 C 中简单且可移植&#xff08;但并非不灵活&#xff09;的 GUI 库&#xff0c;它使用每个平台原生的GUI技术进行绘制。 官网地址&#xff1a;链接 相关文件&#xff1a;链接 一、配置说明 1. 所需链接的库 在使用libui的过程中至少需要链接以下库 user32.lib ke…

NumPy 秘籍中文第二版:一、使用 IPython

原文&#xff1a;NumPy Cookbook - Second Edition 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 在本章中&#xff0c;我们将介绍以下秘籍&#xff1a; 安装 IPython使用 IPython 作为 Shell阅读手册页安装 matplotlib运行 IPython 笔记本导出 IPython 笔记本导入网…

分享:如何给 DBA 减负?

欢迎访问 OceanBase 官网获取更多信息&#xff1a;https://www.oceanbase.com/ 本文来自OceanBase社区分享&#xff0c;仅限交流探讨。原作者肖杨&#xff0c;OceanBase 软件开发工程师。 研发、数据分析师及运维内部人员有数据查询、数据订正等需求&#xff0c;若无管控平台&…

vscode推送文件至github步骤

@目录 1、步骤: 提交到本地仓库从本地仓库再提交到远程仓库2、具体操作: 2.1 准备工作 设置提交代码时的用户信息 开始前我们需要先设置提交的用户信息,包括用户名和邮箱: git config --global user.name "runoob" git config --global user.email "tes…

【mysql性能调优 • 二】mysql的启动关闭原理和实战,及常见的错误排查

前言 MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;属于 Oracle 旗下产品。MySQL是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Database Management System&#xff0c;关系…

【CSS】定位 ⑥ ( 使用绝对定位在父容器任意位置显示子容器 | 代码示例 )

文章目录一、需求分析及核心开发要点二、完整代码示例一、需求分析及核心开发要点 要实现如下功能 , 下图 粉色 部分是 整体 父容器 , 紫色元素 是 中心的核心位置 , 蓝色是左上角的浮标 , 红色是右下角的浮标 ; 首先分析父容器元素 ; 由于 子元素 需要使用 绝对定位 , 此处的…

【权限维持】Windows自启动映像劫持粘滞键辅助屏保后门WinLogon

文章目录权限维持-域环境&单机版-自启动权限维持-域环境&单机版-粘滞键权限维持-域环境&单机版-映像劫持权限维持-域环境&单机版-屏保&登录权限维持-域环境&单机版-自启动 1、自启动路径加载 C:\Users\Administrator\AppData\Roaming\Microsoft\Wind…

家装产业的数字化,正在成为越来越多人的新共识

一场数字化的浪潮&#xff0c;正在各行各业上演着。家装行业&#xff0c;亦不例外。可以说&#xff0c;家装产业的数字化&#xff0c;正在成为越来越多人的新共识。如何借助数字化的手段改造家装行业&#xff0c;如何乘着数字化的东风实现家装行业的全面转型升级&#xff0c;正…

uni-app--》如何实现网上购物小程序(中下)?

&#x1f3cd;️作者简介&#xff1a;大家好&#xff0c;我是亦世凡华、渴望知识储备自己的一名在校大学生 &#x1f6f5;个人主页&#xff1a;亦世凡华、 &#x1f6fa;系列专栏&#xff1a;uni-app &#x1f6b2;座右铭&#xff1a;人生亦可燃烧&#xff0c;亦可腐败&#xf…