http连接处理(中)(四)

news2024/11/21 0:21:20

2. 结合代码分析请求报文解析

上一节我们对http连接的基础知识、服务器接收请求的处理流程进行了介绍,接下来将结合流程图和代码分别对状态机和服务器解析请求报文进行详解。

流程图部分,描述主、从状态机调用关系与状态转移过程。

代码部分,结合代码对http请求报文的解析进行详解。

2.1 流程图与状态机

从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。

  主状态机

三种状态,标识解析位置。

  1. CHECK_STATE_REQUESTLINE,解析请求行
  2. CHECK_STATE_HEADER,解析请求头
  3. CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求

从状态机

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

  1. LINE_OK,完整读取一行
  2. LINE_BAD,报文语法有误
  3. LINE_OPEN,读取的行不完整

2.2 代码分析-http报文解析

上节中介绍了服务器接收http请求的流程与细节,简单来讲,浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列后,工作线程从任务队列中取出一个任务进行处理。

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

void http_conn::process()
{
    HTTP_CODE read_ret=process_read();
    //NO_REQUEST,表示请求不完整,需要继续接收请求数据
    if(read_ret==NO_REQUEST)
    {
        //注册并监听读事件
        modfd(m_epollfd,m_sockfd,EPOLLIN);
        return;
    }

    //调用process_write完成报文响应
    bool write_ret=process_write(read_ret);
    if(!write_ret)
    {
        close_conn();
    }
    //注册并监听写事件
    modfd(m_epollfd,m_sockfd,EPOLLOUT);
}

本节将对报文解析的流程和process_read函数细节进行详细介绍。

HTTP_CODE含义:

表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析时只涉及到四种。

  1. NO_REQUEST
    • 请求不完整,需要继续读取请求报文数据
  2. GET_REQUEST
    • 获得了完整的HTTP请求
  3. BAD_REQUEST
    • HTTP请求报文有语法错误
  4. INTERNAL_ERROR
    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

解析报文整体流程:

process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。

  1. 判断条件
    • 主状态机转移到CHECK_STATE_CONTENT,该条件涉及解析消息体
    • 从状态机转移到LINE_OK,该条件涉及解析请求行和请求头部
    • 两者为或关系,当条件为真则继续循环,否则退出
  2. 循环体
    • 从状态机读取数据
    • 调用get_line函数,通过m_start_line将从状态机读取数据间接赋给text
    • 主状态机解析text
//m_start_line是行在buffer中的起始位置,将该位置后面的数据赋给text
//此时从状态机已提前将一行的末尾字符\r\n变为\0\0,所以text可以直接取出完整的行进行解析
char* get_line(){
    return m_read_buf+m_start_line;
}

http_conn::HTTP_CODE http_conn::process_read()
{
    //初始化从状态机状态、HTTP请求解析结果
    LINE_STATUS line_status=LINE_OK;
    HTTP_CODE ret=NO_REQUEST;
    char* text=0;

    //这里为什么要写两个判断条件?第一个判断条件为什么这样写?
    //具体的在主状态机逻辑中会讲解。

    //parse_line为从状态机的具体实现
    while((m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)||((line_status=parse_line())==LINE_OK))
    {
        text=get_line();

        //m_start_line是每一个数据行在m_read_buf中的起始位置
        //m_checked_idx表示从状态机在m_read_buf中读取的位置
        m_start_line=m_checked_idx;

        //主状态机的三种状态转移逻辑
        switch(m_check_state)
        {
            case CHECK_STATE_REQUESTLINE:
            {
                //解析请求行
                ret=parse_request_line(text);
                if(ret==BAD_REQUEST)
                    return BAD_REQUEST;
                break;
            }
            case CHECK_STATE_HEADER:
            {
                //解析请求头
                ret=parse_headers(text);
                if(ret==BAD_REQUEST)
                    return BAD_REQUEST;

                //完整解析GET请求后,跳转到报文响应函数
                else if(ret==GET_REQUEST)
                {
                    return do_request();
                }
                break;
            }
            case CHECK_STATE_CONTENT:
            {
                //解析消息体
                ret=parse_content(text);

                //完整解析POST请求后,跳转到报文响应函数
                if(ret==GET_REQUEST)
                    return do_request();

                //解析完消息体即完成报文解析,避免再次进入循环,更新line_status
                line_status=LINE_OPEN;
                break;
            }
            default:
            return INTERNAL_ERROR;
        }
    }
    return NO_REQUEST;
}

从状态机逻辑:

上面基础知识讲解中,对于HTTP报文的讲解遗漏了一点细节,在这里作为补充。

