目录
[1. 架构设计](#1. 架构设计)
[2. 功能特点](#2. 功能特点)
[3. 主要流程](#3. 主要流程)
[4. 待改进点](#4. 待改进点)
[1. 常量和头文件](#1. 常量和头文件)
[2. HttpRequest类 - 请求解析](#2. HttpRequest类 - 请求解析)
[3. HttpResponse类 - 响应构建](#3. HttpResponse类 - 响应构建)
[4. Http类 - 主服务器](#4. Http类 - 主服务器)
[5. 请求处理流程](#5. 请求处理流程)
[1. 静态文件服务](#1. 静态文件服务)
[2. 动态请求支持](#2. 动态请求支持)
[3. 特殊处理](#3. 特殊处理)
[1. 类设计特点](#1. 类设计特点)
[2. 方法详细说明](#2. 方法详细说明)
[3. 设计模式](#3. 设计模式)
[4. 使用示例](#4. 使用示例)
[5. 改进建议](#5. 改进建议)
[1. 处理函数设计模式](#1. 处理函数设计模式)
[2. 路由注册机制](#2. 路由注册机制)
[3. 请求处理流程](#3. 请求处理流程)
[1. 查询参数处理](#1. 查询参数处理)
[2. HTTP响应构建](#2. HTTP响应构建)
[3. 日志记录](#3. 日志记录)
一、Http.hpp
模块1:头文件和命名空间
cpp
// 防止头文件重复包含
#pragma once
// 包含自定义网络库的头文件
#include "Socket.hpp" // Socket相关类
#include "TcpServer.hpp" // TCP服务器类
#include "Util.hpp" // 工具函数
#include "Log.hpp" // 日志模块
// 包含标准库头文件
#include <iostream> // 输入输出流
#include <string> // 字符串类
#include <memory> // 智能指针
#include <sstream> // 字符串流
#include <functional> // 函数对象
#include <unordered_map> // 哈希表
// 使用命名空间
using namespace SocketModule; // Socket模块命名空间
using namespace LogModule; // 日志模块命名空间
模块2:全局常量定义
cpp
// HTTP协议相关的常量定义
const std::string gspace = " "; // 空格
const std::string glinespace = "\r\n"; // 行分隔符(回车换行)
const std::string glinesep = ": "; // 头部键值分隔符
// 服务器文件路径相关常量
const std::string webroot = "./wwwroot"; // 网站根目录
const std::string homepage = "index.html"; // 默认首页文件名
const std::string page_404 = "/404.html"; // 404错误页面路径
模块3:HttpRequest类(HTTP请求解析)
cpp
// HTTP请求类,用于解析客户端发来的HTTP请求
class HttpRequest
{
public:
// 构造函数,初始化_is_interact为false(默认不是交互请求)
HttpRequest() : _is_interact(false)
{
}
// 序列化方法(当前未实现,返回空字符串)
std::string Serialize()
{
return std::string();
}
// 解析请求行:例如 "GET / HTTP/1.1"
void ParseReqLine(std::string &reqline)
{
// GET / HTTP/1.1
std::stringstream ss(reqline); // 创建字符串流
ss >> _method >> _uri >> _version; // 按空格分割,分别存入_method、_uri、_version
}
// 反序列化:将HTTP请求字符串解析为HttpRequest对象
// reqstr是一个完整的http request string
bool Deserialize(std::string &reqstr)
{
// 1. 提取请求行
std::string reqline; // 用于存储请求行
// 使用Util工具读取一行(以glinespace为分隔符)
bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);
// 记录调试日志
LOG(LogLevel::DEBUG) << reqline;
// 2. 对请求行进行反序列化
ParseReqLine(reqline); // 调用上面方法解析请求行
// 处理URI:如果请求根目录,添加默认首页
if (_uri == "/")
_uri = webroot + _uri + homepage; // 转换为 ./wwwroot/index.html
else
_uri = webroot + _uri; // 转换为 ./wwwroot/a/b/c.html
// 记录解析结果到日志
LOG(LogLevel::DEBUG) << "_method: " << _method;
LOG(LogLevel::DEBUG) << "_uri: " << _uri;
LOG(LogLevel::DEBUG) << "_version: " << _version;
// 检查URI中是否包含查询参数(?后面的部分)
const std::string temp = "?"; // 查询参数分隔符
auto pos = _uri.find(temp); // 查找?的位置
if (pos == std::string::npos) // 如果没有找到?
{
return true; // 直接返回,没有查询参数
}
// 如果有查询参数,进行分割处理
// _uri示例: ./wwwroot/login?username=zhangsan&password=123456
_args = _uri.substr(pos + temp.size()); // 提取查询参数部分
_uri = _uri.substr(0, pos); // 提取URI部分(去掉查询参数)
_is_interact = true; // 标记为交互请求
// 最终_uri格式: ./wwwroot/XXX.YYY
return true;
}
// 获取URI的getter方法
std::string Uri()
{
return _uri;
}
// 判断是否为交互请求的getter方法
bool isInteract()
{
return _is_interact;
}
// 获取查询参数的getter方法
std::string Args()
{
return _args;
}
// 析构函数
~HttpRequest()
{
}
private:
// HTTP请求行部分
std::string _method; // 请求方法(GET/POST等)
std::string _uri; // 请求的资源路径
std::string _version; // HTTP协议版本
// HTTP头部字段(未完全实现)
std::unordered_map<std::string, std::string> _headers;
std::string _blankline; // 空行
std::string _text; // 请求体(如果有)
// 自定义字段
std::string _args; // 查询参数
bool _is_interact; // 是否为交互请求的标志
};
基本用法:
cpp
#include <iostream>
#include <string>
// 假设有相关的依赖文件
#include "httpreq.h"
// 全局变量定义(实际应该在合适的地方定义)
std::string webroot = "./wwwroot/";
std::string homepage = "index.html";
int main() {
// 创建一个HttpRequest对象
HttpRequest req;
// 准备一个HTTP请求字符串
std::string http_request =
"GET / HTTP/1.1\r\n"
"Host: localhost:8080\r\n"
"User-Agent: Mozilla/5.0\r\n"
"\r\n";
// 解析HTTP请求
bool success = req.Deserialize(http_request);
if (success) {
// 获取解析后的URI
std::cout << "请求URI: " << req.Uri() << std::endl;
// 检查是否是交互请求
if (req.isInteract()) {
std::cout << "这是交互请求" << std::endl;
std::cout << "查询参数: " << req.Args() << std::endl;
} else {
std::cout << "这是静态文件请求" << std::endl;
}
}
return 0;
}
模块4:HttpResponse类(HTTP响应构建)
cpp
// HTTP响应类,用于构建发送给客户端的HTTP响应
class HttpResponse
{
public:
// 构造函数,初始化空行和HTTP版本
HttpResponse() : _blankline(glinespace), _version("HTTP/1.0")
{
}
// 序列化:将HttpResponse对象转换为HTTP响应字符串
std::string Serialize()
{
// 构建状态行:HTTP/1.0 200 OK\r\n
std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;
// 构建响应头
std::string resp_header;
for (auto &header : _headers)
{
// 每个头部格式:key: value\r\n
std::string line = header.first + glinesep + header.second + glinespace;
resp_header += line;
}
// 组合:状态行 + 响应头 + 空行 + 响应体
return status_line + resp_header + _blankline + _text;
}
// 设置目标文件路径
void SetTargetFile(const std::string &target)
{
_targetfile = target;
}
// 设置HTTP状态码和对应的描述
void SetCode(int code)
{
_code = code;
switch (_code)
{
case 200:
_desc = "OK";
break;
case 404:
_desc = "Not Found";
break;
case 301:
_desc = "Moved Permanently";
break;
case 302:
_desc = "See Other";
break;
default:
break;
}
}
// 设置HTTP响应头(如果已存在则不设置)
void SetHeader(const std::string &key, const std::string &value)
{
auto iter = _headers.find(key);
if (iter != _headers.end()) // 如果key已存在
return; // 直接返回,不覆盖
_headers.insert(std::make_pair(key, value)); // 插入新的键值对
}
// 根据文件后缀名确定Content-Type
std::string Uri2Suffix(const std::string &targetfile)
{
// ./wwwroot/a/b/c.html
auto pos = targetfile.rfind("."); // 从后往前查找最后一个.
if (pos == std::string::npos) // 如果没有找到后缀名
{
return "text/html"; // 默认返回text/html
}
std::string suffix = targetfile.substr(pos); // 提取后缀名
if (suffix == ".html" || suffix == ".htm")
return "text/html";
else if (suffix == ".jpg")
return "image/jpeg";
else if (suffix == "png")
return "image/png";
else
return "";
}
// 构建HTTP响应
bool MakeResponse()
{
// 特殊处理:忽略favicon.ico请求
if (_targetfile == "./wwwroot/favicon.ico")
{
LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";
return false; // 返回false表示不发送响应
}
// 重定向测试:如果请求redir_test,返回301重定向
if (_targetfile == "./wwwroot/redir_test")
{
SetCode(301); // 设置状态码301
SetHeader("Location", "https://www.qq.com/"); // 设置重定向地址
return true;
}
// 读取文件内容
int filesize = 0;
bool res = Util::ReadFileContent(_targetfile, &_text); // 读取目标文件
if (!res) // 如果文件读取失败(文件不存在)
{
_text = "";
LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";
// 设置404状态
SetCode(404);
// 加载404错误页面
_targetfile = webroot + page_404; // 设置404页面路径
filesize = Util::FileSize(_targetfile); // 获取404页面大小
Util::ReadFileContent(_targetfile, &_text); // 读取404页面内容
// 设置响应头
std::string suffix = Uri2Suffix(_targetfile); // 根据后缀确定Content-Type
SetHeader("Content-Type", suffix);
SetHeader("Content-Length", std::to_string(filesize));
// 注释掉的302重定向示例
// SetCode(302);
// SetHeader("Location", "http://8.137.19.140:8080/404.html");
// return true;
}
else // 文件读取成功
{
LOG(LogLevel::DEBUG) << "读取文件: " << _targetfile;
// 设置200状态
SetCode(200);
// 设置响应头
filesize = Util::FileSize(_targetfile);
std::string suffix = Uri2Suffix(_targetfile);
SetHeader("Conent-Type", suffix);
SetHeader("Content-Length", std::to_string(filesize));
SetHeader("Set-Cookie", "username=zhangsan;"); // 设置Cookie
// SetHeader("Set-Cookie", "passwd=123456;");
}
return true;
}
// 设置响应体的文本内容
void SetText(const std::string &t)
{
_text = t;
}
// 反序列化方法(当前未实现,直接返回true)
bool Deserialize(std::string &reqstr)
{
return true;
}
// 析构函数
~HttpResponse() {}
// 响应字段(为了方便调试,设为public,实际应该为private)
public:
std::string _version; // HTTP版本
int _code; // 状态码,如404
std::string _desc; // 状态描述,如"Not Found"
std::unordered_map<std::string, std::string> _headers; // 响应头
std::vector<std::string> cookie; // Cookie列表(未使用)
std::string _blankline; // 空行
std::string _text; // 响应体
// 其他属性
std::string _targetfile; // 目标文件路径
};
基本使用(返回静态文件):
cpp
#include <iostream>
#include <string>
// 假设有相关的依赖文件
#include "httpres.h"
// 全局变量定义
std::string webroot = "./wwwroot/";
std::string homepage = "index.html";
std::string page_404 = "404.html";
int main() {
// 创建一个HttpResponse对象
HttpResponse response;
// 设置要返回的目标文件
response.SetTargetFile("./wwwroot/about.html");
// 构建HTTP响应(会自动设置状态码、Content-Type等)
bool shouldSend = response.MakeResponse();
if (shouldSend) {
// 序列化为HTTP响应字符串
std::string http_response = response.Serialize();
std::cout << "生成的HTTP响应:" << std::endl;
std::cout << http_response << std::endl;
// 在实际服务器中,可以通过socket发送这个字符串
// send(client_socket, http_response.c_str(), http_response.size(), 0);
} else {
std::cout << "不需要发送响应(如favicon.ico)" << std::endl;
}
return 0;
}
模块5:函数指针定义和Http类
cpp
// 定义HTTP处理函数的类型:接受HttpRequest和HttpResponse的引用
using http_func_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;
// Http服务器主类
// 功能:
// 1. 返回静态资源
// 2. 提供动态交互的能力
class Http
{
public:
// 构造函数,创建TCP服务器
Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port))
{
}
// 处理HTTP请求的核心方法
void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
// 收到请求
std::string httpreqstr;
// 假设:概率大,读到了完整的请求(实际可能有bug)
// tcp是面向字节流的,这里假设一次recv就读取到完整请求
int n = sock->Recv(&httpreqstr); // 从socket接收HTTP请求字符串
if (n > 0) // 如果接收到了数据
{
// 打印原始请求(调试用)
std::cout << "##########################" << std::endl;
std::cout << httpreqstr;
std::cout << "##########################" << std::endl;
// 对报文完整性进行审核 -- 缺(TODO:需要实现)
// 创建请求和响应对象
HttpRequest req;
HttpResponse resp;
// 解析HTTP请求
req.Deserialize(httpreqstr);
if (req.isInteract()) // 如果是交互请求(带查询参数)
{
// _uri: ./wwwroot/login
if (_route.find(req.Uri()) == _route.end()) // 路由不存在
{
// 处理路由不存在的情况(TODO:实现302重定向等)
// SetCode(302)
}
else // 路由存在
{
// 调用注册的处理函数
_route[req.Uri()](req, resp);
// 序列化响应并发送
std::string response_str = resp.Serialize();
sock->Send(response_str);
}
}
else // 静态资源请求
{
// 设置目标文件路径
resp.SetTargetFile(req.Uri());
// 构建响应
if (resp.MakeResponse())
{
// 序列化并发送响应
std::string response_str = resp.Serialize();
sock->Send(response_str);
}
}
}
// 调试模式代码块
// #ifndef DEBUG
// #define DEBUG
#ifdef DEBUG
// 收到请求
std::string httpreqstr;
// 假设:概率大,读到了完整的请求
sock->Recv(&httpreqstr); // 接收请求
std::cout << httpreqstr; // 打印请求
// 直接构建http应答. 内存级别+固定
HttpResponse resp;
resp._version = "HTTP/1.1";
resp._code = 200; // success
resp._desc = "OK";
std::string filename = webroot + homepage; // "./wwwroot/index.html";
// 读取首页文件内容
bool res = Util::ReadFileContent(filename, &(resp._text));
(void)res; // 避免未使用警告
// 序列化并发送响应
std::string response_str = resp.Serialize();
sock->Send(response_str);
#endif
// 对请求字符串,进行反序列化(已在上面的if块中处理)
}
// 启动HTTP服务器
void Start()
{
// 启动TCP服务器,并绑定请求处理函数
tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client)
{ this->HandlerHttpRquest(sock, client); });
}
// 注册服务(动态请求处理)
void RegisterService(const std::string name, http_func_t h)
{
// 构建完整路径键:./wwwroot/login
std::string key = webroot + name;
// 检查路由是否已存在
auto iter = _route.find(key);
if (iter == _route.end()) // 如果不存在
{
_route.insert(std::make_pair(key, h)); // 插入路由
}
}
// 析构函数
~Http()
{
}
private:
std::unique_ptr<TcpServer> tsvrp; // TCP服务器实例(智能指针管理)
std::unordered_map<std::string, http_func_t> _route; // 路由表
};
-
完整读取请求报头后,识别空行作为分隔符
-
对报头进行解析,提取关键属性:Content-Length(表示有效载荷的数据长度)
-
根据获取的Content-Length值,从后续内容中准确截取指定长度的有效载荷数据
基本用法(启动HTTP服务器)
cpp
#include <iostream>
#include "http.h" // 包含Http类头文件
int main() {
// 1. 创建HTTP服务器,监听8080端口
Http server(8080);
// 2. 启动服务器
server.Start();
// 服务器开始运行,监听连接并处理请求
return 0;
}
注册动态路由(处理表单提交)
cpp
#include <iostream>
#include "http.h"
// 登录处理函数
void HandleLogin(HttpRequest &req, HttpResponse &resp) {
std::cout << "处理登录请求" << std::endl;
// 获取查询参数(如:username=zhangsan&password=123456)
std::string args = req.Args();
// 这里可以解析参数,验证用户名密码...
// 简单示例:直接返回欢迎信息
resp.SetCode(200);
resp.SetHeader("Content-Type", "text/html");
std::string response_body = R"(
<html>
<head><title>登录成功</title></head>
<body>
<h1>欢迎登录!</h1>
<p>参数: )" + args + R"(</p>
</body>
</html>
)";
resp.SetText(response_body);
}
// 搜索处理函数
void HandleSearch(HttpRequest &req, HttpResponse &resp) {
resp.SetCode(200);
resp.SetHeader("Content-Type", "text/html");
resp.SetText("<h1>搜索结果页面</h1><p>查询参数: " + req.Args() + "</p>");
}
int main() {
// 创建HTTP服务器
Http server(8080);
// 注册动态路由(注意:路径不要带webroot前缀)
server.RegisterService("/login", HandleLogin); // 对应 ./wwwroot/login
server.RegisterService("/search", HandleSearch); // 对应 ./wwwroot/search
// 启动服务器
server.Start();
return 0;
}
代码总结
这是一个简单的HTTP服务器实现,主要特点包括:
1. 架构设计
-
使用面向对象设计,分离请求解析(HttpRequest)和响应构建(HttpResponse)
-
支持静态文件服务和动态路由处理
-
基于TcpServer实现底层网络通信
2. 功能特点
-
静态文件服务:自动处理HTML、图片等静态资源
-
动态路由:支持注册自定义处理函数
-
错误处理:自动处理404错误,返回错误页面
-
重定向支持:支持301/302重定向
-
Cookie支持:可设置Cookie
3. 主要流程
-
接收HTTP请求字符串
-
解析请求行和URI
-
判断是否为交互请求(带查询参数)
-
交互请求:查找路由并调用对应处理函数
-
静态请求:读取文件并构建响应
-
发送HTTP响应
4. 待改进点
-
请求完整性验证缺失
-
请求头解析不完整
-
并发处理能力有限
-
安全性考虑不足
-
POST请求体处理未实现
这个实现是一个教学级别的HTTP服务器,适合理解HTTP协议基本原理,但在生产环境中需要更完善的实现。这份代码是一个简单的HTTP服务器实现,支持静态资源服务和基本的动态请求处理。
1、总体结构
这个HTTP服务器由以下几个核心类组成:
-
HttpRequest - 解析HTTP请求
-
HttpResponse - 构建HTTP响应
-
Http - 主服务器类,协调请求和响应
2、详细解析
1. 常量和头文件
cpp
const std::string webroot = "./wwwroot"; // 网站根目录
const std::string homepage = "index.html"; // 默认首页
const std::string page_404 = "/404.html"; // 404错误页面
-
webroot:所有静态文件的存储目录 -
使用
#pragma once防止头文件重复包含
2. HttpRequest类 - 请求解析
主要功能:
-
解析HTTP请求行 :从
GET / HTTP/1.1中提取方法、URI、版本 -
处理查询参数 :支持URL中的
?参数 -
路径补全:将相对路径转换为绝对路径
关键方法:
cpp
// 示例:解析请求行
// 输入:"GET /login?user=admin HTTP/1.1"
// 输出:_method="GET", _uri="./wwwroot/login", _args="user=admin"
bool Deserialize(std::string &reqstr) {
// 1. 读取请求行
// 2. 解析方法、URI、版本
// 3. 处理默认页面(/ -> /index.html)
// 4. 分离查询参数
}
路径处理逻辑:
-
请求
/→./wwwroot/index.html -
请求
/about.html→./wwwroot/about.html -
请求
/login?user=admin→./wwwroot/login+args="user=admin"
3. HttpResponse类 - 响应构建
主要功能:
-
状态码管理:自动设置状态描述
-
头部管理:添加HTTP头部
-
文件服务:读取并返回静态文件
-
重定向支持:301/302重定向
关键方法:
cpp
std::string Serialize() {
// 构建完整的HTTP响应:
// 状态行: HTTP/1.0 200 OK\r\n
// 头部: Content-Type: text/html\r\n
// Content-Length: 1234\r\n
// 空行: \r\n
// 正文: <html>...</html>
}
bool MakeResponse() {
// 特殊文件处理(如favicon.ico)
// 重定向测试页面
// 文件存在性检查
// 自动设置Content-Type和Content-Length
}
文件类型识别:
cpp
// 根据文件后缀设置Content-Type
.html/.htm → "text/html"
.jpg → "image/jpeg"
.png → "image/png"
4. Http类 - 主服务器
核心架构:
cpp
class Http {
private:
std::unique_ptr<TcpServer> tsvrp; // TCP服务器
std::unordered_map<std::string, http_func_t> _route; // 路由表
};
工作流程:
-
接收请求:通过TcpServer接收HTTP原始字符串
-
解析请求:使用HttpRequest解析
-
路由分发:
-
静态资源 → 直接返回文件
-
动态请求 → 调用注册的处理函数
-
-
构建响应:使用HttpResponse构建HTTP响应
-
发送响应:通过socket发送给客户端
路由注册机制:
cpp
// 注册动态处理函数
void RegisterService(const std::string name, http_func_t h) {
// 示例:RegisterService("/login", LoginHandler)
// 内部存储:"./wwwroot/login" -> LoginHandler
}
5. 请求处理流程
cpp
void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client) {
// 1. 接收HTTP请求字符串
// 2. 创建HttpRequest和HttpResponse对象
// 3. 解析请求
if (req.isInteract()) { // 是否有查询参数(动态请求)
// 查找路由表,调用对应的处理函数
_route[req.Uri()](req, resp);
} else { // 静态资源请求
// 设置目标文件,生成响应
resp.SetTargetFile(req.Uri());
resp.MakeResponse();
}
// 4. 序列化并发送响应
sock->Send(resp.Serialize());
}
3、特色功能
1. 静态文件服务
-
自动服务
./wwwroot目录下的所有文件 -
自动识别文件类型
-
404错误处理
2. 动态请求支持
cpp
// 可以注册这样的处理函数:
void LoginHandler(HttpRequest &req, HttpResponse &resp) {
std::string args = req.Args(); // "username=admin&password=123"
// 处理登录逻辑...
resp.SetText("Login Success!");
resp.SetCode(200);
}
// 注册路由
server.RegisterService("/login", LoginHandler);
3. 特殊处理
-
忽略favicon.ico:避免浏览器重复请求
-
重定向测试 :
/redir_test重定向到QQ官网 -
Cookie设置 :示例中设置了
Set-Cookie头部
4、代码优缺点
优点:
-
模块化设计:请求、响应、服务器分离
-
易于扩展:通过路由表支持动态处理
-
错误处理:基本的404页面支持
-
调试友好:详细的日志输出
局限性:
-
HTTP协议不完整:只实现了基本功能
-
请求解析简单:未完整解析HTTP头部
-
内存问题:可能处理大文件时内存占用高
-
性能问题:单线程,未使用epoll等高效I/O
5、使用示例
cpp
int main() {
Http server(8080); // 监听8080端口
// 注册动态路由
server.RegisterService("/api/test", [](HttpRequest &req, HttpResponse &resp){
resp.SetText("Dynamic Response");
resp.SetCode(200);
});
server.Start(); // 启动服务器
return 0;
}
这是一个教学级别的HTTP服务器实现,适合学习HTTP协议和网络编程的基本原理。在生产环境中,建议使用更成熟的开源方案(如Nginx、Apache)。
二、Util.hpp
Util工具类代码详细讲解,这个工具类提供了文件读取和处理的常用功能,我将逐行详细讲解:
cpp
// 防止头文件重复包含
#pragma once
// 包含标准库头文件
#include <iostream> // 输入输出流
#include <fstream> // 文件流操作
#include <string> // 字符串类
// 工具类 - 提供文件操作相关的静态方法
class Util
{
public:
// 读取文件内容到字符串中
// 参数:
// filename - 要读取的文件路径
// out - 输出参数,用于存储读取的文件内容
// 返回值:读取成功返回true,失败返回false
static bool ReadFileContent(const std::string &filename /*std::vector<char>*/, std::string *out)
{
// version1: 原始版本,以文本方式读取文件(注释掉的代码)
// 问题:对于二进制文件(如图片)处理不正确
// std::ifstream in(filename); // 创建输入文件流
// if (!in.is_open()) // 检查文件是否成功打开
// {
// return false; // 打开失败返回false
// }
// std::string line; // 临时存储每行内容
// while(std::getline(in, line)) // 逐行读取文件
// {
// *out += line; // 将每行内容添加到输出字符串
// }
// in.close(); // 关闭文件流
// version2 : 以二进制方式进行读取(当前使用的版本)
// 优点:可以正确读取文本文件和二进制文件
// 首先获取文件大小
int filesize = FileSize(filename); // 调用FileSize方法获取文件大小
// 如果文件大小大于0,说明文件存在且有内容
if(filesize > 0)
{
std::ifstream in(filename); // 创建输入文件流(默认文本模式)
if(!in.is_open()) // 检查文件是否成功打开
return false; // 打开失败返回false
// 调整输出字符串的大小为文件大小
// 注意:这里有一个潜在问题,c_str()返回的是const char*,不应该被修改
// 实际上应该使用:out->resize(filesize); 然后使用&(*out)[0]获取可写指针
out->resize(filesize); // 调整字符串大小
// 读取整个文件内容到字符串中
// 这里使用了c_str()获取字符串的内部缓冲区指针,然后强制转换为char*
// 更安全的写法:in.read(&(*out)[0], filesize);
in.read((char*)(out->c_str()), filesize); // 读取filesize字节到字符串中
in.close(); // 关闭文件流
}
else // 文件大小为0或负数(文件不存在或为空)
{
return false; // 返回false表示读取失败
}
return true; // 读取成功返回true
}
// 从大字符串中读取一行(以指定分隔符为界)
// 参数:
// bigstr - 输入的大字符串,读取后会被修改(移除已读取的部分)
// out - 输出参数,存储读取的一行内容
// sep - 行分隔符,通常是"\r\n"
// 返回值:成功读取一行返回true,否则返回false
static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep/*\r\n*/)
{
// 在bigstr中查找分隔符的位置
auto pos = bigstr.find(sep);
// 如果没有找到分隔符,说明没有完整的一行
if(pos == std::string::npos)
return false; // 返回false
// 从开始到分隔符位置就是一行内容
*out = bigstr.substr(0, pos);
// 从bigstr中删除已经读取的部分(包括分隔符)
// 这样可以继续读取下一行
bigstr.erase(0, pos + sep.size());
return true; // 成功读取一行返回true
}
// 获取文件大小(以字节为单位)
// 参数:
// filename - 要检查的文件路径
// 返回值:文件大小(字节数),文件不存在或无法打开返回-1
static int FileSize(const std::string &filename)
{
// 以二进制模式打开文件
// std::ios::binary 确保以二进制方式读取,避免文本模式下的转换
std::ifstream in(filename, std::ios::binary);
// 检查文件是否成功打开
if(!in.is_open())
return -1; // 打开失败返回-1
// 将文件指针移动到文件末尾
// 参数说明:
// 0 - 偏移量
// in.end - 相对于文件末尾
in.seekg(0, in.end);
// 获取当前位置(即文件大小)
// tellg()返回当前读取位置
int filesize = in.tellg();
// 将文件指针移回文件开头
// 这样不会影响后续的读取操作(如果有的话)
in.seekg(0, in.beg);
in.close(); // 关闭文件流
return filesize; // 返回文件大小
}
};
1、代码总结与注意事项
1. 类设计特点
-
静态工具类:所有方法都是静态的,不需要创建Util对象
-
功能聚焦:专注于文件操作相关的工具函数
-
错误处理:每个方法都有明确的成功/失败返回值
2. 方法详细说明
ReadFileContent方法
-
功能:读取整个文件内容到字符串中
-
改进:从文本模式改为二进制模式,支持图片等二进制文件
-
潜在问题:
cpp// 不安全的写法: in.read((char*)(out->c_str()), filesize); // 更安全的写法: out->resize(filesize); in.read(&(*out)[0], filesize); // 或者 in.read(out->data(), filesize);因为
c_str()返回的是const char*,不应该被写入。虽然在某些实现中可能工作,但不是标准做法。
ReadOneLine方法
-
功能:从大字符串中提取一行(基于自定义分隔符)
-
应用场景:HTTP协议解析中读取请求行和头部
-
副作用 :会修改输入字符串
bigstr,移除已读取的部分
FileSize方法
-
功能:获取文件大小
-
实现原理 :使用
seekg和tellg组合 -
二进制模式 :使用
std::ios::binary确保正确计算大小
3. 设计模式
-
工具类模式:提供一组相关的静态方法
-
无状态:不维护任何成员变量,所有方法都是纯函数
-
参数设计:
-
使用
std::string*作为输出参数(C++风格) -
使用引用参数
std::string&允许修改原字符串
-
4. 使用示例
cpp
// 读取文件内容
std::string content;
if (Util::ReadFileContent("test.txt", &content)) {
std::cout << "文件内容:" << content << std::endl;
}
// 获取文件大小
int size = Util::FileSize("test.txt");
if (size > 0) {
std::cout << "文件大小:" << size << "字节" << std::endl;
}
// 逐行读取
std::string text = "第一行\r\n第二行\r\n第三行";
std::string line;
while (Util::ReadOneLine(text, &line, "\r\n")) {
std::cout << "行内容:" << line << std::endl;
}
5. 改进建议
-
异常处理:考虑添加异常处理机制
-
性能优化:大文件读取可以考虑分块读取
-
跨平台 :路径分隔符处理(Windows使用
\,Unix使用/) -
编码处理:考虑不同编码格式的文件读取
-
内存安全 :修正
ReadFileContent中的潜在问题
这个工具类是HTTP服务器的重要组成部分,为文件操作提供了基础支持。
三、引入之前实现过的相关头文件
Common.hpp
cpp
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum ExitCode
{
OK = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR,
OPEN_ERR
};
class NoCopy
{
public:
NoCopy(){}
~NoCopy(){}
NoCopy(const NoCopy &) = delete;
const NoCopy &operator = (const NoCopy&) = delete;
};
#define CONV(addr) ((struct sockaddr*)&addr)
InetAddr.hpp
cpp
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类
class InetAddr
{
public:
InetAddr() {}
InetAddr(struct sockaddr_in &addr)
{
SetAddr(addr);
}
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
_addr.sin_port = htons(_port);
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
}
InetAddr(uint16_t port) : _port(port), _ip()
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_addr.s_addr = INADDR_ANY;
_addr.sin_port = htons(_port);
}
void SetAddr(struct sockaddr_in &addr)
{
_addr = addr;
// 网络转主机
_port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
// _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
_ip = ipbuffer;
}
uint16_t Port() { return _port; }
std::string Ip() { return _ip; }
const struct sockaddr_in &NetAddr() { return _addr; }
const struct sockaddr *NetAddrPtr()
{
return CONV(_addr);
}
socklen_t NetAddrLen()
{
return sizeof(_addr);
}
bool operator==(const InetAddr &addr)
{
return addr._ip == _ip && addr._port == _port;
}
std::string StringAddr()
{
return _ip + ":" + std::to_string(_port);
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
Log.hpp
cpp
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 策略模式,C++多态特性
// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
// 刷新策略基类
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印日志的策略 : 子类
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
~ConsoleLogStrategy()
{
}
private:
Mutex _mutex;
};
// 文件打印日志的策略 : 子类
const std::string defaultpath = "/var/log/";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
std::ofstream out(filename, std::ios::app); // 追加写入的 方式打开
if (!out.is_open())
{
return;
}
out << message << gsep;
out.close();
}
~FileLogStrategy()
{
}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式
// 1. 形成日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2Str(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
std::string GetTimeStamp()
{
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900,
curr_tm.tm_mon+1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec
);
return timebuffer;
}
// 1. 形成日志 && 2. 根据不同的策略,完成刷新
class Logger
{
public:
Logger()
{
EnableConsoleLogStrategy();
}
void EnableFileLogStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy()
{
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 表示的是未来的一条日志
class LogMessage
{
public:
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
// 日志的左边部分,合并起来
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
}
// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
template <typename T>
LogMessage &operator<<(const T &info)
{
// a = b = c =d;
// 日志的右半部分,可变的
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _src_name;
int _line_number;
std::string _loginfo; // 合并之后,一条完整的信息
Logger &_logger;
};
// 这里故意写成返回临时对象
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
// 全局日志对象
Logger logger;
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}
#endif
Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
(void)n;
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
(void)n;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
pthread_mutex_t *Get()
{
return &_mutex;
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex):_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex &_mutex;
};
}
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const static int gbacklog = 16;
// 模版方法模式
// 基类socket, 大部分方法,都是纯虚方法
class Socket
{
public:
virtual ~Socket() {}
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void Close() = 0;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string &message) = 0;
virtual int Connect(const std::string &server_ip, uint16_t port) = 0;
public:
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
void BuildTcpClientSocketMethod()
{
SocketOrDie();
}
// void BuildUdpSocketMethod()
// {
// SocketOrDie();
// BindOrDie();
// }
};
const static int defaultfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(defaultfd)
{
}
TcpSocket(int fd) : _sockfd(fd)
{
}
~TcpSocket() {}
void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
}
void BindOrDie(uint16_t port) override
{
InetAddr localaddr(port);
int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success";
}
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = ::accept(_sockfd, CONV(peer), &len);
if (fd < 0)
{
LOG(LogLevel::WARNING) << "accept warning ...";
return nullptr; // TODO
}
client->SetAddr(peer);
return std::make_shared<TcpSocket>(fd);
}
// n == read的返回值
int Recv(std::string *out) override
{
// 流式读取,不关心读到的是什么
char buffer[4096*2];
ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
*out += buffer; // 故意
}
return n;
}
int Send(const std::string &message) override
{
return send(_sockfd, message.c_str(), message.size(), 0);
}
void Close() override //??
{
if (_sockfd >= 0)
::close(_sockfd);
}
int Connect(const std::string &server_ip, uint16_t port) override
{
InetAddr server(server_ip, port);
return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
}
private:
int _sockfd; // _sockfd , listensockfd, sockfd;
};
// class UdpSocket : public Socket
// {
// };
}
TcpServer.hpp
cpp
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>
using namespace SocketModule;
using namespace LogModule;
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
class TcpServer
{
public:
TcpServer(uint16_t port) : _port(port),
_listensockptr(std::make_unique<TcpSocket>()),
_isrunning(false)
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start(ioservice_t callback)
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
auto sock = _listensockptr->Accept(&client); // 1. 和client通信sockfd 2. client 网络地址
if (sock == nullptr)
{
continue;
}
LOG(LogLevel::DEBUG) << "accept success ..." << client.StringAddr();
// sock && client
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "fork error ...";
// excepter(sock); //
exit(FORK_ERR);
}
else if (id == 0)
{
// 子进程 -> listensock
_listensockptr->Close();
if (fork() > 0)
exit(OK);
// 孙子进程在执行任务,已经是孤儿了
callback(sock, client);
sock->Close();
exit(OK);
}
else
{
// 父进程 -> sock
sock->Close();
pid_t rid = ::waitpid(id, nullptr, 0);
(void)rid;
}
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
//func_t excepter; // 服务器异常的回调
};
四、相关的HTML文件
404.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 页面未找到</title>
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
color: #333;
text-align: center;
padding: 50px 0;
margin: 0;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
font-size: 5em;
margin: 0;
color: #e74c3c;
}
h2 {
margin-top: 0;
color: #333;
}
p {
margin-bottom: 30px;
}
a {
color: #3498db;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.emoji {
font-size: 3em;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="emoji">😕</div>
<h1>404</h1>
<h2>页面未找到</h2>
<p>抱歉,您访问的页面不存在或已被移除。</p>
<p>您可以返回<a href="/">首页</a>或检查URL是否正确。</p>
</div>
</body>
</html>

Login.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
text-align: center;
}
.login-container h2 {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
text-align: left;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.form-group input[type="submit"] {
background-color: #007bff;
color: #fff;
border: none;
cursor: pointer;
}
.form-group input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="login-container">
<h2>Login</h2>
<form action="/login" method="GET">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<input type="submit" value="Login">
</div>
<a href="Register.html">Login</a> <!-- 跳转到登录页面 -->
<a href="index.html">Register</a> <!-- 跳转到注册页面 -->
</form>
</div>
</body>
</html>

Register.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register Page</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.register-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 350px;
text-align: center;
}
.register-container h2 {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
text-align: left;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.form-group input[type="submit"] {
background-color: #28a745;
color: #fff;
border: none;
cursor: pointer;
}
.form-group input[type="submit"]:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="register-container">
<h2>Register</h2>
<form action="/register" method="GET">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="confirm-password">Confirm Password</label>
<input type="password" id="confirm-password" name="confirm-password" required>
</div>
<div class="form-group">
<input type="submit" value="Register">
</div>
<a href="Login.html">Login</a> <!-- 跳转到登录页面 -->
<a href="index.html">Register</a> <!-- 跳转到注册页面 -->
</form>
</div>
</body>
</html>

index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Default Home Page</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
}
header {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
text-align: center;
}
nav {
background-color: #343a40;
padding: 10px 0;
}
nav a {
color: #fff;
text-decoration: none;
padding: 10px 20px;
display: inline-block;
}
nav a:hover {
background-color: #5a6268;
}
.container {
padding: 20px;
}
.welcome {
text-align: center;
margin-bottom: 20px;
}
.welcome h1 {
margin: 0;
}
.content {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
footer {
background-color: #343a40;
color: #fff;
text-align: center;
padding: 10px 0;
position: fixed;
width: 100%;
bottom: 0;
}
</style>
</head>
<body>
<header>
<h1>Welcome to Our Website</h1>
</header>
<nav>
<a href="#">Home</a>
<a href="Login.html">Login</a> <!-- 跳转到登录页面 -->
<a href="Register.html">Register</a> <!-- 跳转到注册页面 -->
<a href="#">About</a>
<a href="#">Contact</a>
</nav>
<div class="container">
<div class="welcome">
<h1>Welcome to Our Default Home Page</h1>
<p>This is a simple default home page template.</p>
</div>
<div class="content">
<h2>Introduction</h2>
<p>This is a basic HTML template for a default home page. It includes a header, navigation bar, a welcome section, and a content area. You can customize this template to suit your needs.</p>
</div>
<!-- <a href="http://8.137.19.140:8081/image/1.png">板书1</a>
<a href="http://8.137.19.140:8081/image/2.png">板书2</a>
<a href="http://8.137.19.140:8081/image/3.png">板书3</a>
<a href="http://8.137.19.140:8081/image/4.png">板书4</a>
<a href="http://8.137.19.140:8081/image/5.jpg">图片5</a> -->
<img src="/image/5.jpg"/>
<!-- <img src="/image/1.png"/> -->
<a href="/board1.html">查看板书</a>
</div>
<footer>
<p>© 2025 Your Company Name. All rights reserved.</p>
</footer>
</body>
</html>

test.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<a href="https://www.qq.com/">点我</a>
</body>
</html>

五、Main.cc
HTTP服务器主程序代码详细讲解,这是HTTP服务器的主程序文件,包含入口函数和示例处理函数:
cpp
// 包含HTTP服务器主类的头文件
#include "Http.hpp"
// Login函数:处理用户登录请求
// 参数:
// req - HTTP请求对象,包含客户端请求信息
// resp - HTTP响应对象,用于构建返回给客户端的响应
void Login(HttpRequest &req, HttpResponse &resp)
{
// 从请求中获取查询参数(args()返回查询字符串,如"username=zhangsan&passwd=123456")
// 注意:这里只是打印日志,实际上应该解析参数
LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
// 构建简单的响应文本
// 这里直接拼接查询参数,实际应用中应该解析参数并进行验证
std::string text = "hello: " + req.Args(); // 例如: "hello: username=zhangsan&password=123456"
// TODO: 实际登录认证逻辑应该在这里实现
// 1. 解析查询参数,获取用户名和密码
// 2. 验证用户名密码是否正确
// 3. 根据验证结果返回不同响应
// 设置HTTP响应状态码为200(成功)
resp.SetCode(200);
// 设置响应头:Content-Type指定响应体类型为纯文本
resp.SetHeader("Content-Type","text/plain");
// 设置响应头:Content-Length指定响应体大小
resp.SetHeader("Content-Length", std::to_string(text.size()));
// 设置响应体内容
resp.SetText(text);
}
// 以下是被注释掉的示例处理函数,展示了如何添加更多业务逻辑
// void Register(HttpRequest &req, HttpResponse &resp)
// {
// // 注册功能处理函数
// LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
// std::string text = "hello: " + req.Args();
//
// resp.SetCode(200);
// resp.SetHeader("Content-Type","text/plain");
// resp.SetHeader("Content-Length", std::to_string(text.size()));
// resp.SetText(text);
// }
// void VipCheck(HttpRequest &req, HttpResponse &resp)
// {
// // VIP检查功能处理函数
// LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
// std::string text = "hello: " + req.Args();
// resp.SetCode(200);
// resp.SetHeader("Content-Type","text/plain");
// resp.SetHeader("Content-Length", std::to_string(text.size()));
// resp.SetText(text);
// }
// void Search(HttpRequest &req, HttpResponse &resp)
// {
// // 搜索功能处理函数
// // 这里没有实现具体逻辑
// }
// HTTP服务器主入口函数
// 参数:
// argc - 命令行参数个数
// argv - 命令行参数数组
int main(int argc, char *argv[])
{
// 检查命令行参数数量
// 程序期望接收一个参数:端口号
// 正确用法:./server 8080
if(argc != 2) // 如果不是2个参数(程序名+端口号)
{
// 打印使用说明
std::cout << "Usage: " << argv[0] << " port" << std::endl;
// 退出程序,使用预定义的错误码USAGE_ERR(可能在其他头文件中定义)
exit(USAGE_ERR);
}
// 将命令行参数转换为端口号
// argv[0] 是程序名,argv[1] 是端口号字符串
uint16_t port = std::stoi(argv[1]); // stoi将字符串转换为整数
// 创建HTTP服务器实例
// 使用智能指针管理,避免内存泄漏
// std::make_unique是C++14特性,创建unique_ptr智能指针
std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);
// 注册服务(即注册动态请求处理函数)
// 将URI路径与处理函数绑定
// 当用户访问"/login"时,会调用Login函数处理
httpsvr->RegisterService("/login", Login);
// 以下是其他示例服务的注册(被注释掉了)
// httpsvr->RegisterService("/register", Register);
// httpsvr->RegisterService("/vip_check", VipCheck);
// httpsvr->RegisterService("/s", Search);
// httpsvr->RegisterService("/", Login); // 根路径也可以绑定处理函数
// 启动HTTP服务器
// Start()方法会启动服务器并进入事件循环,等待客户端连接
httpsvr->Start();
// 程序结束,返回0表示正常退出
// 注意:由于Start()方法可能会阻塞,这里的return只有在服务器停止后才会执行
return 0;
}
1、代码架构分析
1. 处理函数设计模式
cpp
// 处理函数的标准签名
void HandlerName(HttpRequest &req, HttpResponse &resp)
{
// 1. 从req中提取参数
// 2. 执行业务逻辑
// 3. 设置resp的各个部分
}
2. 路由注册机制
cpp
// 路由表结构
std::unordered_map<std::string, http_func_t> _route;
// 注册路由
httpsvr->RegisterService("/login", Login);
// 内部实现:将"/login"映射到Login函数
3. 请求处理流程
客户端请求 → 服务器接收 → 解析请求 → 路由匹配 → 调用处理函数 → 构建响应 → 发送响应
2、重要技术细节
1. 查询参数处理
cpp
// 请求示例:GET /login?username=zhangsan&password=123456
// req.Args() 返回:"username=zhangsan&password=123456"
// 实际应用中需要解析这个字符串:
// std::string args = req.Args();
// 然后按'&'分割键值对,再按'='分割键和值
2. HTTP响应构建
cpp
// 一个完整的HTTP响应包含:
resp.SetCode(200); // 状态码
resp.SetHeader("Content-Type", ...); // 内容类型
resp.SetHeader("Content-Length", ...);// 内容长度
resp.SetText(...); // 响应体
3. 日志记录
cpp
// 使用日志系统记录调试信息
LOG(LogLevel::DEBUG) << "日志信息";
// 有助于调试和监控服务器运行状态
3、运行服务器
使用tree命令查看当前项目的全局结构:

可以看出,在浏览器端发起请求时,首页作为网站的入口点,整个站点结构呈现为多叉树形态。当用户点击链接时,浏览器会生成新的访问地址并触发二次请求。所有请求的资源都通过HTTP请求的URI进行标识。
编译和运行:
cpp
# 编译(假设使用g++)
g++ -std=c++14 -o httpserver main.cpp Http.cpp Util.cpp Socket.cpp TcpServer.cpp -lpthread
# 运行(在8080端口启动服务器)
./httpserver 8080
这里我们使用makefile文件来编译:
bash
myhttp:Main.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f myhttp
编译完成后,我们运行服务端:

测试请求:
bash
# 访问静态文件
curl http://113.45.79.2:8080/index.html

我们也可以直接通过浏览器来访问,这样更直观并符合HTML的演示:

bash
# 访问动态接口
curl http://113.45.79.2:8080/login?username=zhangsan&password=123456


总结
这个HTTP服务器示例展示了:
-
基于TCP的HTTP服务器基本架构
-
静态文件服务和动态API的统一处理
-
简单的路由注册机制
-
请求/响应的完整处理流程
可以作为学习HTTP协议和服务器开发的入门示例,理解请求处理、响应构建的基本原理。