跳到主要内容
基于 C++ 手写 HTTP 服务器:从请求解析到响应构建 | 极客日志
C++ 大前端
基于 C++ 手写 HTTP 服务器:从请求解析到响应构建 网络编程中 TCP 面向字节流特性需配合应用层协议解决边界问题,HTTP 是典型代表。本文基于 C++ 实现简易 HTTP 服务器,详解 Socket 封装、请求解析、文件读取及响应构造流程。通过对比 GET 与 POST 提交方式差异,剖析 HTTP 报文结构与浏览器交互机制,助读者掌握 Web 服务底层原理。
在网络编程基础中,我们了解到 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
当我们通过域名访问网站时,本质上并不是直接访问这个字符串,而是需要先将域名解析为对应的 IP 地址。
具体流程是:
客户端向本地 DNS 服务器发送解析请求。
若本地无缓存,依次查询根、顶级、权威域名服务器。
最终获取 IP 地址并返回给客户端。
拿到 IP 后,客户端通过 IP 地址 + 端口号 进行访问。浏览器中常省略端口号,是因为 HTTPS 协议默认使用 443 端口,HTTP 默认使用 80 端口,浏览器会隐式补全。
IP 地址负责定位主机,端口号负责定位主机上的具体服务,二者缺一不可。
HTTP 协议
HTTP 通信本质是在 TCP 之上,按照固定格式传输的一段'结构化字符串'。
从客户端角度看,浏览器发起请求时会构造符合规范的文本数据:
请求行 :包含方法(GET/POST)、资源路径、HTTP 版本。
请求报头 :多行 key:value 形式,携带额外信息,以 \r\n 分隔。
空行 :标志报头结束,后续为正文。
请求正文 :GET 请求通常为空,POST 请求则携带表单或 JSON 数据。
状态行 :HTTP 版本、状态码及描述(如 200 OK)。
响应报头 :说明数据类型、长度等。
空行 :分隔元数据和数据内容。
响应正文 :HTML、JSON 等资源。
HTTP 请求与响应结构
首行 :方法 + url + 版本
Header :键值对,冒号分割;每组属性间用 \r\n 分割,遇空行结束。
Body :空行后的内容,可为空。
HTTP 响应结构
首行 :版本号 + 状态码 + 状态码解释
Header :同上
Body :空行后的内容,如 HTML 页面。
HTTP 服务器实现 接下来我们来动手写一个能被浏览器访问的简易 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, url, 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 方法 <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 中包含敏感信息,且可能触发 404 错误(如果路径不匹配)。
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 请求信息的正文部分,相对更安全,适合传输敏感数据。
从 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