在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析,项目中便是利用了这一点。

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

  1. 从状态机从m_read_buf中逐字节读取,判断当前字节是否为\r
    • 接下来的字符是\n,将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
    • 接下来达到了buffer末尾,表示buffer还需要继续接收,返回LINE_OPEN
    • 否则,表示语法错误,返回LINE_BAD
  2. 当前字节不是\r,判断是否是\n(一般是上次读取到\r就到了buffer末尾,没有接收完整,再次接收时会出现这种情况)
    • 如果前一个字符是\r,则将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
  3. 当前字节既不是\r,也不是\n
    • 表示接收不完整,需要继续接收,返回LINE_OPEN
 if(m_checked_idx>1&&m_read_buf[m_checked_idx-1]=='\r')
            {
                m_read_buf[m_checked_idx-1]='\0';
                m_read_buf[m_checked_idx++]='\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }

    //并没有找到\r\n,需要继续接收
    return LINE_OPEN;
}

主状态机逻辑:

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

  1. CHECK_STATE_REQUESTLINE
    • 主状态机的初始状态,调用parse_request_line函数解析请求行
    • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
    • 解析完成后主状态机的状态变为CHECK_STATE_HEADER
//解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
    //在HTTP报文中,请求行用来说明请求类型,要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。
    //请求行中最先含有空格和\t任一字符的位置并返回
    m_url=strpbrk(text," \t");

    //如果没有空格或\t,则报文格式有误
    if(!m_url)
    {
        return BAD_REQUEST;
    }

    //将该位置改为\0,用于将前面数据取出
    *m_url++='\0';

    //取出数据,并通过与GET和POST比较,以确定请求方式
    char *method=text;
    if(strcasecmp(method,"GET")==0)
        m_method=GET;
    else if(strcasecmp(method,"POST")==0)
    {
        m_method=POST;
        cgi=1;
    }
    else
        return BAD_REQUEST;

    //m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有
    //将m_url向后偏移,通过查找,继续跳过空格和\t字符,指向请求资源的第一个字符
    m_url+=strspn(m_url," \t");

    //使用与判断请求方式的相同逻辑,判断HTTP版本号
    m_version=strpbrk(m_url," \t");
    if(!m_version)
        return BAD_REQUEST;
    *m_version++='\0';
    m_version+=strspn(m_version," \t");
    //仅支持HTTP/1.1
    if(strcasecmp(m_version,"HTTP/1.1")!=0)
        return BAD_REQUEST;

    //对请求资源前7个字符进行判断
    //这里主要是有些报文的请求资源中会带有http://,这里需要对这种情况进行单独处理
    if(strncasecmp(m_url,"http://",7)==0)
    {
        m_url+=7;
        m_url=strchr(m_url,'/');
    }

    //同样增加https情况
    if(strncasecmp(m_url,"https://",8)==0)
    {
        m_url+=8;
        m_url=strchr(m_url,'/');
    }

    //一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源
    if(!m_url||m_url[0]!='/')
        return BAD_REQUEST;

    //当url为/时,显示欢迎界面
    if(strlen(m_url)==1)
        strcat(m_url,"judge.html");

    //请求行处理完毕,将主状态机转移处理请求头
    m_check_state=CHECK_STATE_HEADER;
    return NO_REQUEST;
}

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

  1. CHECK_STATE_HEADER
    • 调用parse_headers函数解析请求头部信息
    • 判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。
    • 若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。
    • connection字段判断是keep-alive还是close,决定是长连接还是短连接
    • content-length字段,这里用于读取post请求的消息体长度
//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
    //判断是空行还是请求头
    if(text[0]=='\0')
    {
        //判断是GET还是POST请求
        if(m_content_length!=0)
        {
            //POST需要跳转到消息体处理状态
            m_check_state=CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        return GET_REQUEST;
    }
    //解析请求头部连接字段
    else if(strncasecmp(text,"Connection:",11)==0)
    {
        text+=11;

        //跳过空格和\t字符
        text+=strspn(text," \t");
        if(strcasecmp(text,"keep-alive")==0)
        {
            //如果是长连接,则将linger标志设置为true
            m_linger=true;
        }
    }
    //解析请求头部内容长度字段
    else if(strncasecmp(text,"Content-length:",15)==0)
    {
        text+=15;
        text+=strspn(text," \t");
        m_content_length=atol(text);
    }
    //解析请求头部HOST字段
    else if(strncasecmp(text,"Host:",5)==0)
    {
        text+=5;
        text+=strspn(text," \t");
        m_host=text;
    }
    else{
        printf("oop!unknow header: %s\n",text);
    }
    return NO_REQUEST;
}

如果仅仅是GET请求,如项目中的欢迎界面,那么主状态机只设置之前的两个状态足矣。

