高性能服务器程序框架 - 有限状态机
liebian365 2024-11-20 18:25 3 浏览 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,从而实现状态转移。
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)