C++ 实现 JSON 与 HTTP 协议:从零构建 Web 计算器服务器
在之前的网络编程实践中,我们处理过 TCP 字节流传输。对于面向字节流的 TCP 协议,通信双方通常需要定义一种结构化的数据来描述传输内容。直接传递内存中的结构体字节会引发字节序和内存对齐的问题,不同平台和编译器的规则可能不一致。因此,我们需要借助序列化技术,将结构化数据转换为连续的字节流。
文本序列化(如 JSON)直观、可读性高,便于调试;而二进制序列化(如 Protobuf)体积更小、解析更快。在实际开发中,我们通常使用成熟的第三方库来完成这项工作。本文将介绍如何使用 C++ 结合 nlohmann/json 库处理 JSON 数据,并基于 HTTP 协议实现一个简易的 Web 服务器。
JSON 数据处理
JSON(JavaScript Object Notation)是一种轻量级、基于文本的数据交换格式。它源于 JavaScript,但已广泛应用于多种编程语言。JSON 本质上是一种符合特定规范的字符串,支持整型、浮点型、布尔型、字符串、对象和数组等类型。
在 C++ 中,常用的 JSON 库是 nlohmann/json(即 json.hpp)。它是一个头文件库,无需编译安装,只需将其包含在项目目录中即可。该库提供了一个 json 类对象,用于存储和操作 JSON 数据。
初始化与操作
创建 json 对象主要有两种方式。第一种是通过构造函数进行列表初始化,利用 C++11 特性传入键值对或元素列表:
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// 对象初始化
json j = {{"name", "WZ"}, {"age", 20}, {"gender", "girl"}};
// 数组初始化
json arr = {1, 2, 3, 4, 5};
更推荐的方式是使用赋值运算符,这更符合 C++ 标准库容器的使用习惯:
json j;
j["name"] = "wz";
j["age"] = 20;
j["gender"] = "girl";
序列化与反序列化
json 类提供了 dump() 成员函数用于序列化,返回类型为 std::string。若不传参数,默认生成紧凑格式;若传入整数参数,则按指定缩进格式化输出。
std::string s = j.dump(); // 紧凑格式
std::string s_pretty = j.dump(2); // 2 格缩进
反序列化使用静态成员函数 parse(),接收 JSON 格式的字符串并还原为 json 对象。为了简化字符串中转义字符的处理,C++11 引入了原始字符串字面量语法 R"()":
std::string raw_json = R"({"name":"WZ","age":18})";
json j = json::parse(raw_json);
底层原理简述
nlohmann/json 内部维护一个联合体来存储不同类型的数据,通过类型标签区分当前对象是对象、数组还是基本类型。operator[] 重载了字符串和整型访问,自动处理类型转换和容器初始化。这种设计使得 API 既灵活又直观,同时保证了类型安全。
HTTP 协议基础
HTTP 是基于 TCP 的应用层协议,定义了客户端与服务器之间的交互规则。理解其报文结构是实现 Web 服务器的关键。
URL 与域名解析
URL(统一资源定位符)由协议、域名、端口、路径、查询参数等组成。浏览器输入网址后,首先进行域名解析(DNS),将域名转换为 IP 地址,才能建立 TCP 连接。域名解析过程涉及本地缓存、Hosts 文件以及 DNS 服务器的迭代查询。
请求与响应报文
HTTP 请求报文由请求行、请求头、空行和可选的请求正文组成。请求行包含方法(GET/POST 等)、URL 和协议版本。
常见的请求方法包括 GET(获取资源)和 POST(提交数据)。GET 请求通常不带正文,数据放在 URL 查询参数中;POST 请求则携带正文,常用于表单提交或文件上传。
响应报文结构与请求类似,由响应行、响应头、空行和响应正文组成。状态码指示请求结果,如 200 OK、404 Not Found、500 Internal Server Error 等。
Web 服务器实现
基于上述理论,我们可以用 C++ 编写一个简单的 Web 服务器。整体架构遵循经典的 Socket 编程模式:创建监听套接字、绑定端口、接受连接、处理请求。
核心组件设计
为了代码清晰,我们将 Socket 操作封装在一个 sock 类中,采用 RAII 思想管理资源生命周期。主服务器类 Httpserver 负责启动监听和线程池调度。
class sock {
public:
int socketfd = -1;
~sock() { if (socketfd >= 0) ::close(socketfd); }
void socket() { /* ... */ }
void bind(std::string ip, uint16_t port) { /* ... */ }
void listen() { /* ... */ }
int accept(struct sockaddr_in* client, socklen_t* len) { /* ... */ }
private:
// ... 省略部分实现细节
};
线程池与任务处理
服务端需要并发处理多个客户端连接。为了避免频繁创建销毁线程的开销,引入线程池机制。主线程负责 accept 新连接,将连接句柄封装为 Task 任务放入队列,工作线程从队列获取任务执行具体的 HTTP 逻辑。
请求解析与路由
读取客户端数据时,需依据 HTTP 协议格式判断是否收到完整报文。请求头结束标志为连续的两个回车换行符 \r\n\r\n。对于 POST 请求,还需根据 Content-Length 头读取完整的请求正文。
解析完成后,根据请求方法分发处理逻辑:
- GET 请求:映射到文件系统,读取静态资源(HTML、图片等),设置对应的 MIME 类型。
- POST 请求:解析请求体中的键值对,执行业务逻辑(如本例中的计算器功能)。
void run() {
Http_Request hr;
bool get_result = Get_HttpRequest(socketfd, hr);
if (!get_result) return;
std::string res;
if (hr.method == "GET") {
res = Http_Get_Handler(hr);
} else if (hr.method == "POST") {
res = Http_Post_Handler(hr);
} else {
// 不支持的方法
close(socketfd);
return;
}
send(socketfd, res.c_str(), res.size(), 0);
close(socketfd);
}
业务逻辑示例:计算器
在 POST 处理器中,我们解析表单数据(如 a=10&op=+&b=20),执行计算并将结果以 HTML 形式返回。注意处理除零错误及非法运算符的情况。
bool process_calculation(std::unordered_map<std::string, std::string>& val, int& result) {
int a = std::stoi(val["a"]);
int b = std::stoi(val["b"]);
std::string op = val["op"];
// 处理 URL 编码的运算符
if (op == "%2B") op = "+"; // ...
switch (op[0]) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b == 0) return false;
result = a / b; break;
}
return true;
}
静态资源服务
GET 请求主要处理静态文件。通过 URL 路径拼接服务器根目录,查找文件是否存在。若存在,读取文件内容并构建 200 OK 响应;若不存在,返回 404 错误页面。文件扩展名用于确定 Content-Type,确保浏览器正确渲染。
总结
本文详细讲解了如何在 C++ 中使用 JSON 库处理数据交换,并基于 HTTP 协议实现了基础的 Web 服务器。通过封装 Socket 操作、引入线程池以及设计合理的请求路由机制,我们构建了一个具备静态资源服务和动态计算能力的服务器原型。掌握这些底层原理,有助于深入理解网络编程的核心逻辑,为后续学习 HTTPS、异步 IO 等高级主题打下坚实基础。