前面提到过GET和POST请求报文的区别之一是有无消息体部分,GET请求没有消息体,当解析完空行之后,便完成了报文的解析。

但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装。

为此,我们需要在解析报文的部分添加解析消息体的模块。

while((m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)||((line_status=parse_line())==LINE_OK))

那么,这里的判断条件为什么要写成这样呢?

在GET请求报文中,每一行都是\r\n作为结束,所以对报文进行拆解时,仅用从状态机的状态line_status=parse_line())==LINE_OK语句即可。

但,在POST请求报文中,消息体的末尾没有任何字符,所以不能使用从状态机的状态,这里转而使用主状态机的状态作为循环入口条件。

那后面的&& line_status==LINE_OK又是为什么?

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

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

  1. CHECK_STATE_CONTENT
    • 仅用于解析POST请求,调用parse_content函数解析消息体
    • 用于保存post请求消息体,为后面的登录和注册做准备
//判断http请求是否被完整读入
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{
    //判断buffer中是否读取了消息体
    if(m_read_idx>=(m_content_length+m_checked_idx)){

        text[m_content_length]='\0';

        //POST请求中最后为输入的用户名和密码
        m_string = text;

        return GET_REQUEST;
    }
    return NO_REQUEST;
}

状态机和HTTP报文解析是项目中最繁琐的部分,这次我们一举解决掉它,希望对各位小伙伴在理解项目的过程中有所帮助。

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

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

相关文章

内 网 优 化

拓扑 需求 1)所有部门中都使用了网关冗余技术,为了增强网关稳定性和冗余性 -配置VRRP -SW5是VLAN10和VLAN20的Master ,是VLAN30的Backup -SW6是VLAN10和VLAN20的Backup,是VLAN30的Master 2)交换机之间存在很多冗余链路,必须防止环…

AtcoderABC253场

A - Median?A - Median? 题目大意 给定三个整数a、b和c&#xff0c;判断b是否是这些整数的中位数。 思路分析 判断升序降序两种情况 时间复杂度分析 O(1) 代码 #include<iostream> using namespace std; int main() { int a,b,c; cin>>a>>b>>…

Non-Local Video Denoising by CNN

摘要 Non-local patch based methods were until recently state-of-the-art for image denoising but are now outper formed by CNNs. Y et they are still the state-of-the-art for video denoising, as video redundancy is a key factor to attain high denoising perfor…

JavaWeb——Cookie和Session的工作流程

目录 一、定义 1、Cookie定义 2、Session 二、Cookie和Session的联系和区别 1、联系 2、区别 一、定义 1、Cookie定义 Cookie是浏览器在本地存储数据的一种机制&#xff0c;来自于服务器。 服务器在响应中会带有Set-Cookie字段&#xff0c;通过这个字段就可以把要保存在浏…

及早识别面肌痉挛症状,科学治疗是关键!

随着现代社会的快节奏生活和各种压力的增加&#xff0c;面肌痉挛这一神经肌肉紊乱性疾病的发病率也逐渐上升。面肌痉挛是指由于面肌肌肉群异常收缩而导致的面部肌肉抽搐和不自主运动的症状。如果不及早识别和治疗&#xff0c;将对患者的生活质量产生严重影响。因此&#xff0c;…

C语言,封装自定义函数

1、封装自定义函数&#xff0c;计算数组的最大和&#xff0c;最大差 //第一数组 #include <stdio.h> #include <string.h> int MaxSum(int len,int arr[]); int MaxDel(int len,int arr[]); int main(int argc, const char *argv[]) {int arr[]{5,6,8,51,31,51,88…

2023 7.10~7.16 周报 (RTM研究与正演的Python复现)

0 上周回顾 上周简单阅读了论文《Deep-Learning Full-Waveform Inversion Using Seismic Migration Images》, 但是并没读完…因为这篇论文中提到一个技术吸引了注意力: RTM (Reverse-time migration) 于是计划下周去专门熟悉熟悉RTM的机制, 并且试着用Python复现这个操作. 另…

数据处理 | Matlab实现Lichtenberg算法的机器学习数据选择

文章目录 效果一览基本介绍源码设计参考资料效果一览 基本介绍 Matlab实现Lichtenberg算法的机器学习数据选择 Lichtenberg算法适用于回归和分类数据集,并根据数量和最大覆盖范围选择最佳算法。Lichtenberg算法(Lichtenberg algorithm,LA)是由Pereira等人于2021年提出的一种…

Python爬虫——urllib_下载

