文章目录
- 1. 实现一个基础的HTTP Web服务器
- 1.1 功能实现:
- 1.2 Log.hpp-日志记录器
- 1.3 HttpServer.hpp-网页服务器
- 1.4 Socket.hpp-网络通信器
- 1.5 HttpServer.cc-服务器启动器
1. 实现一个基础的HTTP Web服务器
1.1 功能实现:
总体功能:
提供Web服务,响应客户端(浏览器)的HTTP请求
支持静态文件服务(如HTML、图片等)
多线程处理并发请求
带日志记录功能
- 具体工作流程:
浏览器 → 发送HTTP请求 → 服务器
↓
解析请求
↓
查找文件
↓
返回响应
↓
浏览器 ← 显示页面 ← 服务器
- 各模块职责:
日志记录器(Log.hpp)
- 记录服务器运行状态
- 错误追踪和调试
网页服务器(HttpServer.hpp)
- 解析HTTP请求
- 处理静态文件
- 生成HTTP响应
- 多线程处理请求
网络通信器(Socket.hpp)
- 处理底层网络通信
- 管理TCP连接
服务器启动器(HttpServer.cc)
- 程序入口
- 初始化和启动服务
1.2 Log.hpp-日志记录器
Log.hpp
#pragma once // 防止头文件重复包含
// 系统头文件包含
#include <iostream> // 标准输入输出
#include <time.h> // 时间相关函数
#include <stdarg.h> // 可变参数处理
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态
#include <fcntl.h> // 文件控制选项
#include <unistd.h> // UNIX标准函数
#include <stdlib.h> // 标准库函数
// 基础配置宏定义
#define SIZE 1024 // 缓冲区大小
#define LogFile "log.txt" // 默认日志文件名
// 日志级别定义(按严重程度递增)
#define Info 0 // 普通信息:记录系统正常操作信息
#define Debug 1 // 调试信息:记录调试相关信息
#define Warning 2 // 警告信息:记录潜在问题
#define Error 3 // 错误信息:记录错误但不影响系统运行
#define Fatal 4 // 致命错误:记录导致系统崩溃的错误
// 日志输出方式定义
#define Screen 1 // 输出到屏幕:直接显示在终端
#define Onefile 2 // 输出到单个文件:所有日志记录到同一个文件
#define Classfile 3 // 分类输出:根据日志级别输出到不同文件
class Log {
private:
int printMethod; // 日志输出方式
std::string path; // 日志文件存储路径
public:
// 构造函数:初始化日志系统
Log() {
printMethod = Screen; // 默认输出到屏幕
path = "./log/"; // 默认日志目录
}
// 设置日志输出方式
void Enable(int method) {
printMethod = method;
}
// 将日志级别转换为对应的字符串
std::string levelToString(int level) {
switch (level) {
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error: return "Error";
case Fatal: return "Fatal";
default: return "None";
}
}
// 根据设置的输出方式打印日志
void printLog(int level, const std::string &logtxt) {
switch (printMethod) {
case Screen: // 输出到屏幕
std::cout << logtxt << std::endl;
break;
case Onefile: // 输出到单个文件
printOneFile(LogFile, logtxt);
break;
case Classfile: // 根据日志级别输出到不同文件
printClassFile(level, logtxt);
break;
}
}
// 将日志输出到指定文件
void printOneFile(const std::string &logname, const std::string &logtxt) {
std::string _logname = path + logname;
// 以追加方式打开文件,如果不存在则创建
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0) return; // 打开失败则直接返回
write(fd, logtxt.c_str(), logtxt.size()); // 写入日志内容
close(fd); // 关闭文件描述符
}
// 根据日志级别将日志输出到不同文件
void printClassFile(int level, const std::string &logtxt) {
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // 构造文件名,如"log.txt.Debug"
printOneFile(filename, logtxt);
}
// 重载函数调用运算符,实现日志记录的核心功能
void operator()(int level, const char *format, ...) {
// 1. 获取当前时间
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
// 2. 格式化日志头部(时间和级别信息)
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer),
"[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(),
ctime->tm_year + 1900,
ctime->tm_mon + 1,
ctime->tm_mday,
ctime->tm_hour,
ctime->tm_min,
ctime->tm_sec);
// 3. 处理可变参数,格式化日志内容
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 4. 组合完整的日志消息
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// 5. 输出日志
printLog(level, logtxt);
}
};
// 创建全局日志对象,方便在程序各处使用
Log lg;
/* 示例用法:
int main() {
lg.Enable(Screen); // 设置输出到屏幕
lg(Info, "Server started on port %d", 8080);
lg(Error, "Failed to connect to %s", "database");
return 0;
}
*/
1.3 HttpServer.hpp-网页服务器
HttpServer.hpp
功能:
- HTTP请求处理
- 多线程服务
- 静态文件响应
- Cookie支持
- 错误页面处理
#pragma once // 防止头文件重复包含
// 基础库和系统库引入
#include <iostream> // 标准输入输出
#include <string> // 字符串处理
#include <pthread.h> // POSIX线程库
#include <fstream> // 文件流操作
#include <vector> // 动态数组
#include <sstream> // 字符串流
#include <sys/types.h> // 基本系统数据类型
#include <sys/socket.h> // Socket通信
#include <unordered_map> // 哈希表
// 自定义头文件
#include "Socket.hpp" // Socket封装类
#include "Log.hpp" // 日志系统
// 全局常量定义
const std::string wwwroot="./wwwroot"; // web服务器根目录
const std::string sep = "\r\n"; // HTTP消息分隔符
const std::string homepage = "index.html"; // 默认主页
static const int defaultport = 8082; // 默认端口号
class HttpServer; // 前向声明
// 线程数据结构:存储每个线程处理的连接信息
class ThreadData
{
public:
ThreadData(int fd, HttpServer *s) : sockfd(fd), svr(s) {}
public:
int sockfd; // 客户端连接的socket描述符
HttpServer *svr; // HTTP服务器对象指针
};
// HTTP请求解析类
class HttpRequest
{
public:
// 反序列化HTTP请求
void Deserialize(std::string req)
{
while(true)
{
std::size_t pos = req.find(sep);
if(pos == std::string::npos) break;
std::string temp = req.substr(0, pos);
if(temp.empty()) break;
req_header.push_back(temp); // 保存请求头
req.erase(0, pos+sep.size()); // 移除已处理部分
}
text = req; // 保存请求体
}
// 解析HTTP请求,处理URL和文件路径
void Parse()
{
// 解析请求行(方法、URL、HTTP版本)
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
// 构建文件路径
file_path = wwwroot;
if(url == "/" || url == "/index.html") {
file_path += "/";
file_path += homepage; // 处理默认主页
}
else file_path += url; // 其他页面
// 获取文件后缀
auto pos = file_path.rfind(".");
if(pos == std::string::npos) suffix = ".html";
else suffix = file_path.substr(pos);
}
// 调试打印函数
void DebugPrint()
{
// 输出请求信息用于调试
for(auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
std::cout << "method: " << method << std::endl;
std::cout << "url: " << url << std::endl;
std::cout << "http_version: " << http_version << std::endl;
std::cout << "file_path: " << file_path << std::endl;
std::cout << text << std::endl;
}
public:
std::vector<std::string> req_header; // 请求头部
std::string text; // 请求正文
// 解析后的请求信息
std::string method; // 请求方法(GET、POST等)
std::string url; // 请求URL
std::string http_version; // HTTP协议版本
std::string file_path; // 请求文件路径
std::string suffix; // 文件后缀
};
// HTTP服务器类
class HttpServer
{
public:
// 构造函数:初始化端口和支持的内容类型
HttpServer(uint16_t port = defaultport) : port_(port)
{
content_type.insert({".html", "text/html"});
content_type.insert({".png", "image/png"});
}
// 启动服务器
bool Start()
{
// 初始化Socket
// 1. 创建Socket
listensock_.Socket();
/* 这一步完成以下操作:
a) 调用系统函数 socket(AF_INET, SOCK_STREAM, 0) 创建TCP Socket
- AF_INET: 使用IPv4协议族
- SOCK_STREAM: 使用TCP协议
- 0: 使用默认协议
b) 设置Socket选项
- SO_REUSEADDR: 允许地址重用,避免服务器重启时的"地址已被使用"错误
*/
// 2. 绑定端口
listensock_.Bind(port_);
/* 这一步完成以下操作:
a) 创建sockaddr_in结构体,设置:
- sin_family = AF_INET (IPv4)
- sin_port = htons(port_) (设置端口号,转换为网络字节序)
- sin_addr.s_addr = INADDR_ANY (监听所有网卡接口)
b) 调用bind()函数将Socket与地址绑定
- 如果端口已被占用或权限不足,会失败
*/
// 3. 开始监听
listensock_.Listen();
/* 这一步完成以下操作:
a) 调用listen()函数,将Socket转换为监听状态
- backlog参数设置为10,表示等待连接队列的最大长度
- 超过此长度的新连接请求会被拒绝
b) 此后Socket就能接受客户端连接请求
- 服务器调用Accept()接受新的连接
*/
// 主循环:接受并处理连接
for (;;)
{
// 准备变量存储客户端信息
std::string clientip; // 将存储客户端的IP地址
uint16_t clientport; // 将存储客户端的端口号
// 接受新的客户端连接
int sockfd = listensock_.Accept(&clientip, &clientport);
/* Accept函数做了这些事:
1. 等待客户端连接
2. 获取客户端的IP和端口
3. 返回新的socket描述符用于与该客户端通信
*/
// 连接失败则继续等待下一个连接
if (sockfd < 0) continue;
// 记录新连接日志
lg(Info, "get a new connect, sockfd: %d", sockfd);
// 创建新线程处理请求
// 1. 声明线程ID变量
pthread_t tid; // 用于存储新创建线程的ID
// 2. 创建线程数据结构,传入连接描述符和当前服务器对象
ThreadData *td = new ThreadData(sockfd, this);
/* ThreadData包含:
- sockfd:与客户端通信的socket描述符
- this:当前服务器对象的指针,用于访问服务器的方法
*/
// 3. 创建新线程处理请求
pthread_create(&tid, nullptr, ThreadRun, td);
/* 参数含义:
- &tid:存储新线程ID
- nullptr:使用默认线程属性
- ThreadRun:线程将执行的函数
- td:传递给线程函数的参数
*/
// 新线程会执行ThreadRun函数处理客户端请求
// 主线程继续循环等待新的连接
}
}
// 读取HTML文件内容
static std::string ReadHtmlContent(const std::string &htmlpath)
{
// 1. 打开文件
std::ifstream in(htmlpath, std::ios::binary);
/* 说明:
- binary模式打开确保文件按原样读取
- 不会对换行符进行转换
*/
// 文件打开失败则返回空字符串
if(!in.is_open()) return "";
// 2. 获取文件大小
in.seekg(0, std::ios_base::end); // 将读指针移到文件末尾
auto len = in.tellg(); // 获取当前位置(即文件大小)
in.seekg(0, std::ios_base::beg); // 将读指针移回文件开头
// 3. 读取文件内容
std::string content; // 用于存储文件内容
content.resize(len); // 预分配空间
// 一次性读取整个文件内容到字符串中
in.read((char*)content.c_str(), content.size());
// 4. 关闭文件
in.close();
return content; // 返回文件内容
}
// 根据文件后缀获取Content-Type
std::string SuffixToDesc(const std::string &suffix)
{
// 在content_type映射表中查找文件后缀对应的MIME类型
auto iter = content_type.find(suffix);
// 如果找不到对应的MIME类型
if(iter == content_type.end())
return content_type[".html"]; // 默认返回html的MIME类型:"text/html"
else
return content_type[suffix]; // 返回找到的MIME类型
/* 例如:
- .html -> "text/html"
- .png -> "image/png"
这个MIME类型会被用在HTTP响应头的Content-Type字段中
*/
}
// 处理HTTP请求
void HandlerHttp(int sockfd)
{
// 1. 接收HTTP请求
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
/* 参数解释:
1. sockfd: 套接字描述符,用于标识与客户端的连接
2. buffer: 接收数据的缓冲区
3. sizeof(buffer) - 1: 最大接收长度,预留1个字节给'\0'
4. 0: 标志位,使用默认行为
返回值n:
- 大于0:实际接收的字节数
- 等于0:连接已关闭
- 小于0:接收错误
*/
if (n > 0)
{
buffer[n] = 0; // 字符串结束符
// 2. 解析HTTP请求
HttpRequest req;
req.Deserialize(buffer); // 反序列化请求内容
req.Parse(); // 解析请求(获取方法、URL、版本等)
// 3. 读取请求的文件内容
std::string text;
bool ok = true;
text = ReadHtmlContent(req.file_path); // 读取请求的文件
if(text.empty()) // 文件不存在或读取失败
{
ok = false;
// 返回错误页面
std::string err_html = wwwroot + "/err.html";
text = ReadHtmlContent(err_html);
}
// 4. 构建HTTP响应
// 4.1 响应行
std::string response_line;
if(ok)
response_line = "HTTP/1.0 200 OK\r\n";
else
response_line = "HTTP/1.0 404 Not Found\r\n";
// 4.2 响应头
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size());
response_header += "\r\n";
response_header += "Content-Type: ";
response_header += SuffixToDesc(req.suffix); // 设置正确的MIME类型
response_header += "\r\n";
response_header += "Set-Cookie: name=haha&&passwd=12345"; // 设置Cookie
response_header += "\r\n";
// 4.3 空行
std::string blank_line = "\r\n";
// 4.4 组装完整响应(响应行+响应头+空行+响应体)
std::string response = response_line + response_header + blank_line + text;
// 5. 发送响应给客户端
send(sockfd, response.c_str(), response.size(), 0);
}
// 6. 关闭连接
close(sockfd);
}
// 线程运行函数
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 设置线程分离
ThreadData *td = static_cast<ThreadData *>(args);
td->svr->HandlerHttp(td->sockfd);
delete td;
return nullptr;
}
~HttpServer() {}
private:
Sock listensock_; // 监听socket
uint16_t port_; // 服务器端口
std::unordered_map<std::string, std::string> content_type; // 支持的内容类型映射
};
1.4 Socket.hpp-网络通信器
Socket.hpp
功能:
- TCP连接封装
- 地址绑定
- 端口监听
- 客户端连接处理
- 错误处理
#pragma once // 防止头文件重复包含
// 系统相关头文件
#include <iostream> // 标准输入输出
#include <string> // 字符串处理
#include <unistd.h> // UNIX标准函数定义
#include <cstring> // C字符串处理
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态
#include <sys/socket.h> // Socket接口
#include <arpa/inet.h> // IP地址转换函数
#include <netinet/in.h> // IP协议家族
#include "Log.hpp" // 日志系统
// 错误枚举定义
enum
{
SocketErr = 2, // Socket创建错误
BindErr, // 绑定错误
ListenErr, // 监听错误
};
// 监听队列长度
const int backlog = 10;
// Socket封装类
class Sock
{
public:
Sock() {}
~Sock() {}
public:
// 创建Socket
void Socket()
{
// 创建TCP Socket
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
// 创建失败,记录错误日志并退出
lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
// 设置Socket选项:地址重用
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
// 绑定端口
void Bind(uint16_t port)
{
// 创建并初始化地址结构
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; // IPv4
local.sin_port = htons(port); // 端口号
local.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
// 绑定地址和端口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 绑定失败,记录错误日志并退出
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
// 开始监听
void Listen()
{
// 启动监听
if (listen(sockfd_, backlog) < 0)
{
// 监听失败,记录错误日志并退出
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
// 接受连接
int Accept(std::string *clientip, uint16_t *clientport)
{
// 准备接收客户端地址信息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接受新连接
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
// 接受连接失败,记录警告日志
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
// 获取客户端IP地址
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
// 获取客户端端口号
*clientport = ntohs(peer.sin_port);
return newfd; // 返回新连接的文件描述符
}
// 连接服务器(客户端使用)
bool Connect(const std::string &ip, const uint16_t &port)
{
// 准备服务器地址信息
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
// 建立连接
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
// 连接失败
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true; // 连接成功
}
// 关闭Socket
void Close()
{
close(sockfd_);
}
// 获取Socket文件描述符
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // Socket文件描述符
};
1.5 HttpServer.cc-服务器启动器
HttpServer.cc
功能:
- 程序入口
- 参数解析
- 服务器初始化
- 智能指针管理
// 包含必要的头文件
#include "HttpServer.hpp" // HTTP服务器类定义
#include <iostream> // 标准输入输出
#include <memory> // 智能指针
#include <pthread.h> // POSIX线程库
#include "Log.hpp" // 日志系统
using namespace std;
int main(int argc, char *argv[])
{
// 检查命令行参数
if(argc != 2) // 要求必须提供端口号参数
{
exit(1); // 参数错误,退出程序
}
// 将命令行参数转换为端口号
uint16_t port = std::stoi(argv[1]); // 字符串转换为整数
// 创建HTTP服务器实例
// 以下是三种方式,注释掉的是不推荐的方式
// 方式1(不推荐):普通指针,需要手动管理内存
// HttpServer *svr = new HttpServer();
// 方式2(语法错误):unique_ptr的错误声明方式
// std::unique<HttpServer> svr(new HttpServer());
// 方式3(推荐):使用智能指针unique_ptr,自动管理内存
std::unique_ptr<HttpServer> svr(new HttpServer(port));
// 启动服务器
svr->Start(); // 开始监听和处理请求
// 程序正常退出
return 0;
}