在逻辑单元内部的一种高效的编程方法:有限状态机。
有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来编写相应的处理逻辑,下面代码展示的是状态独立的有限状态机
STATE_MACHINE(Package_pack)
{
PackageType_type=_pack.GetType();
switch(_type)
{
case type_A:
process_package_A(_pack);
break;
case type_B:
process_package_B(_pack);
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_B:
process_package_state_B(_pack);
cur_State=type_C;
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请求的效率。下面代码使用主、从两个有限状态机实现了最简单的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, CHECK_STATE_CONTENT };
/*从状态机的三种可能状态,即行的读取状态,分别表示:读取到一个完整的行、行出错和行数据尚且不完整*/
enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };
/*服务器处理HTTP请求的结果:NO_REQUEST表示请求不完整,需要继续读取客户数据;GET_REQUEST表示获得了一个完整的客户请求;BAD_REQUEST表示客户请求有语法错误;FORBIDDEN_REQUEST表示客户对资源没有足够的访问权限;INTERNAL_ERROR表示服务器内部错误;CLOSED_CONNECTION表示客户端已经关闭连接了*/
enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, FORBIDDEN_REQUEST, 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* szTemp, CHECK_STATE& checkstate )
{
char* szURL = strpbrk( szTemp, " \t" );
/*如果请求行中没有空白字符或“\t”字符,则HTTP请求必有问题*/
if ( ! szURL )
{
return BAD_REQUEST;
}
*szURL++ = '\0';
char* szMethod = szTemp;
if ( strcasecmp( szMethod, "GET" ) == 0 )/*仅支持GET方法*/
{
printf( "The request method is GET\n" );
}
else
{
return BAD_REQUEST;
}
szURL += strspn( szURL, " \t" );
char* szVersion = strpbrk( szURL, " \t" );
if ( ! szVersion )
{
return BAD_REQUEST;
}
*szVersion++ = '\0';
szVersion += strspn( szVersion, " \t" );
/*仅支持HTTP/1.1*/
if ( strcasecmp( szVersion, "HTTP/1.1" ) != 0 )
{
return BAD_REQUEST;
}
/*检查URL是否合法*/
if ( strncasecmp( szURL, "http://", 7 ) == 0 )
{
szURL += 7;
szURL = strchr( szURL, '/' );
}
if ( ! szURL || szURL[ 0 ] != '/' )
{
return BAD_REQUEST;
}
//URLDecode( szURL );
printf( "The request URL is: %s\n", szURL );
/*HTTP请求行处理完毕,状态转移到头部字段的分析*/
checkstate = CHECK_STATE_HEADER;
return NO_REQUEST;
}
/*分析头部字段*/
HTTP_CODE parse_headers( char* szTemp )
{
/*遇到一个空行,说明我们得到了一个正确的HTTP请求*/
if ( szTemp[ 0 ] == '\0' )
{
return GET_REQUEST;
}
else if ( strncasecmp( szTemp, "Host:", 5 ) == 0 )/*处理“HOST”头部字段*/
{
szTemp += 5;
szTemp += strspn( szTemp, " \t" );
printf( "the request host is: %s\n", szTemp );
}
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* szTemp = buffer + start_line;/*start_line是行在buffer中的起始位置*/
start_line = checked_index;/*记录下一行的起始位置*/
/*checkstate记录主状态机当前的状态*/
switch ( checkstate )
{
case CHECK_STATE_REQUESTLINE:/*第一个状态,分析请求行*/
{
retcode = parse_requestline( szTemp, checkstate );
if ( retcode == BAD_REQUEST )
{
return BAD_REQUEST;
}
break;
}
case CHECK_STATE_HEADER:/*第二个状态,分析头部字段*/
{
retcode = parse_headers( szTemp );
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* 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( listenfd >= 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 checked_index = 0;/*当前已经分析完了多少字节的客户数据*/
int start_line = 0;/*行在buffer中的起始位置*/
/*设置主状态机的初始状态*/
CHECK_STATE checkstate = 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, checked_index, checkstate,read_index, start_line );
if( result == NO_REQUEST )/*尚未得到一个完整的HTTP请求*/
{
continue;
}
else if( result == GET_REQUEST )/*得到一个完整的、正确的HTTP请求*/
{
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;
}
这里面有一个关于"\n","\r"的小知识,建议看这篇博客:(69条消息) \r,\n与\r\n有什么区别?_阿牛哥818的博客-CSDN博客
代码中的两个有限状态机分别称为主状态机和从状态机,这体现了它们之间的关系:主状态机在内部调用从状态机。下面先分析从状态机,即parse_line函数,它从buffer中解析出一个行。下图描述了其可能的状态及状态转移过程:
这个状态机的初始状态是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,从而实现状态转移。