urlretrieve(url&#xff0c; filename)函数 url 代表的是下载的路径 filename文件的名字 下载网页: url_page "http://www.baidu.com" urllib.request.urlretrieve(url_page, baidu.html)下载图片: url_img "https://img0.baidu.com/it/u2751401762,34216…

VUE研究

1.v2与v3的区别 vue3对源码的管理根据模块进行拆分&#xff0c;在不同目录中对不同的模块进行分别维护&#xff1b; vue3是基于typescript语言进行开发的&#xff0c;这样可以进行更好的类型检查&#xff1b; vue3体积减小&#xff0c;去除了不常使用的API&#xff0c;Tree sha…

DevOps B站学习版(二)

学习地址&#xff1a; 01.DevOps的诞生_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Pt4y1H7Zq/?p1&vd_source1f09c23f556b3d6a9b7706f8db12fa54%E3%80%81 正文开始 找到这个地方&#xff0c;修改 可以写成基于标签拉取和构建工程&#xff0c;下面也选择Tag即可…

Python 自学 day03 容器tuple(元组)的定义与使用,序列,字典,集合,多返回值传递,不定长参数函数

1. tuple 元组 1.1 元组的定义 定义&#xff1a;元组同列表一样&#xff0c;都是可以封装多个、不同类型的元素在内。但最大的不同点在于: 元组一旦定义完成﹐就不可修改。 1.2 元组的创建方法 t1 (1,111,1111,11,1111,222) #元组的定义方法 t2 (22,) …

Postman+Newman+Git+Jenkins+Slack 接口自动化和监控

目录 前言&#xff1a; 一、Newman 介绍&#xff1a; 1、简介 2、安装 3、检查 4、运行 二、Newman 命令行介绍&#xff1a; newman run [options] 测试结果配置 ------------------------------------分 割 线----------------------------------------------------…

2023年最新水果编曲软件FL Studio Producer Edition 21.0.3 Build 3517中文完美至尊解锁免费下载安装激活详细教程

fl studio21.0.3.3517中文解锁特别版是一款功能强大的编曲软件&#xff0c;也就是众所熟知的水果软件。它可以编曲、剪辑、录音、混音&#xff0c;让您的计算机成为全功能录音室。除此之外&#xff0c;这款软件功能非常强大&#xff0c;为用户提供了许多音频处理工具&#xff0…

《红蓝攻防构建实战化网络安全防御体系》读书笔记

作者&#xff1a;奇安信安服团队 ◆ 1.3 红队 各个团队在演练中的角色与分工情况如下。目标系统运营单位&#xff1a;负责红队整体的指挥、组织和协调。安全运营团队&#xff1a;负责整体防护和攻击监控工作。攻防专家&#xff1a;负责对安全监控中发现的可疑攻击进行分析和研…

LiveNVR监控流媒体Onvif/RTSP功能-安全控制HTTP接口鉴权开启禁止游客访问开启后401 Unauthorized如何播放调用接口

LiveNVR安全控制HTTP接口鉴权开启禁止游客访问开启后401 Unauthorized如何播放调用接口&#xff1f; 1、安全控制1.1、接口鉴权1.2、禁止游客访问 2、401 Unauthorized2.1、携带token调用接口2.1.1、获取鉴权token2.1.2、调用其它接口2.1.2.1、携带 CookieToken2.1.2.2、携带 U…

VUE- quill-editor 编辑器使用及自定义toobar详解

vue使用编辑器&#xff0c;这里讲解编辑器quil-editor 官网&#xff1a;https://quilljs.com/docs/modules/toolbar 1&#xff1a;安装quill-eidtor npm i quill1.3.6 --save 2&#xff1a;创建一个页面&#xff0c;再template里写入 <template><div class"…

@ConditionalOnMissingBean 不生效

还要一点需要注意的是 有顶级接口类型写接口类型,像下面这个也控制不住加载多个相同类型的Bean,因为父类最先加载,子类之间不能算作同一种类型Bean

Git详细安装教程

对于Git这块&#xff0c;我也算是个小白&#xff0c;最近在学习Git&#xff0c;所以趁此机会详细讲解一下Git的安装教程以及安装过程中遇到的问题&#xff0c;也欢迎大家对其补充&#xff0c;共同进步&#xff01; 1、下载Git Git的下载地址&#xff08;windows系统&#xff…

【STM32CubeIDE】 stm32f103的内部Flash读写,double数值读写

单片机stm32f103c8t6&#xff0c;程序存储器64Kb&#xff1a; 对其最后一页&#xff0c;第63页进行读写操作&#xff0c;空间1Kb。 写入一个32位的数据0x12345678到Flash首地址为0x0800FC00.则在Flash中存储情况如下&#xff1a; 即&#xff0c;低位地址存储数据的低位&#xf…