百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

高性能服务器程序框架 - 有限状态机

liebian365 2024-11-20 18:25 23 浏览 0 评论

有限状态机是逻辑单元内部的一种高效编程方法。

有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来编写相应的处理逻辑,代码如下:


STATE_MACHINE(Packahe _pack)
{
    PackageType _type = _pack.GetType();
    switch (_type)
    {
    case type_A:
        process_package_A(_pack);
        break;
    case type_B:
        process_package_B(_pack);
        break;    
    default:
        break;
    }
}

这就是一个简单的有限状态机,只不过该状态机的每个状态都是相互独立的,即状态之间没有相互转移。状态之间的转移是需要状态机内部驱动的。下面是带状态转移的有限状态机:

STATE_MACHINE()
{
    State cur_State = type_A;
    while(cur_State != type_C)
    {
        Package _pack = getNewPackage();
        switch (cur_State)
        {
        case type_A:
            process_package_state_A(_pack);
            cur_State = type_B;
            break;
        case type_A:
            process_package_state_B(_pack);
            cur_State = type_C;
            break;    
        default:
            break;
        }
    }
}

该状态机包含三种状态:type_A、type_B和type_C,其中type_A是状态机的开始状态,type_C是状态机的结束状态。状态机的当前状态记录在cur_State变量中。在一趟循环过程中,状态机先通过getNewPackage方法获得一个新的数据包,然后根据cur_State变量的值判断如何处理该数据包。数据包处理完之后,状态机通过cur_State变量传递目标状态值来实现状态转移。那么当状态机进入下一趟循环时,它将执行新的状态对应的逻辑。

下面我们考虑有限状态机应用的一个实例:HTTP请求的读取和分析。

很多网络协议,包括TCP协议和IP协议,都在其头部中提供头部长度字段。程序根据该字段的值就可以知道是否接受到一个完整的协议头部。但HTTP协议并未提供这样的头部长度字段,并且其头部长度变化也很大,可以只有十几字节,也可以有上百字节。

根据协议规定,我们判断HTTP头部结束的依据是遇到一个空行,该空行包含一对回车换行符(<CR><LF>)。如果一次读操作没有读入HTTP请求的整个头部,即没有遇到空行,那么我们必须等待客户继续写数据并再次读入,因此,我们每完成一次读操作,就要分析新读入的数据中是否有空行。不过在寻找空行的过程中,我们可以同时完成对整个HTTP请求头部的分析(记住,空行前面还有请求行和头部域),以提高解析HTTP请求的效率。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

/*读缓冲区大小*/
#define BUFFER_SIZE 4096

/*
主状态机的两种可能状态,分别表示:
当前正在分析请求行,
当前正在分析头部字段
*/
enum CHECK_STATE{
    CHECK_STATE_REQUESTLINE = 0,
    CHECK_STATE_HEADER
};

/*
从状态机的三种可能状态,即行的读取状态:
读取到一个完整的行,
行出错,
行数据尚且不完整,
*/
enum LINE_STATUS{
    LINE_OK = 0,
    LINE_BAD,
    LINE_OPEN  
};

/*服务器处理HTTP请求的结果*/
enum HTTP_CODE{
    NO_REQUEST, //请求不完整,需要继续读取客户数据
    GET_REQUEST, //获得一个完整的客户请求
    BAD_REQUEST, //客户请求有语法错误
    FORBIDDEN_REQUEST, //ch客户对资源没有足够的访问权限
    INTERNAL_ERROR, //服务器内部错误
    CLOSED_CONNECTION //客户端已经关闭连接了
};

/*为简化问题,没有给客户端发送一个完整的HTTP应答报文,  
而只是根据服务器的处理结果发送如下成功或失败信息*/
static const char* szret[] = {"I get a correct result\n", "Something wrong\n"};

