文章目录
-
- 从零手写高并发服务器(九):HTTP协议支持------从TCP到应用层
- 一、HTTP协议快速回顾
-
- [1.1 HTTP请求格式](#1.1 HTTP请求格式)
- [1.2 HTTP响应格式](#1.2 HTTP响应格式)
- [1.3 解析思路](#1.3 解析思路)
- 二、工具函数Util
- 三、HttpRequest与HttpResponse类
-
- [3.1 HTTP请求解析状态](#3.1 HTTP请求解析状态)
- [3.2 HttpRequest类](#3.2 HttpRequest类)
- 3.3、HttpResponse类
- 四、HttpContext上下文类------HTTP解析状态机
-
- [4.1 HttpContext的作用](#4.1 HttpContext的作用)
- [4.2 HttpContext类实现](#4.2 HttpContext类实现)
- 五、HttpServer类------完整HTTP服务器
-
- [5.1 HttpServer的作用](#5.1 HttpServer的作用)
- [5.2 HttpServer类实现](#5.2 HttpServer类实现)
- 六、完整的HTTP服务器测试
-
- [6.1 创建静态资源](#6.1 创建静态资源)
- [6.2 编写HTTP服务器](#6.2 编写HTTP服务器)
- [6.3 编译运行](#6.3 编译运行)
- [6.4 测试](#6.4 测试)
- 七、提交代码
- 八、本篇总结
- 九、完整server.hpp类顺序总览
- 十、本篇总结
从零手写高并发服务器(九):HTTP协议支持------从TCP到应用层
💬 开篇:上一篇我们的TcpServer已经能跑了,但它只是一个TCP服务器,收到的是原始字节流。本篇我们在TcpServer之上实现HTTP协议解析,让服务器能处理真正的HTTP请求,返回网页内容。
👍 点赞、收藏与分享:这是从传输层到应用层的跨越,写完这篇你就有一个完整的HTTP服务器了。
🚀 循序渐进:HTTP协议回顾 → 正则表达式 → HttpRequest → HttpResponse → HttpContext → HttpServer → 测试。
一、HTTP协议快速回顾
1.1 HTTP请求格式
bash
GET /index.html HTTP/1.1\r\n ← 请求行
Host: www.example.com\r\n ← 请求头部
Connection: keep-alive\r\n
Content-Type: text/html\r\n
\r\n ← 空行(头部结束标志)
请求正文(GET请求通常没有正文) ← 正文
请求行三要素:
- 请求方法:GET / POST / PUT / DELETE ...
- 资源路径:/index.html?key=val
- 协议版本:HTTP/1.1
1.2 HTTP响应格式
bash
HTTP/1.1 200 OK\r\n ← 响应行
Content-Type: text/html\r\n ← 响应头部
Content-Length: 13\r\n
\r\n ← 空行
Hello, World! ← 响应正文
1.3 解析思路
HTTP请求的解析是逐步进行的,因为TCP是流式传输,数据可能分多次到达:
bash
解析状态机:
RECV_HTTP_LINE → 解析请求行
↓ 成功
RECV_HTTP_HEAD → 解析头部(一行一行解析)
↓ 头部解析完毕
RECV_HTTP_BODY → 接收正文(根据Content-Length)
↓ 正文接收完毕
RECV_HTTP_OVER → 解析完成,可以处理请求了
二、工具函数Util
在实现HTTP模块之前,先准备一些工具函数。将以下代码添加到 server.hpp 的最后(在 #endif 之前):
cpp
// ==================== HTTP工具函数 ====================
class Util {
public:
// 字符串分割
static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *arry) {
size_t offset = 0;
while (offset < src.size()) {
size_t pos = src.find(sep, offset);
if (pos == std::string::npos) {
if (offset == src.size()) break;
arry->push_back(src.substr(offset));
return arry->size();
}
if (pos == offset) {
offset = pos + sep.size();
continue;
}
arry->push_back(src.substr(offset, pos - offset));
offset = pos + sep.size();
}
return arry->size();
}
// 读取文件内容
static bool ReadFile(const std::string &filename, std::string *buf) {
std::ifstream ifs(filename, std::ios::binary);
if (ifs.is_open() == false) {
ERR_LOG("OPEN %s FILE FAILED!", filename.c_str());
return false;
}
size_t fsize = 0;
ifs.seekg(0, ifs.end);
fsize = ifs.tellg();
ifs.seekg(0, ifs.beg);
buf->resize(fsize);
ifs.read(&(*buf)[0], fsize);
if (ifs.good() == false) {
ERR_LOG("READ %s FILE FAILED!", filename.c_str());
ifs.close();
return false;
}
ifs.close();
return true;
}
// 写入文件
static bool WriteFile(const std::string &filename, const std::string &buf) {
std::ofstream ofs(filename, std::ios::binary | std::ios::trunc);
if (ofs.is_open() == false) {
ERR_LOG("OPEN %s FILE FAILED!", filename.c_str());
return false;
}
ofs.write(buf.c_str(), buf.size());
if (ofs.good() == false) {
ERR_LOG("WRITE %s FILE FAILED!", filename.c_str());
ofs.close();
return false;
}
ofs.close();
return true;
}
// URL编码:将特殊字符转为%HH格式
static std::string UrlEncode(const std::string &url, bool convert_space_to_plus) {
std::string res;
for (auto &c : url) {
if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c)) {
res += c;
continue;
}
if (c == ' ' && convert_space_to_plus == true) {
res += '+';
continue;
}
char tmp[4] = {0};
snprintf(tmp, 4, "%%%02X", c);
res += tmp;
}
return res;
}
// URL解码
static std::string UrlDecode(const std::string &url, bool convert_plus_to_space) {
std::string res;
for (int i = 0; i < (int)url.size(); i++) {
if (url[i] == '+' && convert_plus_to_space == true) {
res += ' ';
continue;
}
if (url[i] == '%' && (i + 2) < (int)url.size()) {
char v1 = HEXTOI(url[i + 1]);
char v2 = HEXTOI(url[i + 2]);
char v = v1 * 16 + v2;
res += v;
i += 2;
continue;
}
res += url[i];
}
return res;
}
// 获取HTTP状态码描述
static std::string StatuDesc(int statu) {
auto it = _statu_msg.find(statu);
if (it != _statu_msg.end()) {
return it->second;
}
return "Unknow";
}
// 根据文件后缀获取mime类型
static std::string ExtMime(const std::string &filename) {
size_t pos = filename.find_last_of('.');
if (pos == std::string::npos) {
return "application/octet-stream";
}
std::string ext = filename.substr(pos);
auto it = _mime_msg.find(ext);
if (it == _mime_msg.end()) {
return "application/octet-stream";
}
return it->second;
}
// 判断一个文件是否是目录
static bool IsDirectory(const std::string &filename) {
struct stat st;
int ret = stat(filename.c_str(), &st);
if (ret < 0) {
return false;
}
return S_ISDIR(st.st_mode);
}
// 判断一个文件是否是普通文件
static bool IsRegular(const std::string &filename) {
struct stat st;
int ret = stat(filename.c_str(), &st);
if (ret < 0) {
return false;
}
return S_ISREG(st.st_mode);
}
// 判断请求的资源路径是否有效(不能包含..等路径穿越)
static bool ValidPath(const std::string &path) {
std::vector<std::string> subdir;
Split(path, "/", &subdir);
int level = 0;
for (auto &dir : subdir) {
if (dir == "..") {
level--;
if (level < 0) return false;
} else {
level++;
}
}
return true;
}
private:
static char HEXTOI(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'z') return c - 'a' + 10;
if (c >= 'A' && c <= 'Z') return c - 'A' + 10;
return 0;
}
static std::unordered_map<int, std::string> _statu_msg;
static std::unordered_map<std::string, std::string> _mime_msg;
};
// 静态成员初始化
std::unordered_map<int, std::string> Util::_statu_msg = {
{100, "Continue"},
{101, "Switching Protocol"},
{200, "OK"},
{201, "Created"},
{204, "No Content"},
{206, "Partial Content"},
{301, "Moved Permanently"},
{302, "Found"},
{303, "See Other"},
{304, "Not Modified"},
{307, "Temporary Redirect"},
{400, "Bad Request"},
{401, "Unauthorized"},
{403, "Forbidden"},
{404, "Not Found"},
{405, "Method Not Allowed"},
{406, "Not Acceptable"},
{408, "Request Timeout"},
{413, "Payload Too Large"},
{414, "URI Too Long"},
{415, "Unsupported Media Type"},
{500, "Internal Server Error"},
{502, "Bad Gateway"},
{503, "Service Unavailable"},
{504, "Gateway Timeout"},
{505, "HTTP Version Not Supported"}
};
std::unordered_map<std::string, std::string> Util::_mime_msg = {
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
{".js", "application/javascript"},
{".json", "application/json"},
{".xml", "application/xml"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".gif", "image/gif"},
{".ico", "image/x-icon"},
{".svg", "image/svg+xml"},
{".mp3", "audio/mpeg"},
{".mp4", "video/mp4"},
{".avi", "video/x-msvideo"},
{".txt", "text/plain"},
{".pdf", "application/pdf"},
{".doc", "application/msword"},
{".gz", "application/gzip"},
{".zip", "application/zip"},
{".tar", "application/x-tar"},
{".unknown", "application/octet-stream"}
};
三、HttpRequest与HttpResponse类
3.1 HTTP请求解析状态
cpp
// ==================== HTTP请求解析状态 ====================
typedef enum {
RECV_HTTP_ERROR,
RECV_HTTP_LINE,
RECV_HTTP_HEAD,
RECV_HTTP_BODY,
RECV_HTTP_OVER
} HttpRecvStatu;
3.2 HttpRequest类
cpp
// ==================== HttpRequest请求类 ====================
class HttpRequest {
public:
std::string _method;
std::string _path;
std::string _version;
std::string _body;
std::smatch _matches;
std::unordered_map<std::string, std::string> _headers;
std::unordered_map<std::string, std::string> _params;
public:
HttpRequest() : _version("HTTP/1.1") {}
void Reset() {
_method.clear();
_path.clear();
_version = "HTTP/1.1";
_body.clear();
std::smatch match;
_matches.swap(match);
_headers.clear();
_params.clear();
}
void SetHeader(const std::string &key, const std::string &val) {
_headers[key] = val;
}
bool HasHeader(const std::string &key) const {
return _headers.find(key) != _headers.end();
}
std::string GetHeader(const std::string &key) const {
auto it = _headers.find(key);
if (it == _headers.end()) return "";
return it->second;
}
void SetParam(const std::string &key, const std::string &val) {
_params[key] = val;
}
bool HasParam(const std::string &key) const {
return _params.find(key) != _params.end();
}
std::string GetParam(const std::string &key) const {
auto it = _params.find(key);
if (it == _params.end()) return "";
return it->second;
}
size_t ContentLength() const {
if (HasHeader("Content-Length") == false) return 0;
return std::stol(GetHeader("Content-Length"));
}
// 判断是否是短链接
bool Close() const {
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive") {
return false;
}
return true;
}
};
3.3、HttpResponse类
cpp
// ==================== HttpResponse响应类 ====================
class HttpResponse {
public:
int _statu;
bool _redirect_flag;
std::string _redirect_url;
std::string _body;
std::unordered_map<std::string, std::string> _headers;
public:
HttpResponse() : _redirect_flag(false), _statu(200) {}
HttpResponse(int statu) : _redirect_flag(false), _statu(statu) {}
void Reset() {
_statu = 200;
_redirect_flag = false;
_redirect_url.clear();
_body.clear();
_headers.clear();
}
void SetHeader(const std::string &key, const std::string &val) {
_headers[key] = val;
}
bool HasHeader(const std::string &key) {
return _headers.find(key) != _headers.end();
}
std::string GetHeader(const std::string &key) {
auto it = _headers.find(key);
if (it == _headers.end()) return "";
return it->second;
}
void SetContent(const std::string &body, const std::string &type = "text/html") {
_body = body;
SetHeader("Content-Type", type);
}
void SetRedirect(const std::string &url, int statu = 302) {
_statu = statu;
_redirect_flag = true;
_redirect_url = url;
}
// 判断是否是短链接
bool Close() {
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive") {
return false;
}
return true;
}
};
四、HttpContext上下文类------HTTP解析状态机
4.1 HttpContext的作用
HttpContext是HTTP请求的解析上下文,保存当前的解析状态和解析进度。每个Connection都有一个HttpContext,因为TCP是流式的,一次可能收不完一个完整的HTTP请求。
4.2 HttpContext类实现
cpp
// ==================== HttpContext上下文类 ====================
class HttpContext {
private:
int _resp_statu; // 解析出错时记录错误状态码
HttpRecvStatu _recv_statu; // 当前解析阶段
HttpRequest _request; // 已解析的请求信息
private:
// 解析请求行
bool ParseHttpLine(const std::string &line) {
std::smatch matches;
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
bool ret = std::regex_match(line, matches, e);
if (ret == false) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;
return false;
}
// matches[0]-整体 [1]-方法 [2]-路径 [3]-查询字符串 [4]-协议版本
_request._method = matches[1];
std::transform(_request._method.begin(), _request._method.end(),
_request._method.begin(), ::toupper);
_request._path = Util::UrlDecode(matches[2], false);
_request._version = matches[4];
// 解析查询字符串 key=val&key=val
std::string query_string = matches[3];
if (!query_string.empty()) {
std::vector<std::string> query_string_arry;
Util::Split(query_string, "&", &query_string_arry);
for (auto &str : query_string_arry) {
size_t pos = str.find("=");
if (pos == std::string::npos) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;
return false;
}
std::string key = Util::UrlDecode(str.substr(0, pos), true);
std::string val = Util::UrlDecode(str.substr(pos + 1), true);
_request.SetParam(key, val);
}
}
return true;
}
// 接收并解析请求行
bool RecvHttpLine(Buffer *buf) {
if (_recv_statu != RECV_HTTP_LINE) return false;
std::string line = buf->GetLineAndPop();
if (line.size() == 0) {
if (buf->ReadAbleSize() > 8192) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414;
return false;
}
return true; // 等待更多数据
}
if (line.size() > 8192) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414;
return false;
}
bool ret = ParseHttpLine(line);
if (ret == false) return false;
_recv_statu = RECV_HTTP_HEAD;
return true;
}
// 解析一行头部字段
bool ParseHttpHead(std::string &line) {
// 去掉末尾\r\n
if (line.back() == '\n') line.pop_back();
if (line.back() == '\r') line.pop_back();
size_t pos = line.find(": ");
if (pos == std::string::npos) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;
return false;
}
std::string key = line.substr(0, pos);
std::string val = line.substr(pos + 2);
_request.SetHeader(key, val);
return true;
}
// 接收并解析头部
bool RecvHttpHead(Buffer *buf) {
if (_recv_statu != RECV_HTTP_HEAD) return false;
while (true) {
std::string line = buf->GetLineAndPop();
if (line.size() == 0) {
if (buf->ReadAbleSize() > 8192) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 413;
return false;
}
return true; // 等待更多数据
}
// 空行表示头部结束
if (line == "\n" || line == "\r\n") {
break;
}
bool ret = ParseHttpHead(line);
if (ret == false) return false;
}
_recv_statu = RECV_HTTP_BODY;
return true;
}
// 接收正文
bool RecvHttpBody(Buffer *buf) {
if (_recv_statu != RECV_HTTP_BODY) return false;
size_t content_length = _request.ContentLength();
if (content_length == 0) {
_recv_statu = RECV_HTTP_OVER;
return true;
}
// 还需要接收多少字节
size_t real_len = content_length - _request._body.size();
if (buf->ReadAbleSize() >= real_len) {
_request._body.append(buf->ReadPosition(), real_len);
buf->MoveReadOffset(real_len);
_recv_statu = RECV_HTTP_OVER;
return true;
}
// 数据不够,先收下现有的
_request._body.append(buf->ReadPosition(), buf->ReadAbleSize());
buf->MoveReadOffset(buf->ReadAbleSize());
return true;
}
public:
HttpContext() : _resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}
void Reset() {
_resp_statu = 200;
_recv_statu = RECV_HTTP_LINE;
_request.Reset();
}
int RespStatu() { return _resp_statu; }
HttpRecvStatu RecvStatu() { return _recv_statu; }
HttpRequest &Request() { return _request; }
// 接收并解析HTTP请求
// switch没有break是故意的:一次调用尽量多地完成解析
void RecvHttpRequest(Buffer *buf) {
switch (_recv_statu) {
case RECV_HTTP_LINE: RecvHttpLine(buf);
case RECV_HTTP_HEAD: RecvHttpHead(buf);
case RECV_HTTP_BODY: RecvHttpBody(buf);
default: break;
}
}
};
五、HttpServer类------完整HTTP服务器
5.1 HttpServer的作用
HttpServer在TcpServer之上封装HTTP协议处理逻辑:
bash
HttpServer的结构:
┌──────────────────────────────────────────┐
│ HttpServer │
│ │
│ TcpServer _server → 底层TCP服务器 │
│ 路由表: │
│ GET /index → handler1 │
│ POST /login → handler2 │
│ GET /.* → 静态资源处理 │
│ │
│ 流程: │
│ 1. 收到数据 → HttpContext解析 │
│ 2. 解析完成 → 路由匹配 │
│ 3. 调用handler → 生成HttpResponse │
│ 4. 组装响应 → Send发送 │
└──────────────────────────────────────────┘
5.2 HttpServer类实现
cpp
// ==================== HttpServer ====================
#define DEFAULT_TIMEOUT 10
class HttpServer {
private:
using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;
using Handlers = std::vector<std::pair<std::regex, Handler>>;
Handlers _get_route;
Handlers _post_route;
Handlers _put_route;
Handlers _delete_route;
std::string _basedir; // 静态资源根目录
TcpServer _server;
private:
void ErrorHandler(const HttpRequest &req, HttpResponse *rsp) {
std::string body;
body += "<html>";
body += "<head>";
body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";
body += "</head>";
body += "<body>";
body += "<h1>";
body += std::to_string(rsp->_statu);
body += " ";
body += Util::StatuDesc(rsp->_statu);
body += "</h1>";
body += "</body>";
body += "</html>";
rsp->SetContent(body, "text/html");
}
// 将HttpResponse按照HTTP协议格式组装并发送
void WriteResponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp) {
// 1. 补全头部字段
if (req.Close() == true) {
rsp.SetHeader("Connection", "close");
} else {
rsp.SetHeader("Connection", "keep-alive");
}
if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false) {
rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
}
if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false) {
rsp.SetHeader("Content-Type", "application/octet-stream");
}
if (rsp._redirect_flag == true) {
rsp.SetHeader("Location", rsp._redirect_url);
}
// 2. 组装响应字符串
std::stringstream rsp_str;
rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "\r\n";
for (auto &head : rsp._headers) {
rsp_str << head.first << ": " << head.second << "\r\n";
}
rsp_str << "\r\n";
rsp_str << rsp._body;
// 3. 发送(只调用一次str()避免性能问题)
std::string rsp_buf = rsp_str.str();
conn->Send(rsp_buf.c_str(), rsp_buf.size());
}
// 判断是否是静态资源请求
bool IsFileRequest(const HttpRequest &req) {
if (_basedir.empty()) return false;
if (req._method != "GET" && req._method != "HEAD") return false;
if (Util::ValidPath(req._path) == false) return false;
std::string req_path = _basedir + req._path;
if (req_path.back() == '/') req_path += "index.html";
if (Util::IsRegular(req_path) == false) return false;
return true;
}
// 静态资源请求的处理
void FileHandler(const HttpRequest &req, HttpResponse *rsp) {
std::string req_path = _basedir + req._path;
if (req_path.back() == '/') req_path += "index.html";
bool ret = Util::ReadFile(req_path, &rsp->_body);
if (ret == false) return;
rsp->SetHeader("Content-Type", Util::ExtMime(req_path));
}
// 功能性请求的路由分发
void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers) {
for (auto &handler : handlers) {
const std::regex &re = handler.first;
const Handler &functor = handler.second;
bool ret = std::regex_match(req._path, req._matches, re);
if (ret == false) continue;
return functor(req, rsp);
}
rsp->_statu = 404;
}
// 路由查找+处理
void Route(HttpRequest &req, HttpResponse *rsp) {
if (IsFileRequest(req)) {
return FileHandler(req, rsp);
}
if (req._method == "GET" || req._method == "HEAD") {
return Dispatcher(req, rsp, _get_route);
} else if (req._method == "POST") {
return Dispatcher(req, rsp, _post_route);
} else if (req._method == "PUT") {
return Dispatcher(req, rsp, _put_route);
} else if (req._method == "DELETE") {
return Dispatcher(req, rsp, _delete_route);
}
rsp->_statu = 405;
}
// 连接建立成功后的回调
void OnConnected(const PtrConnection &conn) {
conn->SetContext(HttpContext());
DBG_LOG("NEW CONNECTION %p", conn.get());
}
// 收到新数据后的回调
void OnMessage(const PtrConnection &conn, Buffer *buffer) {
while (buffer->ReadAbleSize() > 0) {
// 1. 获取上下文
HttpContext *context = conn->GetContext()->get<HttpContext>();
// 2. 解析数据
context->RecvHttpRequest(buffer);
HttpRequest &req = context->Request();
HttpResponse rsp(context->RespStatu());
// 3. 解析出错,返回错误响应并关闭连接
if (context->RespStatu() >= 400) {
ErrorHandler(req, &rsp);
WriteResponse(conn, req, rsp);
context->Reset();
conn->Shutdown();
return;
}
// 4. 还没解析完,等待更多数据
if (context->RecvStatu() != RECV_HTTP_OVER) {
return;
}
// 5. 解析完毕,路由处理
Route(req, &rsp);
// 6. 发送响应
WriteResponse(conn, req, rsp);
// 7. 重置上下文(keep-alive连接复用)
context->Reset();
// 8. 短链接直接关闭
if (rsp.Close() == true) {
conn->Shutdown();
}
}
}
public:
HttpServer(int port, int timeout = DEFAULT_TIMEOUT) : _server(port) {
_server.EnableInactiveRelease(timeout);
_server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));
_server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
}
void SetThreadCount(int count) { _server.SetThreadCount(count); }
void SetBaseDir(const std::string &path) {
assert(Util::IsDirectory(path) == true);
_basedir = path;
}
void Get(const std::string &pattern, const Handler &handler) {
_get_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void Post(const std::string &pattern, const Handler &handler) {
_post_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void Put(const std::string &pattern, const Handler &handler) {
_put_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void Delete(const std::string &pattern, const Handler &handler) {
_delete_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void SetTimerTask(const Functor &task, int delay) {
_server.RunAfter(task, delay);
}
void Listen() {
_server.Start();
}
};
六、完整的HTTP服务器测试
6.1 创建静态资源
bash
cd ~/TcpServer
mkdir -p wwwroot
vim wwwroot/index.html
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Http Server</title>
</head>
<body>
<h1>Hello! 这是我的HTTP服务器!</h1>
<p>如果你看到这个页面,说明服务器运行成功了。</p>
</body>
</html>
6.2 编写HTTP服务器
bash
cd ~/TcpServer/test
vim http_server.cpp
cpp
#include "../source/server.hpp"
int main() {
HttpServer server(8500);
server.SetThreadCount(3);
server.SetBaseDir("../wwwroot"); // 静态资源根目录(相对于test目录)
// 注册GET路由:/hello
server.Get("/hello", [](const HttpRequest &req, HttpResponse *rsp) {
rsp->SetContent("<html><body><h1>Hello World!</h1></body></html>", "text/html");
});
// 注册GET路由:/say/xxx,使用正则捕获
server.Get("/say/(.*)", [](const HttpRequest &req, HttpResponse *rsp) {
std::string msg = req._matches[1];
std::string body = "<html><body><h1>You said: " + msg + "</h1></body></html>";
rsp->SetContent(body, "text/html");
});
// 注册POST路由:/login
server.Post("/login", [](const HttpRequest &req, HttpResponse *rsp) {
std::string body = "<html><body><h1>Login OK!</h1>";
body += "<p>Body: " + req._body + "</p>";
body += "</body></html>";
rsp->SetContent(body, "text/html");
});
server.Listen();
return 0;
}
6.3 编译运行
bash
cd ~/TcpServer/test
g++ -std=c++17 http_server.cpp -o http_server -lpthread
./http_server
6.4 测试
bash
# 测试静态资源
curl http://127.0.0.1:8500/
# 测试/hello路由
curl http://127.0.0.1:8500/hello
# 测试正则路由
curl http://127.0.0.1:8500/say/nihao
# 测试POST
curl -X POST -d "username=test&password=123" http://127.0.0.1:8500/login
预期输出:
bash
wsh@VM-16-2-ubuntu:~/TcpServer/test$ curl http://127.0.0.1:8500/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Http Server</title>
</head>
<body>
<h1>Hello! 这是我的HTTP服务器!</h1>
<p>如果你看到这个页面,说明服务器运行成功了。</p>
</body>
</html>
wsh@VM-16-2-ubuntu:~/TcpServer/test$ curl http://127.0.0.1:8500/hello
<html><body><h1>Hello World!</h1></body></html>
wsh@VM-16-2-ubuntu:~/TcpServer/test$ curl http://127.0.0.1:8500/say/nihao
<html><body><h1>You said: nihao</h1></body></html>
wsh@VM-16-2-ubuntu:~/TcpServer/test$ curl -X POST -d "username=test&password=123" http://127.0.0.1:8500/login
<html><body><h1>Login OK!</h1><p>Body: username=test&password=123</p></body></html>
全部通过!🎉
七、提交代码
bash
cd ~/TcpServer
git add .
git commit -m "实现HTTP协议支持:Util、HttpContext、HttpServer"
git push
八、本篇总结
| 模块 | 功能 |
|---|---|
| Util | 工具函数:字符串分割、文件读写、URL编解码、MIME类型 |
| HttpContext | HTTP请求解析状态机,处理粘包分包 |
| HttpServer | 在TcpServer之上封装HTTP协议,支持路由和静态资源 |
核心是 HttpContext 的状态机设计:
bash
RECV_HTTP_LINE → 解析请求行(方法、路径、协议版本、查询字符串)
↓
RECV_HTTP_HEAD → 解析头部(一行一行读)
↓
RECV_HTTP_BODY → 根据Content-Length接收正文
↓
RECV_HTTP_OVER → 解析完成,业务处理
关键特性:
- ✅ 完整的HTTP请求解析(支持查询字符串、POST正文)
- ✅ 路由分发(GET、POST、PUT、DELETE)
- ✅ 静态资源服务(自动MIME类型识别)
- ✅ Keep-Alive连接复用
- ✅ 错误处理与状态码返回
- ✅ URL编解码支持
至此,一个完整的HTTP服务器已经实现!
九、完整server.hpp类顺序总览
到目前为止,server.hpp 中所有类的完整顺序:
cpp
#ifndef __SERVER_HPP__
#define __SERVER_HPP__
// ===== 头文件 =====
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <functional>
#include <memory>
#include <cassert>
#include <cstring>
#include <ctime>
#include <typeinfo>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <regex>
#include <sstream>
#include <fstream>
#include <algorithm>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/timerfd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// ===== 1. 日志宏 =====
// ===== 2. Buffer类(含FindCRLF / GetLineAndPop)=====
// ===== 3. Any类 =====
// ===== 4. NetWork类(SIGPIPE处理)=====
// ===== 5. Socket类 =====
// ===== 6. 前向声明 class Poller; class EventLoop; =====
// ===== 7. Channel类(Update/Remove只声明)=====
// ===== 8. Poller类 =====
// ===== 9. TimerTask类 =====
// ===== 10. TimerWheel类(对外接口只声明)=====
// ===== 11. EventLoop类(含AssertInLoop / HasTimer)=====
// ===== 12. Channel::Update() / Remove() 实现 =====
// ===== 13. TimerWheel::TimerAdd/Refresh/Cancel 实现 =====
// ===== 14. LoopThread类 =====
// ===== 15. LoopThreadPool类 =====
// ===== 16. ConnStatu枚举 =====
// ===== 17. Connection类 =====
// ===== 18. Acceptor类 =====
// ===== 19. TcpServer类 =====
// ===== 20. Util工具类 + 静态成员初始化 =====
// ===== 21. HttpRecvStatu枚举 =====
// ===== 22. HttpRequest类 =====
// ===== 23. HttpResponse类 =====
// ===== 24. HttpContext类 =====
// ===== 25. HttpServer类 =====
#endif
十、本篇总结
| 模块 | 功能 |
|---|---|
| Util | 工具函数:字符串分割、文件读写、URL编解码、MIME类型 |
| HttpRequest | 保存解析后的HTTP请求信息 |
| HttpResponse | 组织HTTP响应数据 |
| HttpContext | HTTP请求解析状态机,处理粘包 |
| HttpServer | 在TcpServer之上封装HTTP协议,支持路由和静态资源 |
至此,整个项目的核心代码全部完成!我们实现了:
bash
底层 → 上层:
Buffer → Socket → Channel → Poller → EventLoop
→ TimerWheel → LoopThread → LoopThreadPool
→ Connection → Acceptor → TcpServer
→ HttpRequest/Response/Context → HttpServer
💬 下一篇预告:性能测试与优化!用压测工具对服务器进行并发测试,分析瓶颈,进行优化。然后是项目总结篇,回顾整个架构设计。