Web服务器解析HTTP请求

        web服务器接收客户端发送过来的HTTP请求,把接收到的信息进行行提取,获取请求行以及请求头部字段,利用主从状态机进行解析状态的转移,并根据解析结果返回HTTP状态码,完成整个解析过程。

        文末附带游双老师的《深入解析高性能服务器编程》的http请求代码,该实例用主状态机表示正在分析的状态,用从状态机表示行获取的状态,用HTTP_CODE表示服务器处理请求返回的状态码。

所有的状态以及状态码均使用枚举完成:

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

// 从状态机的三种状态,即行的读取状态,分别表示:读取到一个完整的行、行出错和行数据尚且不完整
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请求:

// 从状态机,用于解析出一行内容
LINE_STATUS parse_line( char* buffer, int& checked_index, int& read_index )

// 分析请求行 例如GET /56200338.jpg HTTP/1.1
HTTP_CODE parse_requestline( char* temp, CHECK_STATE& checkstate)

// 分析头部字段
HTTP_CODE parse_headers(char* temp)

// 分析HTTP请求的入口函数
HTTP_CODE parse_content(char* buffer, int& checked_index, CHECK_STATE& checkstate, int& read_index, int& start_line)

以下是对源码进行几点详细解释:

1、主状态机用来判断在分析请求行or分析请求头

2、从状态机用来读取行的状态分别表示获取一个完整行、行出错以及行的数据不完整

3、用 LINE_STATUS parse_line( char* buffer, int& checked_index, int& read_index ) 获取一行的内容提供给后续分析

4、用 HTTP_CODE parse_requestline( char* temp, CHECK_STATE& checkstate) 来分析请求行

5、用 HTTP_CODE parse_headers(char* temp) 分析头部字段

6、用 HTTP_CODE parse_content(char* buffer, int& checked_index, CHECK_STATE& checkstate, int& read_index, int& start_line) 来作为HTTP请求的入口函数 其主要流程是获取一行的内容,如果获取完整行,根据主状态机的状态判断是进行分析请求行还是分析头部字段;如果获取的非完整行或者行出错,根据linestatus返回响应的HTTP_CODE

7、recv客户端发来的信息并存到buffer中, data_read记录获取的字节数,read_index在原来的基础上继续往后更新data_read个位置,checked_index代表正在分析的字节在buffer的位置,并通过parse_line()更新checked_index,更新前start_line用来记录当前行的起始位置,用过checked_index来更新,因为每经过一次parse_line(),checked_index都会指向下一行的位置,这时可以把checked_index赋值给start_line,更新后start_line用来记录下一行的起始位置,所以需要用temp来记录更新前start_line指向的位置,通过temp就能确定一行(因为行末尾已经用'\0'分隔好了),进而进行分析。

书中源码如下:

#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请求的结果: 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","Someting 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;
}

// 分析请求行 例如GET /56200338.jpg HTTP/1.1
HTTP_CODE parse_requestline( char* temp, CHECK_STATE& checkstate)
{
    char* url = strpbrk( temp, " \t");
    // 如果请求行中没有空白字符或者"\t"字符,则HTTP请求必有问题(\t代表空8个字符)
    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;
    }
    // 检索字符串str1中第一个不在字符串str2中出现的字符下标
    url += strspn(url, " \t");
    char* version = strpbrk(url, " \t");
    if(!version){
        return BAD_REQUEST;
    }
    *version++ = '\0';
    version += strspn(version," \t");
    // 仅支持HTTP/1.1
    if( strncasecmp(url, "http://",7)==0){
        url += 7;
        // 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置
        url = strchr(url, '/');
    }
    if( !url || url[0] != '/'){
        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请求结果
    HTTP_CODE retcode = NO_REQUEST;
    // 主状态机,用于从buffer中取出所有完整行
    while((linestatus = parse_line(buffer,checked_index,read_index))==LINE_OK)
    {
        // start_line是行在buffer中的起始位置
        char* temp = buffer + start_line;
        // 记录下一行的起始位置
        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* argv[])
{
    if(argc<=2)
    {
        printf("usage: %s ip_address prot_number\n",basename(argv[0]));
        return 1;
    }

    // 从输入参数获取服务器的ip与监听端口
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    // 用address结构体来存放服务器的信息,包括协议,ip,端口
    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);

    // 用client_address来存放客户端的信息
    struct sockaddr_in client_address;
    socklen_t client_addrlength = sizeof(client_address);
    // 利用accept函数把客户端的信息存进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;
        // 行在buffer中的起始位置
        int start_line = 0;

        // 设置主状态机的初始状态
        CHECK_STATE checkstate = CHECK_STATE_REQUESTLINE;
        // 循环读取客户数据并分析
        while (1)
        {
            // int recv( SOCKET s, char *buf, int len, int flags) 返回实际读取的字节数
            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);
            // 尚未得到一个完整的HTTP请求
            if(result==NO_REQUEST)
            {
                continue;
            }
            // 得到一个完整的、正确的HTTP请求
            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;
}

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

czy1219

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值