目的:
一个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
- 请求头中每一行的后面都要加"\r\n"结尾;
- 第一行是状态行,分别是请求方法(GET)、请求路径(/signin?next=%2F)、协议版本(HTTP/2);
- 其余所有行均以XXX: XXXX的格式表示;
- 最后需要一个"\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
- 请求头中每一行的后面都要加"\r\n"结尾;
- 第一行是状态行,分别是请求方法(POST)、请求路径(/login)、协议版本(HTTP/1.1);
- 请求头内的剩余内容均以XXX: XXXX的格式表示;
- 请求头最后需要一个"\r\n"的空行作为结束的标志;
- 放在请求体内的请求参数以
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/([^ ]*)$ 匹配状态行
2、^([^:]*): ?(.*)$ 匹配请求头内的XXX: XXXX字段
3、(?!&)(.*?)=(.*?)(?=&|$) 匹配POST的请求参数
HttpRequest类结构 httprequest.h
#ifndef HTTPREQUEST_H
#define HTTPREQUEST_H
#include <unordered_set>
#include <unordered_map>
#include <string>
#include <regex>
#include <algorithm>
#include <memory>
#include <mysql/mysql.h>
#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<string,string> header;//存储请求头字段
std::unordered_map<string,string> post; //存储POST请求参数
static const std::unordered_set<string> DEFAULT_HTML;
static const std::unordered_map<string,int> DEFAULT_HTML_TAG;
};
#endif // !HTTPREQUEST_H
HttpRequest类实现 httprequest.cpp
#include "httprequest.h"
const std::unordered_set<string> HttpRequest::DEFAULT_HTML=
{"/home","/register","/login","/video","/picture"};
const std::unordered_map<string,int> HttpRequest::DEFAULT_HTML_TAG=
{{"/register.html", 0},{"/login.html", 1}};
HttpRequest::HttpRequest():state(REQUEST_LINE)
{
Log::getInstance()->init();
}
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<const char*>(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);//解析完一行请求头(由"\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];
LOG_INFO("%s: %s",match[1].str().c_str(),match[2].str().c_str());
}
else
{
state=BODY;
}
}
void HttpRequest::parseBody(const string& line)//解析请求体
{
body=line;
parsePost();//解析POST请求的请求体
state=FINISH;
}
void HttpRequest::parsePost()//解析POST请求的请求体
{
if(method=="POST"&&header["Content-Type"]=="application/x-www-form-urlencoded")
{
parseUrlencoded();//解析POST请求的请求参数
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()//解析POST请求的请求参数
{
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;
LOG_INFO("%s=%s",match[1].str().c_str(),match[2].str().c_str());
}
}
bool HttpRequest::userVertify(const string& username,const string& password,int tag)
{
SqlConnPool* pool = SqlConnPool::getInstance();
std::shared_ptr<SqlConn> 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;
}
}
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");
}
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//检查isKeepAlive是否开启
{
if(header.count("Connection"))
{
return header.find("Connection")->second=="keep-alive";
}
return false;
}
测试程序 testHttpRequest.cpp
分别解析GET请求和POST请求,根据解析内容进行判断。
#include "httprequest.h"
#include <iostream>
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:"<<request.getMethod()<<endl;
cout<<"path:"<<request.getPath()<<endl;
cout<<"version:"<<request.getVersion()<<endl;
if(request.isKeepAlive())
cout<<"isKeepAlive"<<endl;
}
void testGet()
{
HttpRequest request;
Buffer input;
input.append("GET /signin?next=%2F HTTP/2\r\n"
"Host: www.zhihu.com\r\n"
"User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"
"Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\n"
"Accept-Encoding: gzip, deflate, br\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");
request.parse(input);
cout<<"method:"<<request.getMethod()<<endl;
cout<<"path:"<<request.getPath()<<endl;
cout<<"version:"<<request.getVersion()<<endl;
if(request.isKeepAlive())
cout<<"isKeepAlive"<<endl;
}
int main()
{
cout<<"POST------------------------------------------"<<endl;
testPost();
cout<<"GET-------------------------------------------"<<endl;
testGet();
}
运行结果:
由日志信息可以判断,对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 阻塞队列