解析HTTP请求报文(GET、POST)
作者:mmseoamin日期:2024-01-18

目的:

一个WEB服务器需要解析客户端(浏览器)发来的请求,两种常见的请求方式是GET和POST。

GET的请求格式:

  • GET请求没有请求体只有请求头
  • GET请求的请求参数放在URL后加上一个"?"的后面,参数以key=value的形式传递,参数与参数之间使用"&"进行连接。
    GET /signin?next=%2F HTTP/2\r\n
    Host: www.zhihu.com\r\n
    User-Agent: Mozilla/5.0\r\n
    Accept: */*\r\n
    Accept-Language: zh-CN\r\n
    Accept-Encoding: gzip, deflate\r\n
    Connection: keep-alive\r\n
    Upgrade-Insecure-Requests: 1\r\n
    Cache-Control: max-age=0\r\n
    TE: trailers\r\n
    \r\n
    1. 请求头中每一行的后面都要加"\r\n"结尾;
    2. 第一行是状态行,分别是请求方法(GET)、请求路径(/signin?next=%2F)、协议版本(HTTP/2);
    3. 其余所有行均以XXX: XXXX的格式表示;
    4. 最后需要一个"\r\n"的空行作为请求头结束的标志。

    POST的请求格式:

    • POST请求传送的数据放在请求体中;
    • POST请求的请求参数放在请求体中,由请求头中的"Content-Type"字段决定其格式;
    • 如果是"Content-Type: application/x-www-form-urlencoded",则请求参数以key=value的形式传递,参数与参数之间使用"&"进行连接
    • 如果是"Content-Type: multipart/form-data",则使用boundary(分割线)充当参数与参数之间的连接(相当于&)
      POST /login HTTP/1.1\r\n
      Host: 127.0.0.1:8888\r\n
      User-Agent: Mozilla/5.0\r\n
      Accept: */*\r\n
      Accept-Language: zh-CN\r\n
      Accept-Encoding: gzip, deflate\r\n
      Content-Type: application/x-www-form-urlencoded\r\n
      Content-Length: 29\r\n
      Connection: keep-alive\r\n
      \r\n
      username=root&password=123456
      1. 请求头中每一行的后面都要加"\r\n"结尾;
      2. 第一行是状态行,分别是请求方法(POST)、请求路径(/login)、协议版本(HTTP/1.1);
      3. 请求头内的剩余内容均以XXX: XXXX的格式表示;
      4. 请求头最后需要一个"\r\n"的空行作为结束的标志;
      5. 放在请求体内的请求参数以key=value的形式传递,参数与参数之间使用"&"进行连接。

      功能介绍: 

      使用状态机和正则表达式完成了对HTTP请求报文的解析,支持解析GET报文和POST报文(仅限Content-Type: application/x-www-form-urlencoded)。

      由于计划完成的web服务器需要实现展示主页(GET)、用户登录(POST)、用户注册(POST)、获取图片(GET)、获取视频(GET)五个功能,所以web服务器的请求解析模块满足:

      若为GET请求,可根据状态行信息,完成对请求内容地址的转换,以及请求头内其他内容的提取。

      若为POST请求,可根据请求参数,完成登录和注册这两个功能(登录:根据后台数据库表中的信息判断用户名与密码是否正确;注册:向后台数据库表中插入符合条件的新用户名和密码)。

      状态机流程:

      enum PARSE_STATE
          {
              REQUEST_LINE,
              HEADERS,
              BODY,
              FINISH
          };

      如果为GET请求: REQUEST_LINE——>HEADERS——>FINISH;

      如果为POST请求:REQUEST_LINE——>HEADERS——>BODY——>FINISH。

       

      用到的正则表达式:

       1、^([^ ]*) ([^ ]*) HTTP/([^ ]*)$        匹配状态行

      解析HTTP请求报文(GET、POST),第1张

       2、^([^:]*): ?(.*)$        匹配请求头内的XXX: XXXX字段

       解析HTTP请求报文(GET、POST),第2张

       3、(?!&)(.*?)=(.*?)(?=&|$)        匹配POST的请求参数

       解析HTTP请求报文(GET、POST),第3张

      HttpRequest类结构        httprequest.h

      #ifndef HTTPREQUEST_H
      #define HTTPREQUEST_H
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include "buffer.h"
      #include "log.h"
      #include "sqlconnpool.h"
      using std::string;
      class HttpRequest
      {
      public:
          enum PARSE_STATE//解析流程的状态
          {
              REQUEST_LINE,
              HEADERS,
              BODY,
              FINISH
          };
          HttpRequest();
          ~HttpRequest()=default;
          bool parse(Buffer& buffer);//解析全过程
          const string& getMethod() const;
          const string& getPath() const;
          const string& getVersion() const;
          bool isKeepAlive() const;
      private:
          void parseRequestLine(const string& line);//解析状态行
          void parseHeader(const string& line);//解析请求头
          void parseBody(const string& line);//解析请求体
          void parsePath();//解析请求路径
          void parsePost();//解析POST请求
          void parseUrlencoded();//解析POST请求的请求参数
          bool userVertify(const string& username,const string& password,int tag);//身份验证
          PARSE_STATE state;
          string method;
          string path;
          string version;
          string body;
          std::unordered_map header;//存储请求头字段
          std::unordered_map post; //存储POST请求参数
          static const std::unordered_set DEFAULT_HTML;
          static const std::unordered_map DEFAULT_HTML_TAG;
      };
      #endif // !HTTPREQUEST_H

       HttpRequest类实现       httprequest.cpp

      #include "httprequest.h"
      const std::unordered_set HttpRequest::DEFAULT_HTML=
      {"/home","/register","/login","/video","/picture"};
      const std::unordered_map HttpRequest::DEFAULT_HTML_TAG=
      {{"/register.html", 0},{"/login.html", 1}};
      HttpRequest::HttpRequest()
      {
          Log::getInstance()->init();
          init();
      }
      void HttpRequest::init()
      {
          method="";
          path="";
          version="";
          body="";
          state = REQUEST_LINE;
          header.clear();
          post.clear();
      }
      bool HttpRequest::parse(Buffer& buffer)
      {
          if(buffer.readableBytes()<=0)
              return false;
          while(buffer.readableBytes()&&state!=FINISH)
          {
              const char CRLF[3]="\r\n";
              const char* lineEnd=std::search(buffer.peek(),static_cast(buffer.beginWrite()),CRLF,CRLF+2);
              string line(buffer.peek(),lineEnd);
              switch (state)
              {
              case REQUEST_LINE:
                  parseRequestLine(line);
                  parsePath();
                  break;
              case HEADERS:
                  parseHeader(line);
                  break;
              case BODY:
                  parseBody(line);
                  break;
              default:
                  break;
              }
              if(lineEnd==buffer.beginWrite())//解析完请求体(不由"\r\n"结尾)
                  break;
              buffer.retrieveUntil(lineEnd+2);//解析完一行Headers(由"\r\n"结尾)
          }
          return true;
      }
      void HttpRequest::parsePath()
      {
          if(path=="/") 
              path="/home.html";
          else
              if(DEFAULT_HTML.count(path))
                  path+=".html";
      }
      void HttpRequest::parseRequestLine(const string& line)
      {
          std::regex patten("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$");
          std::smatch match;
          if(!std::regex_match(line,match,patten))
          {
              LOG_ERROR("%s","Parse RequestLine Error");
          }
          method=match[1];
          path=match[2];
          version=match[3];
          state=HEADERS;
      }
      void HttpRequest::parseHeader(const string& line)
      {
          std::regex patten("^([^:]*): ?(.*)$");
          std::smatch match;
          if(std::regex_match(line,match,patten))
          {
              header[match[1]]=match[2];
          }        
          else
          {
              state=BODY;
          }
      }
      void HttpRequest::parseBody(const string& line)
      {
          body=line;
          parsePost();
          state=FINISH;
      }
      void HttpRequest::parsePost()
      {
          if(method=="POST"&&header["Content-Type"]=="application/x-www-form-urlencoded")
          {
              parseUrlencoded();
              if(DEFAULT_HTML_TAG.count(path))
              {
                  int tag=DEFAULT_HTML_TAG.find(path)->second;
                  if(userVertify(post["username"],post["password"],tag))
                  {
                      path="/home.html";
                  }
                  else
                  {
                      path="/error.html";
                  }
              }
          }
      }
      void HttpRequest::parseUrlencoded()
      {
      	std::regex patten("(?!&)(.*?)=(.*?)(?=&|$)");
          std::smatch match;
          string::const_iterator begin=body.begin();
          string::const_iterator end=body.end();
          while(std::regex_search(begin,end,match,patten))
          {
              post[match[1]]=match[2];
              begin=match[0].second;
          }
      }
      bool HttpRequest::userVertify(const string& username,const string& password,int tag)
      {
          SqlConnPool* pool = SqlConnPool::getInstance();
          std::shared_ptr conn=pool->getConn();
          string order1="SELECT username,password FROM user WHERE username='"+username+"' LIMIT 1";
          string order2="INSERT INTO user(username, password) VALUES('"+username+"','"+password+"')";
          MYSQL_RES* res=conn->query(order1);
          string user;
          string pwd;
          MYSQL_ROW row=nullptr;
          while((row=mysql_fetch_row(res))!=nullptr) 
          {
              user=row[0];
              pwd=row[1];
          }
          if(tag)//登录
          {
              if(pwd!=password)//密码错误
              {
                  LOG_ERROR("%s","Password Error");
                  return false;
              }
              LOG_INFO("%s Login Success",username);
          }
          else//注册
          {
              if(!user.empty())//用户名已被使用
              {
                  LOG_ERROR("%s","Username Used");
                  return false;
              }
              if(!conn->update(order2))//数据库插入失败
              {
                  LOG_ERROR("%s","Insert Error");
                  return false;
              }
              LOG_INFO("%s Register Success",username);
          }
          mysql_free_result(res);
          return true;
      }
      const string& HttpRequest::getMethod() const
      {
          return method;
      }
      const string& HttpRequest::getPath() const
      {
          return path;
      }
      const string& HttpRequest::getVersion() const
      {
          return version;
      }
      bool HttpRequest::isKeepAlive() const
      {
          if(header.count("Connection"))
          {
              return header.find("Connection")->second=="keep-alive";
          }
          return false;
      }

       测试程序        testHttpRequest.cpp

      分别解析GET请求和POST请求,根据解析内容进行判断。

      #include "httprequest.h"
      #include 
      using namespace std;
      void testPost()
      {
          HttpRequest request;
          Buffer input;
          input.append("POST /login HTTP/1.1\r\n"
                  "Host: 127.0.0.1:8888\r\n"
                  "User-Agent: Mozilla/5.0\r\n" 
                  "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n"
                  "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n"
                  "Accept-Encoding: gzip, deflate\r\n"
                  "Content-Type: application/x-www-form-urlencoded\r\n"
                  "Content-Length: 29\r\n"
                  "Connection: keep-alive\r\n"
                  "\r\n"
                  "username=root&password=123456");
          request.parse(input);
          cout<<"method:"< 
      

      运行结果:

      解析HTTP请求报文(GET、POST),第4张

      解析HTTP请求报文(GET、POST),第5张

       由日志信息可以判断,对GET和POST的请求解析正确。

      附:

       Makefile

      CXX = g++
      CFLAGS = -std=c++14 -O2 -Wall -g 
      TARGET = testHttpRequest
      OBJS =  buffer.cpp log.cpp blockqueue.h\
      		sqlconn.cpp sqlconnpool.cpp httprequest.cpp\
              testHttpRequest.cpp
      all: $(OBJS)
      	$(CXX) $(CFLAGS) $(OBJS) -o $(TARGET)  -pthread -L/usr/lib64/mysql -lmysqlclient
      clean:
      	rm -rf $(OBJS) $(TARGET)

      数据库连接池(C++11实现)_{(sunburst)}的博客-CSDN博客

      同步+异步日志系统(C++实现)_{(sunburst)}的博客-CSDN博客_c++ 异步日志

      缓冲区Buffer类的设计(参考Muduo实现)_{(sunburst)}的博客-CSDN博客

      基于C++11实现的阻塞队列(BlockQueue)_{(sunburst)}的博客-CSDN博客_c++11 阻塞队列