/*从状态机, 用于解析出一行内容*/
LINE_STATUS parse_line(char* buffer, int& checked_index, int& read_index)
{
    char temp;
    /*checked_index指向buffer(应用程序的读缓冲区)中当前正在分析的字节,
      read_index指向buffer中客户数据的尾部的下一字节。
      buffer中第0~checked_index都已经分析完毕。
      第checked_index ~ (read-index-1)字节由下面的循环挨个分析*/
    for(; checked_index < read_index; ++checked_index){
        /*获取当前要分析的字节*/
        temp = buffer[checked_index];
        /*如果当前的字节是'\r',即回车符,则说明可能读取一个完整的行*/
        if(temp == '\r'){
            /*如果"\r"字符碰巧是目前buffer中的最后一个已经被读入的客户数据,
              那么这次分析没有读取到一个完整的行,
              返回LINE_OPEN以表示还需要读取客户端才能进一步分析*/
              if( checked_index + 1 == read_index ){
                  return LINE_OPEN;
              }
              /*如果下一个字符是"\n",则说明我们成功读取到一个完整的行*/
              else if (buffer[checked_index+1] == '\n'){
                  buffer[checked_index++] = '\0';
                  buffer[checked_index++] = '\0';
                  return LINE_OK;
              }
              /*否则的话,说明客户端发送的HTTP请求存在语法错误*/
              return LINE_BAD;
        }
        /*如果当前字节是"\n",即换行符,则也说明可能读取到一个完整的行*/
        else if(temp == '\n'){
            if((checked_index > 1) && (buffer[checked_index - 1] == '\r')){
                buffer[checked_index-1] = '\0';
                buffer[checked_index++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    /*如果所有内容都分析完毕也没遇到"\r"则返回LINE_OPEN, 
    表示还需要继续读取客户数据才能进一步分析*/
    return LINE_OPEN;
}

/*分析请求行*/
HTTP_CODE parse_requestline(char* temp, CHECK_STATE& checkstate)
{
    char *url = strpbrk(temp, " \t");
    /*如果请求行中没有空白字符或"\t"字符,则HTTP请求必有问题*/
    if(!url){
        return BAD_REQUEST;
    }
    *url++ = '\0';
    char* method = temp;
    if(strcasecmp(method, "GET") == 0){
       //仅支持GET方法
       printf("The request method is GET\n"); 
    }
    else{
        return BAD_REQUEST;
    }

    url += strspn(url, " \t");
    char* version = strpbrk(url, " \t");
    if(!version){
        return BAD_REQUEST;
    }
    *version++ = '\0';
    version += strspn(version, " \t");
    if(strcasecmp(version, "HTTP/1.1") != 0){
        return BAD_REQUEST;
    }
    if(strncasecmp(url, "http://", 7) == 0){
        url += 7;
        url = strchr(url, '/');
    }
    if(!url || url[0] != '/n'){
        return BAD_REQUEST;
    }
    printf("The request URL is: %s\n", url);
    /*HTTP请求行处理完毕,状态转移到头部字段的分析*/
    checkstate = CHECK_STATE_HEADER;
    return NO_REQUEST;
}

/*分析头部字段*/
HTTP_CODE parse_headers(char *temp)
{
    /*遇到一个空行,说明我们得到了一个正确的HTTP请求*/
    if(temp[0] == '\0'){
        return GET_REQUEST;
    }
    else if (strncasecmp(temp, "Host:", 5) == 0){
        temp += 5;
        temp += strspn(temp, " \t");
        printf("the request host is: %s\n", temp);
    }
    else{
        printf("I can not handle this header\n");
    }
    return NO_REQUEST;
}

/*分析HTTP请求的入口函数*/
HTTP_CODE parse_content(char* buffer, int& checked_index, CHECK_STATE& checkstate,
                int& read_index, int& start_line)
{
    LINE_STATUS linestatus = LINE_OK; /*记录当前行的读取状态*/
    HTTP_CODE retcode = NO_REQUEST; /*记录HTTP请求的处理结果*/
    /*主状态机, 用于从buffer中取出所有完整的行*/
    while((linestatus = parse_line(buffer, checked_index, read_index)) == LINE_OK){
        char * temp = buffer + start_line; /*start_line是行在buffer中的起始位置*/
        start_line = checked_index; /*记录下一行的起始位置*/
        /*checkstate记录主状态机的当前状态*/
        switch (checkstate)
        {
        case CHECK_STATE_REQUESTLINE:
            retcode = parse_requestline(temp, checkstate);
            if(retcode == BAD_REQUEST){
                return BAD_REQUEST;
            }
            break;
        case CHECK_STATE_HEADER:
            retcode = parse_headers(temp);
            if(retcode == BAD_REQUEST){
                return BAD_REQUEST;
            }    
            else if(retcode == GET_REQUEST){
                return GET_REQUEST;
            }
            break;
        default:
            return INTERNAL_ERROR;
        }
    }
    if (linestatus == LINE_OPEN){
        return NO_REQUEST;
    }
    else{
        return BAD_REQUEST;
    }
}

int main(int argc, char const *argv[])
{
    if(argc < 2){
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip ,&address.sin_addr);
    address.sin_port = htons(port);

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listen >= 0);
    int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(listenfd, 5);
    assert(ret != -1);
    
    struct sockaddr_in client_address;
    socklen_t client_addrlength = sizeof(client_address);
    int fd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
    if(fd < 0){
        printf("errno is: %d\n", errno);
    }
    else{
        char buffer[BUFFER_SIZE];
        memset(buffer,  '\0', BUFFER_SIZE);
        int data_read = 0;
        int read_index = 0;
        int check_index = 0;
        int start_line = 0;

        CHECK_STATE checlstate = CHECK_STATE_REQUESTLINE;
        while (1){
            data_read = recv(fd, buffer+read_index, BUFFER_SIZE-read_index, 0);
            if(data_read == -1){
                printf("reading failed\n");
                break;
            }
            else if(data_read == 0){
                printf("remote client has closed the connection\n");
                break;
            }
            read_index += data_read;
            HTTP_CODE result = parse_content(buffer, check_index, checlstate,
                        read_index, start_line);
            if(result == NO_REQUEST){
                continue;
            }
            else if(result == GET_REQUEST){
                send(fd, szret[0], strlen(szret[0]), 0);
                break;
            }
            else{
                send(fd, szret[1], strlen(szret[1]), 0);
                break;
            }
        }
        close(fd);
    }
    close(listenfd);
    return 0;
}

我们将上述两个有限状态机分别称为主状态机和从状态机,这体现了他们之间的关系:主状态机在内部调用从状态机。

下面是从状态机,即parse_line函数的转移过程:

这个状态机的初始状态是LINE_OK,其原始驱动力来自于buffer中新道达的客户数据。在main函数中,我们循环调用recv函数往buffer中读入客户数据。每次成功读取数据后,我们就调用parse_content函数来分析新读入的数据。parse_content函数首先要做的就是调用parse_line函数来获取一个行。现在假设服务器经过一次recv调用之后,buffer的内容以及部分变量的值如下图a所示:


parse_line函数处理之后的结果如b所示,它挨个检查a所示的buffer中checked_index到read_index-1之间的字节,判断是否存在行结束符,并更新checked_index的值。当前buffer中不存在行结束符,所以parse_line返回LINE_OPEN。

接下来,程序继续调用recv以读取更多客户数据,这次读操作后buffer中的内容以及部分变量的值如c所示,然后parse_line函数就又开始处理这部分新到来的数据,如d所示。这次它读到了一个完整的行,即“HOST:localhost\r\n”。此时,parse_line函数就可以将这行内容递交给parse_content函数中的主状态机来处理了。

主状态机使用checkstate变量来记录当前的状态。

  • 如果当前的状态是CHECK_STATE_REQUESTLINE,则表示parse_line函数解析的行是请求行,于是主状态机调用parse_requestline来分析请求行
  • 如果当前的状态是CHECK_STATE_HEADER,则表示parse_line函数解析的出的是头部字段,于是主状态机调用parse_headers来分析头部字段。

checkstate变量的初始值是CHECK_STATE_REQUESTLINE,parse_requestline函数在成功地分析完请求行之后将其设置为CHECK_STATE_HEADER,从而实现状态转移。

相关推荐

“版本末期”了?下周平衡补丁!国服最强5套牌!上分首选

明天,酒馆战棋就将迎来大更新,也聊了很多天战棋相关的内容了,趁此机会,给兄弟们穿插一篇构筑模式的卡组推荐!老规矩,我们先来看10职业胜率。目前10职业胜率排名与一周前基本类似,没有太多的变化。平衡补丁...

VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符&quot;

首先,程序中头文件的选择,要选择头文件,在文件中是没有对M_PI的定义的。选择:项目——>”XXX属性"——>配置属性——>C/C++——>预处理器——>预处理器定义,...

东营交警实名曝光一批酒驾人员名单 88人受处罚

齐鲁网·闪电新闻5月24日讯酒后驾驶是对自己和他人生命安全极不负责的行为,为守护大家的平安出行路,东营交警一直将酒驾作为重点打击对象。5月23日,东营交警公布最新一批饮酒、醉酒名单。对以下驾驶人醉酒...

Qt界面——搭配QCustomPlot(qt platform)

这是我第一个使用QCustomPlot控件的上位机,通过串口精确的5ms发送一次数据,再将读取的数据绘制到图表中。界面方面,尝试卡片式设计,外加QSS简单的配了个色。QCustomPlot官网:Qt...

大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写

老友相聚,仗剑江湖!《大话西游2》2021全民PK季4月激燃打响,各PK玩法鏖战齐开,零门槛参与热情高涨。PK季期间,不仅各种玩法奖励丰厚,参与PK趣闻录活动,投稿自己在PK季遇到的趣事,还有机会带走...

测试谷歌VS Code AI 编程插件 Gemini Code Assist

用ClaudeSonnet3.7的天气测试编码,让谷歌VSCodeAI编程插件GeminiCodeAssist自动编程。生成的文件在浏览器中的效果如下:(附源代码)VSCode...

顾爷想知道第4.5期 国服便利性到底需优化啥?

前段时间DNF国服推出了名为“阿拉德B计划”的系列改版计划,截至目前我们已经看到了两项实装。不过关于便利性上,国服似乎还有很多路要走。自从顾爷回归DNF以来,几乎每天都在跟我抱怨关于DNF里面各种各样...

掌握Visual Studio项目配置【基础篇】

1.前言VisualStudio是Windows上最常用的C++集成开发环境之一,简称VS。VS功能十分强大,对应的,其配置系统较为复杂。不管是对于初学者还是有一定开发经验的开发者来说,捋清楚VS...

还嫌LED驱动设计套路深?那就来看看这篇文章吧

随着LED在各个领域的不同应用需求,LED驱动电路也在不断进步和发展。本文从LED的特性入手,推导出适合LED的电源驱动类型,再进一步介绍各类LED驱动设计。设计必读:LED四个关键特性特性一:非线...

Visual Studio Community 2022(VS2022)安装图文方法

直接上步骤:1,首先可以下载安装一个VisualStudio安装器,叫做VisualStudioinstaller。这个安装文件很小,很快就安装完成了。2,打开VisualStudioins...

Qt添加MSVC构建套件的方法(qt添加c++11)

前言有些时候,在Windows下因为某些需求需要使用MSVC编译器对程序进行编译,假设我们安装Qt的时候又只是安装了MingW构建套件,那么此时我们该如何给现有的Qt添加一个MSVC构建套件呢?本文以...

Qt为什么站稳c++GUI的top1(qt c)

为什么现在QT越来越成为c++界面编程的第一选择,从事QT编程多年,在这之前做C++界面都是基于MFC。当时为什么会从MFC转到QT?主要原因是MFC开发界面想做得好看一些十分困难,引用第三方基于MF...

qt开发IDE应该选择VS还是qt creator

如果一个公司选择了qt来开发自己的产品,在面临IDE的选择时会出现vs或者qtcreator,选择qt的IDE需要结合产品需求、部署平台、项目定位、程序猿本身和公司战略,因为大的软件产品需要明确IDE...

Qt 5.14.2超详细安装教程,不会来打我

Qt简介Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开库,主要用来开发图形用户界面(GraphicalUserInterface,GUI)程序。Qt是纯C++开...

Cygwin配置与使用(四)——VI字体和颜色的配置

简介:VI的操作模式,基本上VI可以分为三种状态,分别是命令模式(commandmode)、插入模式(Insertmode)和底行模式(lastlinemode),各模式的功能区分如下:1)...

取消回复欢迎 发表评论: