跳到主要内容C++ 手写 HTTP 服务器:从请求解析到响应构建全流程 | 极客日志C++大前端
C++ 手写 HTTP 服务器:从请求解析到响应构建全流程
C++ 手写 HTTP 服务器涉及 TCP 通信基础、HTTP 协议结构解析及代码实现。文章涵盖 URL 与 DNS 原理、请求响应报文格式、Socket 编程模型以及 GET 与 POST 方法差异。通过完整代码示例展示如何构建支持文件访问的简易 Web 服务器,帮助理解网络编程底层逻辑。
C++ 手写 HTTP 服务器:从请求解析到响应构建全流程
在网络编程中,TCP 是面向字节流的通信方式,本身不具备消息边界功能。要实现完整的网络通信,必须设计应用层协议。虽然我们可以自定义协议,但为了通用性,业界已有成熟的方案,比如 HTTP(超文本传输协议)。
常见应用层协议概览
| 协议名称 | 协议全称 | 默认端口 | 传输层协议 | 说明 |
|---|
| HTTP | 超文本传输协议 | 80 | TCP | 网页访问(明文) |
| HTTPS | 安全超文本传输协议 | 443 | TCP | 加密网页 |
| FTP | 文件传输协议 | 21 / 20 | TCP | 文件上传下载 |
| TFTP | 简单文件传输协议 | 69 | UDP | 简单传输 |
| SMTP | 邮件发送协议 | 25 | TCP | 发送邮件 |
| POP3 | 邮件接收协议 | 110 | TCP | 下载邮件 |
| IMAP | 邮件访问协议 | 143 | TCP | 管理邮件 |
| DNS | 域名系统 | 53 | UDP/TCP | 域名解析 |
| DHCP | 动态主机配置协议 | 67 / 68 | UDP | 自动分配 IP |
| Telnet | 远程登录协议 | 23 | TCP | 不安全远程登录 |
| SSH | 安全远程登录 | 22 | TCP | 加密远程连接 |
| SNMP | 网络管理协议 | 161 | UDP | 网络设备管理 |
| NTP | 网络时间协议 | 123 | UDP | 时间同步 |
认识 URL 与 DNS 解析
当我们通过浏览器访问网站时,输入的其实是域名。例如 https://www.baidu.com/。浏览器并不会直接通过这个字符串访问,而是需要先将其解析为对应的 IP 地址。
具体流程如下:
- 客户端(浏览器或操作系统)向本地 DNS 服务器发送解析请求。
- 如果本地 DNS 服务器没有缓存结果,它会向更高层级的 DNS 服务器发起查询:根域名服务器 -> 顶级域名服务器 -> 权威域名服务器。
- 最终找到该域名对应的 IP 地址并返回给客户端。
拿到 IP 地址后,客户端通过 IP 地址 + 端口号 建立连接。你可能注意到 HTTPS 链接中通常不显示端口号,这是因为 HTTPS 协议隐含了默认端口 443,就像生活中 110 代表报警电话一样,浏览器会自动补全,而非端口不存在。
IP 地址负责定位主机,端口号负责定位主机上的具体服务,二者缺一不可。
HTTP 协议结构
HTTP 通信本质上是在 TCP 之上,按照固定格式传输的一段'结构化字符串'。一次完整的 HTTP 通信包含请求报文和响应报文。
请求报文结构
- 请求行:包含请求方法(如 GET、POST)、资源路径以及 HTTP 版本。明确'我要做什么、访问哪里、用什么协议版本'。
- 请求报头:多行 key:value 形式,携带额外信息(如 Host、Content-Type)。以
\r\n 分隔。
- 空行:表示报头结束,后续内容即为正文。
- 请求正文:GET 请求通常为空,POST 请求则携带表单或 JSON 数据。
响应报文结构
- 状态行:包含 HTTP 版本、状态码及描述信息(如
200 OK)。
- 响应报头:说明返回数据的类型、长度等。
- 空行:分隔元数据和数据内容。
- 响应正文:客户端真正关心的部分,如 HTML、JSON 或图片数据。
HTTP 服务器实现
接下来我们尝试用 C++ 实现一个能被浏览器访问的简易 HTTP 服务器。
一、实现效果
启动服务器后,在浏览器输入 http://ip:port/ 即可访问我们自己写的网页:
二、基本原理
HTTP 服务器本质还是一个 TCP 服务器,只不过多了一步:解析请求 + 返回符合 HTTP 协议的数据。
socket → bind → listen → accept ↓ 创建线程 ↓ read → 解析 → write
三、核心代码思路
1. 读取浏览器请求
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
GET /index.html HTTP/1.1
Host: xxx
2. 解析请求行
std::stringstream ss(req_header[0]);
std::string method, url, version;
ss >> method >> url >> version;
3. 拼接文件路径
std::string path;
if (url == "/" || url == "/index.html")
path = "./wwwroot/index.html";
else
path = "./wwwroot" + url;
4. 读取网页内容
std::ifstream in(path);
while (std::getline(in, line)) {
content += line + "\n";
}
5. 构造 HTTP 响应
std::string response;
response = "HTTP/1.0 200 OK\r\n";
response += "Content-Length: " + std::to_string(content.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += content;
write(sockfd, response.c_str(), response.size());
6. 简单处理 404
if (!in.is_open()) {
std::string body = "<h1>404 Not Found</h1>";
response = "HTTP/1.0 404 Not Found\r\n";
response += "Content-Length: " + std::to_string(body.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += body;
}
HTTP 服务器完整代码
Socket.hpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <cstring>
enum Err {
SocketErr = 1,
BindErr,
ListenErr
};
class Socket {
public:
Socket() {}
void Init() {
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0) {
std::cout << "socket fail!!!" << std::endl;
exit(SocketErr);
}
}
void Bind(uint16_t port, std::string ip) {
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &server.sin_addr);
if (bind(sockfd_, (struct sockaddr *)&server, sizeof server) < 0) {
std::cout << "bind fail" << std::endl;
exit(BindErr);
}
}
void Listen() {
if (listen(sockfd_, 10) < 0) {
std::cout << "listen fail" << std::endl;
exit(ListenErr);
}
}
int Accept(uint16_t *client_port, std::string *client_ip) {
struct sockaddr_in client;
bzero(&client, sizeof client);
socklen_t len = sizeof(client);
int newfd = accept(sockfd_, (struct sockaddr *)&client, &len);
if (newfd < 0) {
return -1;
}
char ip[64];
inet_ntop(AF_INET, &client.sin_addr, ip, sizeof ip);
*client_port = ntohs(client.sin_port);
*client_ip = ip;
return newfd;
}
bool Connect(uint16_t server_port, std::string server_ip) {
struct sockaddr_in server;
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
if (connect(sockfd_, (struct sockaddr *)&server, sizeof(server)) < 0) {
return false;
}
return true;
}
~Socket() {
close(sockfd_);
}
private:
int sockfd_;
};
HttpServer.cc
#include <iostream>
#include <unistd.h>
#include "Socket.hpp"
#include <fstream>
#include <vector>
#include <sstream>
const std::string wwwroot = "./wwwroot";
class HttpServer;
class ThreadData {
public:
ThreadData(int sockfd, HttpServer *ts) : sockfd_(sockfd), ts_(ts) {}
public:
int sockfd_;
HttpServer *ts_;
};
class HttpServer {
public:
HttpServer(uint16_t port, std::string ip) : port_(port), ip_(ip) {}
void Init() {
listenfd_.Init();
listenfd_.Bind(port_, ip_);
listenfd_.Listen();
}
void start() {
while (1) {
uint16_t client_port;
std::string client_ip;
int sockfd = listenfd_.Accept(&client_port, &client_ip);
if (sockfd < 0) {
continue;
}
ThreadData *td = new ThreadData(sockfd, this);
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
static void *ThreadRun(void *arg) {
pthread_detach(pthread_self());
ThreadData *td = (ThreadData *)arg;
td->ts_->HttpHandler(td->sockfd_);
delete td;
return nullptr;
}
std::string ReadHtmlContent(std::string &htmlpath) {
std::string content;
std::ifstream in(htmlpath.c_str());
if (!in.is_open()) {
return "404";
}
std::string line;
while (std::getline(in, line)) {
content += line;
}
in.close();
return content;
}
void HttpHandler(int sockfd) {
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof buffer - 1);
std::string request;
if (s > 0) {
buffer[s] = 0;
std::cout << buffer << std::endl;
request = buffer;
std::vector<std::string> req_header;
while (!request.empty()) {
ssize_t pos = request.find("\r\n", 0);
if (pos == std::string::npos) {
break;
}
std::string line = request.substr(0, pos);
if (line.empty()) {
break;
}
req_header.push_back(line);
request.erase(0, pos + 2);
}
std::stringstream ss(req_header[0]);
std::string method;
std::string url;
std::string http_version;
ss >> method >> url >> http_version;
std::string path = url;
if (path == "/" || path == "/index.html") {
path = wwwroot + "/index.html";
} else {
path = wwwroot + url;
}
std::string text = ReadHtmlContent(path);
std::string response;
if (text == "404") {
std::string body = "<h1>404 Not Found</h1>";
response = "HTTP/1.0 404 Not Found\r\n";
response += "Content-Length: " + std::to_string(body.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += body;
} else {
response = "HTTP/1.0 200 OK\r\n";
response += "Content-Length: " + std::to_string(text.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += text;
}
write(sockfd, response.c_str(), response.size());
}
close(sockfd);
}
~HttpServer() {}
private:
Socket listenfd_;
uint16_t port_;
std::string ip_;
};
int main(int argc, char *argv[]) {
uint16_t port = std::stoi(argv[2]);
std::string ip = argv[1];
HttpServer *svr = new HttpServer(port, ip);
svr->Init();
svr->start();
return 0;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>index</title>
</head>
<body>
<h1>你是一个好人</h1>
<a href="http://localhost:8080/a/b/hello.html">下一页</a>
</body>
</html>
hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<h1>祝你幸福</h1>
<a href="http://localhost:8080/index.html">回到首页</a>
</body>
</html>
HTTP 的方法
其中最常用的就是 GET 和 POST 方法。我们通过现象来看看它们有什么不同。
GET 方法
<form action="http://localhost:8080/a/b/hello.html" method="GET">
银行卡账号:<input type="text" name="user"><br>
银行卡密码:<input type="password" name="pass"><br>
<input type="submit" value="登录">
</form>
使用 GET 方法提交表单后,参数会拼接到 URL 之后。这意味着敏感信息(如密码)可能会暴露在地址栏或日志中,且 URL 长度有限制。
POST 方法
<form action="http://localhost:8080/a/b/hello.html" method="POST">
银行卡账号:<input type="text" name="user"><br>
银行卡密码:<input type="password" name="pass"><br>
<input type="submit" value="登录">
</form>
使用 POST 方法时,提交的参数显示在 HTTP 请求信息的正文部分(Body),不会出现在 URL 中,相对更安全,适合传输敏感数据。
总结
从之前的 TCP 通信,到现在亲手实现一个 HTTP 服务器,我们已经了解了:
- 一个请求是怎么被服务器解析的?
- 浏览器和服务器到底在'说什么'?
- GET 和 POST 的区别是什么?
总而言之,HTTP 不再是黑盒,而只是一个'有规则的字符串协议'。理解这一点,对深入掌握网络编程至关重要。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online