一、网络编程基础
- 计算机网络体系结构
OSI 七层模型
OSI(Open Systems Interconnection)七层模型是一个理论上的网络通信框架,由国际标准化组织(ISO)提出。它将网络通信分为七个层次,每一层都有特定的功能和协议:
本文详细介绍了 C++ 网络编程的核心知识,涵盖 OSI 与 TCP/IP 模型、Socket 基础、TCP/UDP 协议特性、I/O 多路复用技术、并发编程模型及网络安全等内容。文章提供了丰富的代码示例,包括基础 API 使用、HTTP 服务器开发、自定义协议设计及 Boost.Asio 和 Protobuf 等第三方库的实践,旨在帮助开发者掌握高性能网络编程技能。

OSI(Open Systems Interconnection)七层模型是一个理论上的网络通信框架,由国际标准化组织(ISO)提出。它将网络通信分为七个层次,每一层都有特定的功能和协议:
TCP/IP 模型是实际应用中广泛使用的网络协议栈,由四层组成:
GET(获取资源)、POST(提交数据)、PUT(更新资源)、DELETE(删除资源)。192.168.1.1。2001:0db8::ff00:0042。Socket(套接字)是网络通信的基本操作单元,是支持 TCP/IP 协议的网络通信的基本操作单元。它是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字由一个 IP 地址和一个端口号唯一标识。
Socket 地址结构是用来表示网络通信中端点地址的数据结构。在 C++ 网络编程中,最常用的是 sockaddr 及其相关结构。
sockaddr 是一个通用的地址结构,定义如下:
struct sockaddr {
unsigned short sa_family; // 地址家族,如 AF_INET
char sa_data[14]; // 协议地址
};
对于 IPv4 地址,更常用的是 sockaddr_in 结构:
struct sockaddr_in {
short sin_family; // 地址家族,通常为 AF_INET
unsigned short sin_port;// 端口号
struct in_addr sin_addr;// IP 地址
unsigned char sin_zero[8];// 填充字段,通常置 0
};
struct in_addr {
unsigned long s_addr; // IPv4 地址(32 位)
};
对于 IPv6 地址,使用 sockaddr_in6 结构:
struct sockaddr_in6 {
u_int16_t sin6_family; // 地址家族,AF_INET6
u_int16_t sin6_port; // 端口号
u_int32_t sin6_flowinfo;// IPv6 流信息
struct in6_addr sin6_addr;// IPv6 地址
u_int32_t sin6_scope_id;// 接口范围 ID
};
struct in6_addr {
unsigned char s6_addr[16]; // IPv6 地址(128 位)
};
网络字节序是大端序(Big-Endian),而主机字节序可能是大端序或小端序。因此需要进行字节序转换。
ntohl() - 网络字节序转主机字节序(32 位)
uint32_t ntohl(uint32_t netlong);
示例:
uint32_t net_ip = 0x0100007F; // 127.0.0.1 的网络字节序
uint32_t ip = ntohl(net_ip);
ntohs() - 网络字节序转主机字节序(16 位)
uint16_t ntohs(uint16_t netshort);
示例:
uint16_t net_port = 0x901F; // 8080 的网络字节序
uint16_t port = ntohs(net_port);
htonl() - 主机字节序转网络字节序(32 位)
uint32_t htonl(uint32_t hostlong);
示例:
uint32_t ip = 0x7F000001; // 127.0.0.1
uint32_t net_ip = htonl(ip);
htons() - 主机字节序转网络字节序(16 位)
uint16_t htons(uint16_t hostshort);
示例:
uint16_t port = 8080;
uint16_t net_port = htons(port);
inet_ntop() - 将网络字节序转换为点分十进制字符串(支持 IPv4 和 IPv6)
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
示例:
struct in_addr addr;
addr.s_addr = 0x0100007F;
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);
inet_pton() - 将点分十进制字符串转换为网络字节序(支持 IPv4 和 IPv6)
int inet_pton(int af, const char* src, void* dst);
示例:
const char* ip_str = "127.0.0.1";
struct in_addr addr;
inet_pton(AF_INET, ip_str, &addr);
inet_ntoa() - 将网络字节序的 32 位整数转换为点分十进制字符串
char* inet_ntoa(struct in_addr in);
示例:
struct in_addr addr;
addr.s_addr = 0x0100007F; // 127.0.0.1 的网络字节序
char* ip_str = inet_ntoa(addr); // 返回 "127.0.0.1"
inet_addr() - 将点分十进制 IP 字符串转换为网络字节序的 32 位整数
unsigned long inet_addr(const char* cp);
示例:
const char* ip_str = "127.0.0.1";
unsigned long ip = inet_addr(ip_str); // 返回网络字节序
TCP 协议在传输数据前需要通过三次握手建立连接:
TCP 通过以下机制确保数据的可靠传输:
TCP 通过滑动窗口机制实现流量控制:
socket() 函数用于创建一个套接字,它是网络通信的基础。在 C++ 中通常使用 <sys/socket.h> 头文件中的 socket() 函数。
int socket(int domain, int type, int protocol);
成功时返回套接字描述符,失败返回 -1。
bind() 函数将套接字与特定的 IP 地址和端口号绑定。
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
对于 IPv4,通常使用 sockaddr_in 结构体:
struct sockaddr_in {
short sin_family; // AF_INET
unsigned short sin_port;// 端口号 (网络字节序)
struct in_addr sin_addr;// IP 地址
char sin_zero[8]; // 填充
};
listen() 函数使套接字进入被动监听模式,等待客户端连接请求。
int listen(int sockfd, int backlog);
成功返回 0,失败返回 -1。调用 listen() 后,套接字变为被动套接字。
accept() 函数从已完成连接队列中取出第一个连接请求,创建一个新的套接字用于与客户端通信。
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
成功返回新的套接字描述符 (用于与客户端通信),失败返回 -1。原监听套接字继续等待其他连接请求。
connect() 是用于建立网络连接的函数,通常在客户端使用。它将套接字与远程服务器的地址和端口关联起来。
函数原型:
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
参数说明:
sockfd: 套接字描述符,由 socket() 函数创建。addr: 指向目标服务器地址结构的指针,通常是 sockaddr_in 或 sockaddr_in6。addrlen: 地址结构的长度。返回值:
0,失败返回 -1 并设置 errno。典型用法:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
exit(EXIT_FAILURE);
}
send() 用于通过已连接的套接字发送数据。
函数原型:
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
参数说明:
sockfd: 已连接的套接字描述符。buf: 指向要发送数据的缓冲区。len: 要发送的数据长度(字节数)。flags: 可选标志,通常设为 0(如 MSG_DONTWAIT 表示非阻塞发送)。返回值:
-1 并设置 errno。注意事项:
len,需要检查并处理部分发送的情况。EAGAIN 或 EWOULDBLOCK 错误。典型用法:
const char* msg = "Hello Server";
ssize_t bytes_sent = send(sockfd, msg, strlen(msg), 0);
if (bytes_sent == -1) {
perror("send failed");
}
recv() 用于从已连接的套接字接收数据。
函数原型:
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
参数说明:
sockfd: 已连接的套接字描述符。buf: 用于存储接收数据的缓冲区。len: 缓冲区的最大容量。flags: 可选标志(如 MSG_WAITALL 表示等待所有数据到达)。返回值:
0 表示连接已关闭,失败返回 -1 并设置 errno。注意事项:
len,需多次调用 recv 读取完整数据。EAGAIN 或 EWOULDBLOCK 错误。典型用法:
char buffer[1024];
ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
perror("recv failed");
} else if (bytes_received == 0) {
printf("Connection closed by peer\n");
} else {
buffer[bytes_received] = '\0'; // 添加字符串终止符
printf("Received: %s\n", buffer);
}
总结:
connect 用于建立连接,send 和 recv 是面向连接的数据传输函数(如 TCP)。阻塞 I/O 是指当程序执行 I/O 操作时,如果数据没有准备好(例如网络数据未到达或文件未读取完成),程序会一直等待(阻塞),直到数据准备好并完成操作后才继续执行后续代码。
特点:
read(), write(), accept() 等默认都是阻塞模式示例:
int n = read(socket_fd, buffer, sizeof(buffer)); // 会阻塞直到数据到达
非阻塞 I/O 是指当程序执行 I/O 操作时,如果数据没有准备好,函数会立即返回一个错误(通常是 EWOULDBLOCK 或 EAGAIN),而不是等待数据准备就绪。
特点:
fcntl() 设置 O_NONBLOCK 标志示例:
fcntl(socket_fd, F_SETFL, O_NONBLOCK); // 设置为非阻塞模式
int n = read(socket_fd, buffer, sizeof(buffer)); // 立即返回
if (n < 0 && errno == EWOULDBLOCK) {
// 数据尚未准备好
}
在网络编程中,非阻塞 I/O 通常与 I/O 多路复用(select/poll/epoll)结合使用,以实现高性能的网络服务器。
多路复用技术是一种允许单个线程或进程同时监控多个文件描述符 (fd) 的机制,用于检测哪些 fd 可读、可写或出现异常。这样可以避免为每个连接创建单独的线程,提高服务器处理并发连接的能力。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval* timeout);
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大 fd 数 | 有限 (FD_SETSIZE) | 无限制 | 无限制 |
| 效率 | O(n) | O(n) | O(1) |
| 触发方式 | LT | LT | LT/ET |
| 内核支持 | 所有平台 | 所有平台 | Linux |
| 内存拷贝 | 每次调用都需要 | 同 select | 仅一次 |
std::thread(C++11 起)创建线程。std::mutex)以避免数据竞争。fork() 系统调用创建子进程(Unix/Linux)。CreateProcess() API。| 特性 | 多线程 | 多进程 |
|---|---|---|
| 资源占用 | 低(共享内存) | 高(独立内存) |
| 通信效率 | 高(直接共享变量) | 低(需 IPC 机制) |
| 容错性 | 差(线程崩溃影响全局) | 强(进程隔离) |
| 适用场景 | I/O 密集型、高并发任务 | CPU 密集型、需高稳定性任务 |
线程池是一种多线程处理形式,它维护一组线程,等待监督管理者分配可并发执行的任务。线程池避免了频繁创建和销毁线程的开销,提高了系统性能。
class ThreadPool {
public:
ThreadPool(size_t);
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;
~ThreadPool();
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};
异步 I/O 是一种非阻塞的 I/O 操作方式,允许程序在 I/O 操作进行时继续执行其他任务,当 I/O 操作完成时会收到通知。
auto future = std::async(std::launch::async, [](){
// I/O 操作
return result;
});
// 其他工作
auto result = future.get(); // 获取结果
boost::asio::io_service io;
boost::asio::ip::tcp::socket socket(io);
socket.async_connect(endpoint, [](const boost::system::error_code& ec){
// 连接完成处理
});
io.run(); // 运行事件循环
将异步 I/O 的回调处理放在线程池中执行,可以:
// 伪代码示例
async_io_operation([&pool](Result result){
pool.enqueue([result]{ // 处理 I/O 结果 });
});
HTTP 请求解析是指服务器接收并解析客户端发送的 HTTP 请求的过程。一个 HTTP 请求通常由以下几部分组成:
请求头:包含多个键值对,提供关于请求的附加信息
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html
请求行:包含请求方法(GET、POST 等)、请求的 URI 和 HTTP 协议版本
GET /index.html HTTP/1.1
在 C++ 中,解析 HTTP 请求通常涉及:
HTTP 响应构建是指服务器创建并发送回客户端的 HTTP 响应。一个 HTTP 响应包含:
响应头:包含多个键值对,提供关于响应的元信息
Content-Type: text/html
Content-Length: 1234
状态行:包含协议版本、状态码和状态描述
HTTP/1.1 200 OK
在 C++ 中构建 HTTP 响应通常包括:
示例响应构建代码片段:
std::string BuildResponse(int status, const std::string& content) {
std::ostringstream response;
response << "HTTP/1.1 " << status << " OK\r\n";
response << "Content-Type: text/html\r\n";
response << "Content-Length: " << content.length() << "\r\n";
response << "\r\n";
response << content;
return response.str();
}
静态文件服务器是一种专门用于提供静态内容(如 HTML、CSS、JavaScript、图片等)的网络服务器。它不需要处理动态内容生成,而是直接返回存储在服务器上的文件。
../ 等字符访问服务器根目录之外的文件。sendfile 系统调用可以避免用户空间和内核空间之间的数据拷贝。void handle_request(const Request& req, Response& res) {
// 验证路径安全
if (!is_path_safe(req.path)) {
res.status = 403;
return;
}
// 检查文件是否存在
if (!file_exists(req.path)) {
res.status = 404;
return;
}
// 读取文件内容
auto content = read_file(req.path);
// 设置响应头
res.headers["Content-Type"] = get_mime_type(req.path);
res.headers["Content-Length"] = std::to_string(content.size());
// 发送响应
res.body = std::move(content);
res.status = 200;
}
协议头是网络通信中位于消息前部的固定或可变长度的数据块,主要包含控制信息。常见设计要素:
0xACBF),用于快速识别协议有效性v1.0 用 0x01 表示)0x01=请求,0x02=响应)典型二进制协议头示例(14 字节):
| 魔数 (2B) | 版本 (1B) | 类型 (1B) | 序列号 (4B) | 长度 (4B) | 保留 (2B) |
消息体承载实际业务数据,设计要点:
[userID][name][age]Body Length 字段保留字段,版本号支持升级int32 从 4 的倍数地址开始)注:实际设计中需配合
字节填充(padding)和位域(bit field)优化空间
序列化(Serialization)是将数据结构或对象转换为可存储或传输的格式(如字节流)的过程。反序列化(Deserialization)则是将序列化后的数据恢复为原始数据结构或对象的过程。
手动实现:通过重载 << 和 >> 运算符或编写专门的序列化函数。
class MyClass {
public:
int a;
std::string b;
// 序列化
void serialize(std::ostream& os) const {
os << a << " " << b;
}
// 反序列化
void deserialize(std::istream& is) {
is >> a >> b;
}
};
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// 序列化
MyClass obj;
json j;
j["a"] = obj.a;
j["b"] = obj.b;
std::string serialized = j.dump();
// 反序列化
auto j2 = json::parse(serialized);
MyClass obj2;
obj2.a = j2["a"];
obj2.b = j2["b"];
SSL (Secure Sockets Layer) 和 TLS (Transport Layer Security) 是用于在计算机网络中提供安全通信的加密协议。它们通过在传输层和应用层之间插入一个安全层来工作,确保数据在传输过程中的机密性、完整性和身份验证。
OpenSSL 是一个开源的 SSL/TLS 实现库,提供加密、证书管理和安全通信功能,广泛应用于 C/C++ 网络编程中。
openssl 命令用于生成证书和测试。#include <openssl/ssl.h>
#include <openssl/err.h>
// 初始化 OpenSSL 库
SSL_library_init();
SSL_load_error_strings();
// 创建 SSL 上下文
SSL_CTX* ctx = SSL_CTX_new(TLS_server_method());
// 加载证书和私钥
SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM);
// 创建 SSL 对象并绑定套接字
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockfd);
// 执行 TLS 握手
SSL_accept(ssl); // 服务器端
// 或 SSL_connect(ssl); // 客户端
// 安全数据传输
SSL_write(ssl, data, len);
SSL_read(ssl, buffer, sizeof(buffer));
// 清理资源
SSL_free(ssl);
SSL_CTX_free(ctx);
SSL_CTX_new():创建 SSL 上下文,指定协议版本(如 TLS_server_method)。SSL_new():基于上下文创建 SSL 会话对象。SSL_set_fd():将 SSL 对象与 TCP 套接字绑定。SSL_accept()/SSL_connect():分别用于服务器和客户端握手。SSL_read()/SSL_write():替代常规的 recv()/send() 进行加密通信。if (SSL_get_error(ssl, ret) == SSL_ERROR_SSL) {
ERR_print_errors_fp(stderr); // 打印 OpenSSL 错误堆栈
}
验证证书链:
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
SSL_CTX_load_verify_locations(ctx, "ca.crt", NULL);
生成自签名证书:
openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -days 365
SSL_free)。CRYPTO_set_locking_callback() 设置锁回调。SSL_CTX_set_session_cache_mode)。协议配置:禁用不安全选项(如 SSLv3):
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3);
数据加密是指将明文数据通过特定的算法转换为密文的过程,目的是保护数据的机密性,防止未经授权的访问或泄露。
身份认证是验证用户或系统身份的过程,确保通信双方的真实性。
高并发处理策略是指在网络编程中,为了应对大量客户端同时请求服务而采取的一系列技术和方法。以下是几种常见的高并发处理策略:
select、poll、epoll(Linux)或 kqueue(BSD)等机制,监控多个文件描述符(如套接字)的状态变化。async/await)实现非阻塞 I/O 操作,避免线程阻塞。这些策略可以单独或组合使用,具体选择需根据应用场景和性能需求权衡。
网络 I/O 性能调优是指通过优化网络输入输出操作来提高程序的网络通信效率。以下是一些关键的调优方法:
setsockopt 函数设置 SO_RCVBUF 和 SO_SNDBUF 选项来调整缓冲区大小。fcntl 函数设置套接字为非阻塞模式。select、poll 或 epoll 等机制可以同时监控多个套接字的状态,减少线程数量。epoll 在 Linux 上性能较高,适合高并发场景。sendfile 或 splice 等系统调用避免数据在用户空间和内核空间之间的多次拷贝。writev 和 readv 等函数进行批量读写,减少系统调用次数。TCP_NODELAY 选项)可以减少小数据包的延迟。这些方法需要根据具体应用场景和性能瓶颈进行选择和组合。
socket() 函数创建一个套接字,指定协议族(如 AF_INET)和类型(如 SOCK_STREAM 表示 TCP)。bind() 函数将套接字绑定到特定的 IP 地址和端口号。通常使用 INADDR_ANY 表示接受任意网络接口的连接。listen() 函数使套接字进入被动监听状态,等待客户端连接。可以指定最大连接队列长度。accept() 函数接受客户端的连接请求。该函数会阻塞直到有客户端连接,并返回一个新的套接字用于与该客户端通信。recv() 和 send() 函数与客户端进行数据交换。服务器可以循环接收和发送消息。close() 或 closesocket() 关闭套接字,释放资源。socket() 函数创建套接字。connect() 函数连接到服务器的 IP 地址和端口号。send() 和 recv() 函数与服务器进行通信。客户端可以发送消息并接收服务器的回复。close() 或 closesocket() 关闭套接字。// 服务器端伪代码
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_socket, 5);
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_len);
recv(client_socket, buffer, sizeof(buffer), 0);
send(client_socket, "Hello Client", strlen("Hello Client"), 0);
close(client_socket);
close(server_socket);
// 客户端伪代码
int client_socket = socket(AF_INET, SOCK_STREAM, 0);
connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
send(client_socket, "Hello Server", strlen("Hello Server"), 0);
recv(client_socket, buffer, sizeof(buffer), 0);
close(client_socket);
select()/poll()。recv() 会一直等待数据到达。文件传输系统是网络编程中常见的应用,用于在不同主机之间高效、可靠地传输文件。以下是实现文件传输系统的关键要素:
// 简单示例框架
class FileTransfer {
public:
void sendFile(const std::string& filename);
void receiveFile(const std::string& savePath);
private:
bool validateFile(const std::string& path);
void sendChunk(const char* data, size_t size);
void receiveChunk(char* buffer, size_t size);
};
实现文件传输系统时,需要特别注意网络字节序、平台兼容性以及异常处理等问题。
Boost.Asio 是 Boost 库中用于网络和底层 I/O 编程的跨平台 C++ 库,提供异步 I/O 模型,支持 TCP、UDP、定时器、文件描述符等操作。其核心基于Proactor 设计模式,通过事件驱动机制高效处理并发任务。
tcp::socket:TCP 通信端点。udp::socket:UDP 通信端点。Resolver
将主机名和端口解析为端点(endpoint):
tcp::resolver resolver(io);
auto endpoints = resolver.resolve("example.com", "80");
Socket 类
boost::asio::ip::tcp::socket socket(io);
io_context
事件调度中心,管理 I/O 操作和回调。所有异步操作需通过 io_context::run() 执行。
boost::asio::io_context io;
io.run(); // 启动事件循环
void async_connect(boost::asio::ip::tcp::socket& socket, const boost::asio::ip::tcp::resolver::results_type& endpoints) {
boost::asio::async_connect(
socket, endpoints,
[](boost::system::error_code ec, const auto&) {
if (!ec) std::cout << "Connected!\n";
});
}
boost::asio::steady_timer timer(io, std::chrono::seconds(1));
timer.async_wait([](auto ec) {
if (!ec) std::cout << "Timeout!\n";
});
通过 boost::system::error_code 捕获异常:
socket.async_read_some(..., [](error_code ec, size_t length) {
if (ec == boost::asio::error::eof) std::cerr << "Connection closed\n";
});
io_context 可多线程调用 run(),但单个对象(如 socket)需同步访问。std::shared_ptr 管理异步操作中的对象生命周期。Protobuf(Protocol Buffers)是 Google 开发的一种高效的数据序列化格式。它可以将结构化数据转换为二进制格式,用于网络传输或存储。与 XML 和 JSON 等文本格式相比,Protobuf 序列化后的数据更小、解析速度更快。
.proto 文件定义数据结构message Person {
required string name = 1;
optional int32 id = 2;
repeated string email = 3;
}
protoc --cpp_out=. person.proto
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.add_email("[email protected]");
std::string serialized_data;
person.SerializeToString(&serialized_data);
Person new_person;
new_person.ParseFromString(serialized_data);

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online