Linux 网络编程实战:基于 C++ 实现 JSON+HTTP Web 计算器服务器
引入
在之前的学习中,我们了解了序列化与反序列化的概念。对于使用 TCP 协议进行通信的双方,由于 TCP 是面向字节流的,在发送数据之前,通常需要定义一种结构化的数据来描述传输内容。在 C++ 中,这种结构化数据通常表现为对象或结构体。然而,我们不能直接将结构体内存中对应的字节原样发送到另一端,因为直接传递内存字节会引发字节序和结构体内存对齐的问题。
因此,我们需要借助序列化。序列化是指将结构化的数据按照预定的规则转换为连续的字节流。其主要目的是屏蔽平台差异,使得位于不同平台的进程能够以统一的方式解析该字节流。文本序列化直观、可读性高,而二进制序列化传输体积更小。在实际开发中,我们通常不需要从头实现序列化,可以使用成熟的第三方库来完成这项工作。本文将介绍的第一个主题——JSON,就是一种广泛应用的文本序列化格式。
JSON 详解
JSON(JavaScript Object Notation)是一种轻量级、基于文本、人类可读的数据交换格式。它源于 JavaScript,但能跨语言解析。JSON 支持整型、浮点型、布尔型、字符串、对象及数组等基本数据类型。
nlohmann/json 库的使用
在 C++ 中,常用的 JSON 库包括 json.hpp(即 nlohmann/json)。它是一个单头文件库,无需编译,只需将其包含到项目中即可。
初始化与操作
json 类的定义位于 nlohmann 命名空间内。创建 json 对象主要有两种方式:
- 列表初始化:利用 C++11 特性,通过构造函数传递键值对。
#include "json.hpp"
#include <iostream>
using json = nlohmann::json;
int main() {
json j = {{"name", "WZ"}, {"age", 20}, {"gender", "girl"}};
std::cout << j.dump() << std::endl;
return 0;
}
- 赋值运算符:更推荐的方式,符合 C++ 标准库容器习惯。
json j;
j["name"] = "wz";
j["age"] = 20;
j["gender"] = "girl";
序列化与反序列化
json 类提供了 dump() 成员函数用于序列化,返回 std::string。若不传参数,生成紧凑格式;若传入整数,则按指定空格数格式化输出。
反序列化使用静态成员函数 parse()。C++11 引入了原始字符串字面量 R"()" 语法,方便处理包含特殊字符的 JSON 字符串。
std::string s = R"({"name":"WZ","age":18})";
json j = json::parse(s);
原理简析
json.hpp 内部维护一个 json 类,核心成员包括类型变量与联合体形式的值变量。联合体设计使得同一时刻只能表示一种数据类型。构造函数根据输入决定类型,例如接收 std::initializer_list<std::pair> 时初始化为对象,接收 std::initializer_list<json> 时初始化为数组。
重载的 operator[] 实现了灵活的访问接口:接收 string 参数处理对象,接收 size_t 参数处理数组。同时支持隐式类型转换,如 int age = j["age"]。
HTTP 协议基础
HTTP 作为应用层协议,定义了客户端与服务端交互的规则。其本质是对请求与响应报文的格式约束。
URL 与域名解析
URL(Uniform Resource Locator)由协议、域名、端口、路径等组成。浏览器输入网址后,需先通过 DNS 解析将域名转换为 IP 地址,才能建立 TCP 连接。
HTTP 报文结构
HTTP 是文本协议,请求报文包含请求行、请求头、空行和可选的请求正文。响应报文结构类似,包含响应行、响应头、空行和响应正文。
请求方法
常见的请求方法有 GET 和 POST。GET 用于获取资源,通常不带请求正文;POST 用于提交数据,携带请求正文。其他方法如 PUT、DELETE 等也可通过 POST 映射到虚拟路径实现。
状态码
- 2xx: 成功,如 200 OK。
- 3xx: 重定向,如 301、302。
- 4xx: 客户端错误,如 400 Bad Request、404 Not Found。
- 5xx: 服务器错误,如 500 Internal Server Error。
Web 服务器实现
基于上述理论,我们使用 C++ 实现一个简单的 Web 服务器,支持静态资源访问和简单的计算功能。
架构设计
服务器整体框架遵循固定模式:创建监听套接字,绑定 IP 与端口,进入监听状态。为了解耦连接接收与业务处理,引入线程池管理任务。
Socket 封装
我们将系统接口封装为 sock 类,管理文件描述符生命周期,提供 socket、bind、listen、accept 等方法,并集成日志模块。
Httpserver 类
Httpserver 类维护 sock 对象、IP、端口及监听标志。init 函数负责创建和绑定套接字,start 函数启动监听并进入循环接受连接。
class Httpserver {
public:
Httpserver(std::string _ip = "0.0.0.0", uint16_t _port = 80)
: ip(_ip), port(_port), islistening(false) {}
void init() {
listen_socket.socket();
listen_socket.bind(ip, port);
}
void start() {
listen_socket.listen();
if (islistening) return;
islistening = true;
threadpool& tp = threadpool::getinstance();
tp.start();
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
while (islistening) {
size_t client_fd = listen_socket.accept(&client, &client_len);
Task t(client_fd);
tp.push(t);
}
}
private:
uint16_t port;
std::string ip;
sock listen_socket;
bool islistening;
};
通信处理
读取请求
TCP 是面向字节流的,需要依据 HTTP 报文格式判断是否读取完整。HTTP 请求头结束于连续的两个 CRLF(\r\n\r\n)。我们封装 Get_HttpRequest 函数,循环读取直到找到分隔符,并根据 Content-Length 处理请求正文。
解析请求
定义 Http_Request 结构体存储解析后的字段。Deserialization 函数将请求行和请求头分割存入哈希表。
class Http_Request {
public:
std::unordered_map<std::string, std::string> headers;
std::string text, method, url, http_version;
bool Deserialization(std::string& head) {
// 分割请求头和请求行
// ... 省略具体分割逻辑 ...
// 使用 stringstream 解析请求行
std::stringstream ss(first_line);
ss >> method >> url >> http_version;
return true;
}
};
业务分发
根据 method 字段调用不同的处理函数:
- GET 请求:处理静态资源。根据 URL 拼接文件路径,查找文件后缀确定 MIME 类型,读取文件内容构建响应。
- POST 请求:处理动态业务。本例实现了一个计算器,解析请求体中的键值对(a, op, b),执行计算并返回结果。
void run() {
Http_Request hr;
if (!Get_HttpRequest(socketfd, hr)) {
close(socketfd); 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);
}
关键代码细节
静态资源处理
Http_Get_Handler 首先判断 URL 是否为根目录,若是则默认返回 index.html。否则拼接路径读取文件。若文件不存在,返回 404 页面。
std::string Http_Get_Handler(Http_Request& hr) {
std::string file_path;
if (hr.url == "/" || hr.url == "/index.html") {
file_path = path + "/index.html";
} else {
file_path = path + hr.url;
}
std::string body = read_file(file_path);
// 构建响应报文...
return res;
}
计算器逻辑
Http_Post_Handler 解析请求体,提取 a, op, b 三个参数。注意处理 URL 编码(如 %2B 转 +)。执行计算后构建 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"];
// 解码运算符
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;
}
结语
本文从 JSON 序列化原理入手,深入讲解了 HTTP 协议细节,并通过实战代码演示了如何从零构建一个支持多线程的 C++ Web 服务器。掌握这些底层机制,有助于更好地理解网络通信的本质。


