Linux网络编程——有限状态机

这篇具有很好参考价值的文章主要介绍了Linux网络编程——有限状态机。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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中解析出一个行。下图描述了其可能的状态及状态转移过程:

Linux网络编程——有限状态机

这个状态机的初始状态是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函数中的主状态机来处理了。

Linux网络编程——有限状态机

主状态机使用checkstate变量来记录当前的状态。如果当前的状态是CHECK_STATE_REQUESTLINE,则表示parse_line函数解析出的行是请求行,于是主状态机调用parse_requestline来分析请求行;如果当前的状态是CHECK_STATE_HEADER,则表示parse_line函数解析出的是头部字段,于是主状态机调用parse_headers来分析头部字段.checkstate变量的初始值是CHECK_STATE_REQUESTLINE,parse_requestline函数在成功地分析完请求行之后将其设置为CHECK_STATE_HEADER,从而实现状态转移。文章来源地址https://www.toymoban.com/news/detail-465102.html

到了这里,关于Linux网络编程——有限状态机的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《TCP/IP网络编程》阅读笔记--Timewait状态和Nagle算法

            对于服务器端/客户端,当一端结束连接时,会向另一端发送 FIN 消息;两端的在经过四次挥手过程后,其 Socket 不会马上消除,而是会处于一个 Time-wait 状态的阶段,此时 Socket 拥有的 端口号并没有得到释放 ,因此 不能使用相同的端口号 ;         只有先断开连接

    2024年02月09日
    浏览(43)
  • Unity | 渡鸦避难所-6 | 有限状态机控制角色行为逻辑

    有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型 在游戏开发中应用有限状态机,能够将复杂的行为逻辑分解为一组简单的状态和转换规则,每个状态都可以独立地处理其逻辑,使代码

    2024年01月16日
    浏览(44)
  • 【Linux网络编程】网络编程套接字二

    喜欢的点赞,收藏,关注一下把! TCP和UDP在编程接口上是非常像的,前面我们说过TCP是面向连接的,UDP我们上篇博客也写过了,我们发现UDP服务端客户端写好启动直接就发消息了没有建立连接。TCP是建立连接的,注定在写的时候肯定有写不一样的地方。具体怎么不一样,我们

    2024年04月15日
    浏览(64)
  • Linux网络编程——UDP编程

    1、UDP通信协议,服务器端和客户端无需建立连接,只需要知道对方套接字的地址信息就可以发送数据 2、UDP通信流程图: 功能:创建套接字并返回套接字描述符 功能:将套接字与IP地址和端口号绑定 功能:发送数据 功能:接收数据 功能:关闭套接字 1、代码功能:两个进程

    2023年04月19日
    浏览(46)
  • 【网络编程】Linux网络编程基础与实战第三弹——网络名词术语

    数据包从源地址到目的地址所经过的路径,由一系列路由节点组成。 某个路由节点为数据包选择投递方向的选路过程。 路由器工作原理 路由器是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号的设备。

    2024年02月08日
    浏览(46)
  • Linux系统应用编程(五)Linux网络编程(上篇)

    1.两个网络模型和常见协议 (1)OSI七层模型(物数网传会表应) 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层(自下到上) (2)TCP/IP四层模型(网网传应) 网络接口层(链路层)、网络层、传输层、应用层 (3)常见网络协议所属层 2.字节序 (1)两种

    2023年04月25日
    浏览(39)
  • Linux网络编程:网络基础

    文章目录: 一:协议   二:网络应用设计模式_BS模式和CS模式 三:网络分层模型(OSI七层 TCP/IP四层) 四:通信过程 五:协议格式  1.数据包封装 2.以太网帧格式和ARP数据报格式  3.IP段格式  4.UDP数据报格式 5.TCP数据报格式 六:TCP协议 1.TCP通信时序(面向连接的可靠数据通

    2024年02月12日
    浏览(83)
  • 【Linux网络编程】网络编程套接字(TCP服务器)

    作者:爱写代码的刚子 时间:2024.4.4 前言:本篇博客主要介绍TCP及其服务器编码 只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址 但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换 字符串转in

    2024年04月14日
    浏览(79)
  • linux【网络编程】之网络基础

    “协议” 是一种约定 软件设计方面的优势—低耦合 分层依据:功能比较集中,耦合度较高的模块—高内聚 每一层都要解决特定的问题 每一层都有自己匹配的协议,每一层协议都解决自己的问题 OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参

    2024年02月04日
    浏览(54)
  • Linux网络编程 网络基础知识

    目录 1.网络的历史和协议的分成 2.网络互联促成了TCP/IP协议的产生 3.网络的体系结构 4.TCP/IP协议族体系 5.网络各层的协议解释 6.网络的封包和拆包 7.网络预备知识      Internet-\\\"冷战\\\"的产物 1957年十月和十一月,前苏联先后欧两颗”Spuinik”卫星上天 1958年美国总统艾森豪威尔向

    2024年02月10日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包