跳到主要内容
C++ 手写 JSON 与 HTTP 协议实现 Web 计算器服务器 | 极客日志
C++
C++ 手写 JSON 与 HTTP 协议实现 Web 计算器服务器 基于 C++ 实现支持 JSON 序列化的 HTTP Web 服务器。文章详解 JSON 数据结构原理及 nlohmann/json 库用法,深入剖析 HTTP 协议报文结构(请求行、头、体),涵盖 GET 静态资源获取与 POST 动态数据处理逻辑。通过 Socket 编程、线程池管理及文件读写操作,构建了一个具备计算功能的简易 Web 服务器,展示了网络编程的核心流程与最佳实践。
灰度发布 发布于 2026/3/26 更新于 2026/4/23 1 浏览C++ 手写 JSON 与 HTTP 协议实现 Web 计算器服务器
本文前置知识:序列化与反序列化
在之前的博客中,详细介绍了 序列化 与 反序列化 的概念。对于使用 TCP 协议进行通信的双方,由于 TCP 是面向字节流的,在发送数据之前,通常需要定义一种结构化的数据来描述传输内容,并以此作为数据的容器。在 C++ 中,这种结构化数据通常表现为对象或结构体。然而,不能直接将结构体内存中对应的字节原样发送到另一端,因为直接传递内存字节会引发 字节序 和结构体 内存对齐 的问题。不同平台、不同编译器所遵循的内存对齐规则可能不同,这可能导致接收方在解析结构体字段时出现错误。
因此,需要借助 序列化 。序列化 是指将结构化的数据按照预定的规则转换为连续的字节流。其主要目的是屏蔽平台差异,使得位于不同平台的进程能够以统一的方式解析该字节流。序列化通常分为两种形式:文本序列化 与 二进制序列化。
文本序列化将结构化的数据转换为一个完整的字符串。字符串本身是以字符为单位的连续序列,每个字符通常占用一个字节,因此字符串本质上也是一个连续的字节流。由于字符串以字符为单位解析,不存在字节序问题。通信双方只需约定字符串的格式与编码方式,即可正确解析该字符序列,最终将连续的字节流还原为结构化的数据。
二进制序列化则直接发送数据在内存中的原始二进制序列,无需额外转换。这两种方式各有优劣:文本序列化直观、可读性高、便于调试;而二进制序列化发送的是二进制数据,人类难以直接阅读。文本序列化会将数据转换为字符形式,可能导致传输体积增大——例如整数 100000 在文本序列化中会被转换为 "100000" 占 6 个字节,而作为 int 类型的二进制序列化仅需 4 个字节。因此,二进制序列化在传输体积上通常更小。此外,文本序列化还需要对字符串进行解析以恢复原始数据,而二进制序列化的解析开销通常更低,因为它直接对应数据的原始二进制表示。
特性 文本序列化 (JSON/XML) 二进制序列化 (Protobuf/Thrift) 可读性 极高(肉眼可读) 低(十六进制乱码) 传输体积 较大(数字变字符,带大量引号) 极小(紧凑编码) 解析速度 较慢(需字符串扫描、词法解析) 极快(直接偏移寻址或位运算) 跨语言 完美(天然支持) 优秀(需编译 IDL 文件)
在上一篇博客中,手动实现了文本序列化,即将结构体各字段按一定格式拼接为完整字符串。之所以手动实现,是为了帮助大家理解序列化的基本原理,并为本文内容做铺垫。
然而在实际开发中,通常不需要从头实现序列化,可以使用成熟的第三方库来完成这项工作。这些库的实现通常更完善、更高效。本文将介绍的第一个主题——JSON,就是一种广泛应用的文本序列化格式。
JSON
首先,介绍一下什么是 JSON。JSON(JavaScript Object Notation)是一种轻量级、基于文本、人类可读的数据交换格式。JSON 源于 JavaScript,借鉴了其对象和数组的表示方法。但由于 JSON 本身是文本格式,且所表示的基本数据类型(如整型、布尔值等)在绝大多数编程语言中都得到支持,因此 JSON 并不局限于 JavaScript,而是能够被多种编程语言解析与生成。正因如此,JSON 不仅具备 跨平台 能力,还能实现 跨语言 的数据交换。
了解 JSON 的基本定义后,进一步探讨其本质。如上所述,JSON 实质上是一种文本序列化的方式。在此之前,曾手动实现过文本序列化,其核心原理是将结构体的各个字段按照特定格式拼接为一个完整的字符串。因此,JSON 的本质其实就是符合 JSON 规范(风格)的字符串。
理论上,只要清楚 JSON 格式的规范,就可以利用字符串操作函数手动拼接出符合 JSON 风格的字符串,而无需借助第三方库。字符串拼接本身并不复杂,因此自然引出一个疑问:相比手动实现,第三方库的优势究竟在哪里?如果仅实现序列化(即转换为 JSON 字符串),那么使用第三方库似乎并未显著减轻负担,因为序列化这一步本身并不困难。要回答这个问题,首先需要明确 JSON 风格字符串的具体形式,进而理解第三方库所承担的工作。
JSON 支持若干 基本数据类型,例如整型、浮点型和布尔型,也支持 字符串、对象 等复杂类型:
JSON 类型 C++ 对应类型 描述 Number int, double, floatJSON 不区分整数和浮点数,统一视为数字。 Boolean bool只有 true 和 false 两个字面值。 String std::string必须使用 双引号 包围,支持转义字符(如 \n, \t)。 Null nullptr / NULL表示空值或不存在,常用于可选字段。
需注意,基本数据类型(如整型、浮点型)可直接书写,而字符串类型必须用双引号括起来。
如果需要将一个对象或结构体的数据传递给另一端,在未接触 JSON 时,通常需要手动将其各字段拼接成字符串,再发送该字符串的字节流。而 JSON 可以直接表示对象,其方法是用一对大括号包裹内容,括号内是一个或多个 键值对。每个 键值对 中,键与值之间用冒号分隔,不同键值对之间用逗号分隔。
这里的键值对对应于结构体或对象的成员变量:键表示成员名称,值表示该成员的取值。这种表示方式不仅书写方便,也能直观体现对象结构及其属性值:
{ "age" : 20 , "sex" : "girl" , "height" : 160 }
需要注意的是,值可以是任意基本类型,但键必须是字符串类型。
对于数组,JSON 使用中括号表示,括号内为数组元素,各元素之间以逗号分隔。数组元素可以是基本类型,也可以是对象等复杂类型:
[ 100 , "bob" , { "id" : 1234 , "name" : "mike" } ]
了解 JSON 字符串的格式后,可以回应前文提出的问题:既然已知对象用花括号表示、键值对用逗号分隔,数组用中括号表示、元素间用逗号分隔,确实可以通过字符串拼接函数,将结构化数据转换为符合 JSON 格式的字符串。
然而,如果需要修改对象中某一字段的值,通常有两种做法:一是修改原始结构体的值,然后重新拼接整个字符串;二是直接修改已拼接好的字符串(在不改动原始数据的情况下)。第二种方式需要定位字符串中的特定字段,进行覆盖并调整后续字符位置,过程较为繁琐。此外,在序列化过程中,某些字符(如双引号、反斜杠等)需要进行转义处理,这也增加了手动实现的复杂度。
更重要的是,除了序列化,还需考虑反序列化——即将 JSON 字符串解析并还原为原始数据。根据上述 JSON 格式,反序列化需识别键值对、分离键与值,并将值转换回对应数据类型。若遇到嵌套对象(即对象中某个属性的值仍为对象),则需递归处理,实现难度显著增加:
{ "id" : 1234 , "name" : "bob" , "person" : { "id" : 125 , "name" : "kie" } }
如果序列化与反序列化均自行实现,那么在序列化时若遗漏逗号或括号,将给反序列化带来极大困难,甚至导致解析失败。
因此,引入第三方库显得十分必要。其优势不仅在于提供高效的序列化与反序列化功能,更在于它提供了一系列功能丰富的接口,用于直接操作 JSON 对象内部维护的结构化原始数据 。这些库通常通过一个与编程语言相对应的数据结构(在 C++ 中通常是一个 类对象 )来 映射和承载 解析后的 JSON 数据,同时提供成员函数来方便地进行相关操作。
在 C++ 中,常用的 JSON 库包括 json.hpp(即 nlohmann/json)。它提供的 JSON 对象可被视为一个容器,用于存储要发送或解析的 JSON 数据,并封装了丰富的操作方法,大大简化了 JSON 的处理流程。
这里需要注意,nlohmann/json 是一个第三方库,这意味着 C++ 标准库并不包含该库,因此需要自行引入。本文采用的方式是:获取官方 json.hpp 源代码文件,将其全部内容复制到 Linux 系统下的相应目录中并保存。当然,引入该库还有其它多种方法,在此不再赘述。
需要明确的是,该第三方库维护了一个类,该类可实例化为一个 JSON 对象。它不仅作为数据容器,还能以更灵活的方式支持我们对内部结构化数据的管理与维护。通过该对象提供的函数,可以直接操作数据项,而不需要去关心底层字符串的具体拼写格式。
首先关注其使用方法,即如何操作这个 json 对象。json 类的定义位于 json.hpp 中的 nlohmann 命名空间内,因此需要指定该命名空间,随后创建一个 json 对象。
json 对象最常见的数据类型是对象(object)和数组(array)。初始化一个 json 对象主要有两种方式。第一种是通过构造函数完成,由于 json.hpp 支持 C++11,可以使用列表初始化的语法。
如前所述,JSON 对象的内容由一系列键值对组成。在列表初始化中,可以直接向构造函数传递一系列 std::pair 对象,每个 pair 对应一个键值对。
#include "json.hpp"
#include <iostream>
int main () {
nlohmann::json j = {{"name" , "WZ" }, {"age" , 20 }, {"gender" , "girl" }};
std::string name = j["name" ];
std::string s = j.dump ();
return 0 ;
}
除了通过构造函数进行初始化,另一种更推荐的方式是直接使用赋值运算符。可以这样理解:若 json 对象存储的是 JSON 对象,其内部实际上维护了一个字典(即哈希表)。更详细的实现原理将在后文说明。
我们知道哈希表内部存储键值对,并重载了 [ ] 运算符。若哈希表中不存在指定的键,则会插入对应的键值对,从而完成初始化。这种方式不仅方便,也更符合 C++ 标准库容器的使用习惯,其效果与上述列表初始化相同,因此本人更推荐此种写法:
nlohmann::json j;
j["name" ] = "wz" ;
j["age" ] = 20 ;
j["gender" ] = "girl" ;
了解如何创建 json 对象后,下一步是进行序列化。json 类提供了 dump() 成员函数用于序列化,其返回类型为 std::string。因为 JSON 本质上是一个具有特定格式的字符串,而 dump() 的返回值正是该格式的字符串表示。该函数可接收一个整数参数,若不传递参数,则默认生成紧凑格式(compact)的字符串。紧凑格式是指所有键值对均在同一行内输出,键值对之间仅以逗号分隔,不包含换行与额外空格,因此可读性相对较低。
若向 dump() 传递一个整型参数,则输出的字符串会进行格式化:每个键值对单独占一行,并且该参数值表示每一级缩进的空格数。例如,若参数值为 2,则每对键值前会有 2 个空格。如果 JSON 对象中嵌套了其他对象或数组,内层元素会根据嵌套深度进一步增加缩进。
{
"name" : "WangZhe" ,
"stats" : {
"level" : 99 ,
"equipment" : [
"Sword" ,
"Shield"
]
}
}
通常不建议在序列化时添加缩进,因为缩进虽然提高了可读性,但也会引入额外的换行符和空格,从而增加字符串的体积。若 JSON 数据包含大量键值对或嵌套层次较深,这种体积增长会在网络传输等场景中带来额外开销。因此,一般情况下建议调用 dump() 时不传入参数。
接下来介绍如何初始化表示数组的 json 对象。初始化数组同样有两种方法:第一种仍然是通过构造函数的列表初始化,但此时传递的是值(而非键值对),构造函数会据此完成初始化:
#include "json.hpp"
#include <iostream>
int main () {
nlohmann::json j1 = {1 , 2 , 3 , 4 , 5 };
std::string s1 = j1. dump ();
std::cout << s1 << std::endl;
return 0 ;
}
第二种方式是使用 json 类提供的 push_back() 函数,该函数专用于向表示数组的 json 对象末尾添加元素。可将其简单理解为在内部维护的数组尾部插入元素,具体原理将在后文详细阐述。初始化完成后,同样可调用 dump() 进行序列化。
json 对象的功能不止于此。假设 json 对象内部存储的是一个对象或者数组,可以像操作标准库中的哈希表或者 vector 一样,通过 [ ] 运算符访问或修改其字段值:
#include "json.hpp"
#include <iostream>
int main () {
nlohmann::json j = {{"name" , "WZ" }, {"age" , 20 }, {"gender" , "girl" }};
std::string name = j["name" ];
std::cout << name << std::endl;
j["name" ] = "kiki" ;
std::cout << j["name" ] << std::endl;
std::string s = j.dump (2 );
std::cout << s << std::endl;
nlohmann::json j2 = {1 , 2 , 3 , 4 , 5 };
std::cout << j2[1 ] << std::endl;
j2[1 ] = 100 ;
std::cout << j2[1 ] << std::endl;
std::string s2 = j2. dump ();
std::cout << s2 << std::endl;
return 0 ;
}
最后介绍反序列化,即将 JSON 格式的字符串还原为 json 对象。json 类提供了静态成员函数 parse(),它接收一个 JSON 格式的字符串,在内部解析后存储到 json 对象中。此过程即反序列化——将连续的字节流还原为结构化的 json 对象,其内部保存的数据即为原始内容。
为了熟悉 parse() 的用法,编写如下代码进行验证。首先需要准备一个符合 JSON 语法的字符串。根据前面的介绍,JSON 对象由大括号包裹,数组由中括号包裹,键名与字符串值必须使用双引号。这些特殊字符在 C++ 字符串中需要使用转义字符表示,因此手动构造 JSON 字符串较为繁琐,例如:
std::string s = "{\"name\":\"WZ\",\"skills\":[\"C++\",\"Linux\"]}" ;
为此,C++11 引入了原始字符串字面量(raw string literal)语法:R"()"。其基本格式为:
R"delimiter( raw_characters )delimiter"
其中 R 指明该字符串为原始字符串,括号内为字符串内容,delimiter 为可选的分隔标识符。在原始字符串中,绝大多数字符(包括引号和换行)无需转义。仅当字符串内容本身包含 ")" 时,才需要在括号前添加一个自定义分隔符以避免歧义。
#include "json.hpp"
#include <iostream>
int main () {
std::string s = R"({"name":"WZ","age":18,"is_student":true,"gender":"female"})" ;
nlohmann::json j = nlohmann::json::parse (s);
std::cout << j["name" ] << std::endl;
std::cout << j["age" ] << std::endl;
std::cout << j["is_student" ] << std::endl;
std::cout << j["gender" ] << std::endl;
return 0 ;
}
原理 接下来介绍 JSON 的实现原理。基于上文的背景,json.hpp 库内部维护一个 json 类,该类包含两个核心成员变量:类型变量与值变量。其中,类型变量用于记录当前 json 对象所维护的原始数据类型,例如是一个对象、一个数组,还是一种基本数据类型。除了类型变量,类中还维护一个值变量,该值变量被实现为一个联合体。
#include <iostream>
#include <string>
#include <vector>
#include <map>
enum class value_t {
null, number_integer, string, array, object
};
class MyJson {
public :
union internal_value {
int64_t number_integer;
std::string* string;
std::vector<MyJson>* array;
std::map<std::string, MyJson>* object;
internal_value () : number_integer (0 ) {}
};
value_t m_type = value_t ::null;
internal_value m_value;
};
在该联合体中,基本数据类型(如整数)直接存储其值,而复杂数据类型(如字符串、数组等)则存储指针,以此减少 json 对象本身的内存占用。json 类的关键组成部分之一是其构造函数。该类提供了多个版本的构造函数,每个版本对应一种特定的数据类型。每个构造函数的主要职责是:将类型变量设置为对应的数据类型,并同时初始化值变量。
基于上文,对象和数组支持通过列表初始化进行构造。对于对象,列表初始化使用一系列 pair(二元组)来完成;对于数组,则直接列举各个元素的值。列表初始化的底层机制与 std::initializer_list 相关,它是一个标准库提供的模板类。
json 类定义了两个接收 std::initializer_list 的构造函数:一个接收 std::initializer_list<std::pair<std::string, Json>>,用于对象初始化;另一个接收 std::initializer_list<json>,用于数组初始化。
若列表初始化传入的是 pair 列表,则会调用接收 std::initializer_list<std::pair<std::string,json>> 的构造函数。该函数的执行过程如下:首先,编译器会在栈上构建一个临时的只读数组,其元素类型为 std::pair<std::string,json>;接着,std::initializer_list 内部会保存两个指针,分别指向该临时数组的起始与结束位置。在构造函数内部,首先将类型变量设为 object,并在堆上动态分配一个 std::map 作为值变量;之后,遍历临时数组,将每个 pair 插入该 std::map 中。
若传入的不是 pair 列表,则会调用接收 std::initializer_list<json> 的构造函数。此时,类型变量被设置为 array,并在堆上分配一个 std::vector<json> 作为值变量,随后将临时数组中的每个 json 元素依次插入该向量。
class MyJson {
MyJson (std::initializer_list<std::pair<std::string, json>> init) {
m_type = value_t ::object;
m_value.object = new std::map <std::string, json>();
for (auto it = init.begin (); it != init.end (); ++it) {
m_value.object->insert (*it);
}
}
MyJson (std::initializer_list<json> init) {
m_type = value_t ::array;
m_value.array = new std::vector <json>();
m_value.array->reserve (init.size ());
for (auto it = init.begin (); it != init.end (); ++it) {
m_value.array->push_back (*it);
}
}
MyJson (int value) {
m_type = value_t ::number_integer;
m_value.number_integer = value;
}
MyJson (const char * value) {
m_type = value_t ::string;
m_value.string = new std::string (value);
}
~MyJson () {
if (m_type == value_t ::string) {
delete m_value.string;
} else if (m_type == value_t ::array) {
delete m_value.array;
}
}
};
在了解 json 类的基本构造机制后,接下来介绍几个关键的成员函数——operator[] 运算符的重载。
operator[] 有两个重载版本:一个接收 std::string 类型参数,另一个接收整型参数。接收 std::string 的版本用于处理 JSON 对象(键值对结构)。此时,json 对象的值变量应为一个哈希表(或 std::map)。如果调用该运算符时,当前 json 对象为空(即类型为 null),则该运算符会先将其类型转为 object,并初始化值变量为空的哈希表,然后插入对应的键值对;若非空,则直接进行键值对的插入或访问。
接收整型参数的版本用于处理 JSON 数组。类似地,若当前 json 对象为空,运算符会将其类型转为 array,并初始化值变量为一个空的向量。此外,该版本还包含自动扩容逻辑:若访问的下标超出当前数组长度,向量会自动扩容至该下标加一的大小,新增位置将以默认构造的 json 对象(即 null 类型)填充。
json& operator [](const std::string& key) {
if (m_type == value_t ::null) {
m_type = value_t ::object;
m_value.object = new std::map <std::string, json>();
}
if (m_type != value_t ::object) {
throw std::domain_error ("JSON 类型不是 object,无法使用字符串 key 访问" );
}
return (*m_value.object)[key];
}
json& operator [](size_t index) {
if (m_type == value_t ::null) {
m_type = value_t ::array;
m_value.array = new std::vector <MyJson>();
}
if (m_type != value_t ::array) {
throw std::domain_error ("JSON 类型不是 array,无法使用索引访问" );
}
if (index >= m_value.array->size ()) {
m_value.array->resize (index + 1 );
}
return (*m_value.array)[index];
}
在了解了 operator[] 的基本访问逻辑后,需要注意其返回类型是 json 对象的引用。在代码中,常会书写如下语句:
此语句的底层执行逻辑如下:首先调用 operator[] 重载函数,函数内部检查 j 对象不为空且类型为 object(而非 array 或其他类型),随后找到键 "age" 对应的值。该值本身是一个 json 对象,函数返回其引用。然而,为了将 json 对象转换为 int 这样的基本类型,编译器会尝试进行隐式类型转换。具体地,当赋值运算符的左右操作数类型不匹配时,编译器会检查 json 类是否定义了相应的类型转换运算符,若已定义,则自动调用。因此,上述代码实际上依次调用了两个重载函数:operator[] 和 operator int()。
class MyJson {
public :
operator int () const {
if (m_type != value_t ::number_integer) {
throw std::runtime_error ("类型不匹配,无法转换为 int" );
}
return static_cast <int >(m_value.number_integer);
}
operator std::string () const {
if (m_type != value_t ::string) {
throw std::runtime_error ("类型不匹配,无法转换为 string" );
}
return *(m_value.string);
}
operator bool () const {
}
};
此语句的执行过程是:首先调用 operator[] 并返回一个 json 对象的引用。这个被引用的 json 对象作为赋值运算符的左操作数(属于自定义类型),随后会调用其赋值运算符 operator=。该类定义了多个重载版本的 operator=,其中一个接收 int 类型参数。该赋值运算符会首先检查当前 json 对象的类型是否匹配:如果匹配(例如原本就是整数类型),则直接修改其内部存储的值;如果不匹配,则需要先释放当前对象可能持有的资源(如堆内存),再将类型标签更新为目标类型,并初始化对应的值变量。
MyJson& operator =(int value) {
if (m_type == value_t ::number_integer) {
m_value.number_integer = value;
} else {
this ->destroy ();
m_type = value_t ::number_integer;
m_value.number_integer = value;
}
return *this ;
}
通过结合 operator[]、类型转换运算符以及赋值运算符的重载,json 类实现了灵活且直观的读写接口,同时在内部保证了类型安全与资源管理的正确性。
json.hpp 的底层设计在一定程度上模糊了数组与对象的界限。其 operator[] 不仅是一个访问器,更扮演了构造助手的角色。它利用 std::vector<json> 的默认构造特性,以 null 对象作为'内存粘合剂',实现了边访问边构造的灵活性,同时借助 my_type 成员确保了 C++ 层面的类型安全。
补充 至此,已从使用与原理两个层面解析了 JSON。在原理层面,并未详细解释 dump 与 parse 的具体实现原理,因为本文的重点在于'使用轮子而非造轮子'。适当了解轮子的构造,有助于更从容、得心应手地运用 JSON,但对其底层的理解也应适度——将 JSON 类的实现完全剖析清楚,反而可能收益有限。dump 与 parse 的具体实现较为复杂,感兴趣的读者可自行深入研究。
对于结构化数据(即 JSON 对象),可以调用 dump 函数将其序列化为符合 JSON 规范的字符串。该字符串是以字符为单位的字符序列,每个字符通常对应一个字节。dump 函数的作用正是将数据结构转化为连续的字符序列。然而,转换为字符序列后,还需经过一步额外处理才能通过网络发送:即通过特定编码将字符序列转换为字节序列。因此,这里补充说明一下编码的相关知识。
我们知道,计算机底层只能存储二进制序列。但现实生活中大部分信息需以字符串形式表示,因此需要将字符串中的各个字符映射为唯一的二进制值,即进行编码。早期最常见的编码是 ASCII 码,它使用一个字节为英文字母及特殊符号分配唯一的二进制值,其范围是 0~127。
随着计算机的发展,需要表示的字符不再仅限于英文,还包括中文、其他语言字符乃至表情符号等。一个字节已不足以表示如此多的字符,于是 UTF-8 编码应运而生。UTF-8 是当前最主流、最常用的编码方式,它能表示包括中文在内的多国语言,并且完全兼容 ASCII 码。
为了对全球各类字符进行编码,Unicode 标准为每个字符定义了一个唯一的'码点'(Code Point),相当于字符的身份证。UTF-8 则是一种将码点转换为 1 至 4 个字节的二进制序列的规则,使得计算机能够存储和处理这些字符。具体字符映射到几个字节,取决于其码点的大小:
码点范围 (十六进制) 字节数 字节模板 (二进制) 0000 0000 - 0000 007F1 0xxxxxxx (完全兼容 ASCII)0000 0080 - 0000 07FF2 110xxxxx 10xxxxxx0000 0800 - 0000 FFFF3 1110xxxx 10xxxxxx 10xxxxxx (大部分汉字在这)0001 0000 - 0010 FFFF4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
此时读者可能会产生疑问:当一个字符映射为多个字节时,会存在字节序(Endianness)问题,但为何在将文本序列化为字符串时,大多数编译器和平台使用 UTF-8 编码却不会遇到字节序问题?
原因在于 UTF-8 是一种面向字节的编码。尽管一个字符可能对应多个字节,但 UTF-8 始终以字节为单位进行解析,而非将多个字节作为一个整体来处理。其解析规则如下:
若某字节的最高位为 0,则该字节直接对应一个字符(即 ASCII 字符)。
若字符对应多个字节,则第一个字节称为'前导字节'。对于占用 n 个字节的字符(1 ≤ n ≤ 4),其前导字节的高 n 位为 1,第 n+1 位为 0,后续的每个辅助字节均以 10 开头。这种设计使得解析器能够明确识别字符的起始与边界,从而正确地将多个字节组合解析为一个字符。
字节序问题本质源于多字节整型在内存中的存储顺序,而 UTF-8 在本质上是一种字节流协议。由于 UTF-8 的前导字节已包含字符长度与边界信息,且解析过程是逐字节顺序进行的,因此它天然避免了因 CPU 大小端架构差异所引发的问题。
相对地,像 UTF-16 这类定长编码(每个字符固定对应 2 或 4 字节),由于需将多个字节作为一个整体解析,就会受到平台字节序的影响。
JSON 标准明确规定使用 UTF-8 作为其编码方式,这进一步确保了其在不同系统和环境间的兼容性与一致性。
HTTP
引入 在上文详细讲解了 JSON 之后,接下来将过渡到 HTTP 的内容。在具体介绍 HTTP 协议之前,仍然通过一个例子来引入。
大家平时应该都有使用浏览器上网的习惯。常常在浏览器中输入一个网址,用来打开或获取网页,甚至是观看视频等。但想过没有,在输入网址的背后,其实涉及客户端与服务端之间的通信原理。浏览器作为客户端,会与服务端进行通信,而最终呈现给我们的各种网页、视频等内容,都可以统称为'资源'。这些资源都是从服务端获取的,而 HTTP 正是一个应用层的通信协议。
要理解 HTTP 的原理,首先要从整个通信过程的起点说起,也就是在浏览器输入网址的那一刻。
原理 根据上文可知,获取网页、音视频等资源的过程,其背后实际基于客户端 - 服务器模型(Client-Server Model),即客户端与服务器之间的通信。现在,让我们从这一通信过程的起点开始讲解——也就是在浏览器中输入网址的那一刻。首先需要明确的是,由于该过程本质上是客户端与服务器之间的通信,因此通信双方必须依赖 IP 地址与端口号,才能确保数据准确发送到目标主机上的对应进程。IP 地址通常表示为点分十进制形式的字符串,端口号则为整数值。然而,在输入网址时,并不会手动输入 IP 地址和端口号,却在按下回车键之后,对应的网页、音视频等资源几乎立即呈现在浏览器中。这究竟是如何实现的呢?
这就需要先了解'网址'这一概念。网址是一种通俗的说法,其专业术语是 URL(Uniform Resource Locator,统一资源定位符)。一个完整的 URL 通常由以下几个部分组成:协议、域名、端口、路径、查询参数和片段。
https://www.example.com:443/music/list?id =1024&type =pop#comment
组成部分 示例内容 专业术语 协议 https://Scheme 域名 www.example.comDomain/Host 端口 :443Port 路径 /music/listPath 参数 ?id=1024&type=popQuery String 锚点 #commentFragment
URL 被称为'统一资源定位符',是因为在浏览器中所见的各种内容——无论是网页、图片还是视频——本质上都是资源,通常存放在服务器上。客户端(浏览器)向服务器发起请求以获取这些资源。为了准确定位资源,不仅需要知道服务器的 IP 地址和端口,还需明确资源在服务器上的具体位置,这正是路径所起的作用。因此,URL 实际上提供了一种统一的方法来定位网络上的资源。关于路径的具体细节,将在后文进一步展开说明。
以上简要说明了路径的作用,端口也熟悉其含义,而协议则对应通信双方所使用的应用层协议。那么,域名又是什么?接下来的内容将围绕域名展开讲解。
域名 实际上,在浏览器中可以直接输入 IP 地址来替代域名进行访问。既然如此,为什么大多数情况下仍使用域名呢?原因在于,IP 地址是一串点分十进制数字,对普通用户而言并不友好。如果每次访问网站都需要记忆和输入 IP 地址,将会非常不便。相比之下,域名更为直观,例如 www.baidu.com 能让人联想到百度网站,而不暴露其背后的 IP 地址信息。可以将域名比作'人名',而 IP 地址则相当于'身份证号'——显然,使用域名访问更加直观和方便。
然而,问题随之而来:从形式上看,域名与 IP 地址并无直接关联,但获取资源的本质是进程间通信,必须依赖 IP 地址。因此,域名必须通过某种方式转换为对应的 IP 地址。这一转换过程即是接下来要介绍的域名解析服务。
在进行 DNS 查询之前,系统会首先在本机缓存中查找是否有该域名映射的 IP 地址。如果之前曾在浏览器中访问过该域名对应的网站,浏览器可能会保留相应的缓存。若浏览器缓存未命中,则会查询操作系统缓存;若操作系统缓存也未命中,则会进一步查询磁盘上的 hosts 文件。hosts 文件中存储的内容是一组组'域名-IP
相关免费在线工具 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