跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表

目录

  1. Linux TCP 服务器开发:Echo 实现、并发优化与安全实践
  2. 一、基础篇:实现一个能跑的 Echo 服务器
  3. 1.1 先解决两个核心问题:绑定与连接
  4. 1.2 Echo 客户端与服务器完整实现
  5. 二、并发篇 1:多进程版本 Echo 服务器
  6. 2.1 多进程核心逻辑与代码
  7. 2.2 解决僵尸进程问题
  8. 方案 1:注册信号处理函数(推荐)
  9. 方案 2:创建孙子进程(孤儿进程)
  10. 2.3 多进程模型的小细节:获取客户端 IP
  11. 2.4 多进程模型的痛点
  12. 三、并发篇 2:多线程版本服务器
  13. 3.1 多线程核心逻辑与代码
  14. 3.2 线程与文件描述符的两个关键问题
  15. 问题 1:进程打开的 FD,线程能看到吗?
  16. 问题 2:线程敢不敢关闭自己不需要的 FD?
  17. 3.3 短服务 vs 长服务
  18. 四、并发篇 3:线程池版本服务器
  19. 4.1 线程池核心设计
  20. 4.2 线程池核心代码(简化版)
  21. 五、扩展篇:远程命令执行服务器(附安全防护)
  22. 5.1 远程命令执行核心逻辑
  23. 5.2 安全防护:白名单机制
  24. 5.3 白名单核心代码
  25. 六、总结
C

Linux TCP 服务器开发:Echo 实现、并发优化与安全实践

Linux TCP 服务器开发实战,涵盖基础 Socket 编程、多进程多线程及线程池并发模型优化,并针对远程命令执行场景提供白名单安全防护方案。重点解析 bind/connect 参数、僵尸进程处理、线程资源管理及任务队列设计,确保高并发下的稳定性与安全性。

292440837发布于 2026/2/80 浏览
Linux TCP 服务器开发:Echo 实现、并发优化与安全实践

Linux TCP 服务器开发:Echo 实现、并发优化与安全实践

Linux 下 TCP 服务器开发是后端核心技能之一。本文从基础 Socket 编程入手,逐步实现 Echo 服务器,并通过多进程、多线程、线程池优化并发能力,最后扩展到远程命令执行场景并补充安全防护方案。

一、基础篇:实现一个能跑的 Echo 服务器

Echo 服务器的核心逻辑很简单——客户端发什么,服务器就返回什么。这一步先搞定基础的 Socket 编程流程,以及开发中最容易踩的绑定、连接问题。

1.1 先解决两个核心问题:绑定与连接

在编写服务器代码前,先理清两个高频问题:bind 绑定和 connect 连接参数。

image

服务器端调用 bind 函数时,核心是绑定 IP 和端口:

  • 如果绑定 0.0.0.0,表示监听服务器所有网卡的该端口,这是服务器的常规操作;
  • 如果绑定具体 IP(如 192.168.1.100),则只监听该网卡的请求,适合多网卡的定向监听场景;
  • 绑定失败常见原因:端口被占用、权限不足(1024 以下端口需要 root)。

image

客户端的 connect 函数核心是指定要连接的服务器信息:

  • 参数必须是服务器的 struct sockaddr_in 结构体,包含服务器 IP 和端口;
  • 客户端无需手动 bind 本地端口,系统会自动分配一个未使用的端口,这是客户端和服务器的核心区别之一。

image

客户端 connect 的核心代码如下:

// 初始化服务器地址结构
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(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); // 服务器 IP
// 连接服务器
int ret = connect(client_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
    perror("connect failed");
    close(client_sock);
    return -1;
}

1.2 Echo 客户端与服务器完整实现

理清核心问题后,直接上 Echo 客户端和服务器的核心代码,结合图示逻辑拆解。

image

Echo 客户端核心流程:创建 socket → 连接服务器 → 发送数据 → 接收返回数据 → 关闭 socket。

核心代码片段:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUF_SIZE 1024

int main() {
    // 1. 创建客户端 socket
    int client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock == -1) {
        perror("socket failed");
        return -1;
    }
    // 2. 初始化服务器地址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
    // 3. 连接服务器
    if (connect(client_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(client_sock);
        return -1;
    }
    // 4. 发送数据并接收 echo
    char buf[BUF_SIZE] = "Hello Echo Server!";
    write(client_sock, buf, strlen(buf)); // 发送数据
    memset(buf, 0, BUF_SIZE);
    read(client_sock, buf, BUF_SIZE); // 接收返回数据
    printf("Received from server: %s\n", buf);
    // 5. 关闭 socket
    close(client_sock);
    return 0;
}

image

Echo 服务器核心流程:创建 socket → 绑定 IP 端口 → 监听 → 接受连接 → 读取数据 → 原样返回 → 关闭连接。

核心代码片段:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_PORT 8080
#define BUF_SIZE 1024

int main() {
    // 1. 创建监听 socket
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock == -1) {
        perror("socket failed");
        return -1;
    }
    // 2. 初始化服务器地址(绑定 0.0.0.0:8080)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY; // 等价于 0.0.0.0
    // 3. 绑定地址
    if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(listen_sock);
        return -1;
    }
    // 4. 监听(backlog 设为 5,允许 5 个半连接)
    if (listen(listen_sock, 5) == -1) {
        perror("listen failed");
        close(listen_sock);
        return -1;
    }
    printf("Echo server listening on 0.0.0.0:%d\n", SERVER_PORT);
    // 5. 接受连接并处理 echo
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int conn_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &client_addr_len);
    if (conn_sock == -1) {
        perror("accept failed");
        close(listen_sock);
        return -1;
    }
    printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    // 6. 读取客户端数据并原样返回
    char buf[BUF_SIZE];
    ssize_t n = read(conn_sock, buf, BUF_SIZE);
    if (n > 0) {
        write(conn_sock, buf, n); // 原样返回
    }
    // 7. 关闭连接
    close(conn_sock);
    close(listen_sock);
    return 0;
}

二、并发篇 1:多进程版本 Echo 服务器

上面的基础版服务器有个致命问题——只能处理一个客户端连接,处理完就退出了。要支持并发,首先想到用多进程:父进程负责接受连接,子进程负责处理业务。

2.1 多进程核心逻辑与代码

image

核心设计思路:

  1. 父进程一直循环调用 accept,拿到新的连接套接字 conn_sock;
  2. 每次拿到 conn_sock 后,fork() 创建子进程;
  3. 子进程不需要监听套接字 listen_sock,要主动关闭(避免文件描述符泄漏);
  4. 子进程专注处理 conn_sock 的 Echo 业务,处理完关闭 conn_sock 并退出。

优化后的多进程服务器核心代码(只改 accept 后的逻辑):

// 替换基础版中 accept 后的代码
while (1) {
    // 父进程循环接受连接
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int conn_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &client_addr_len);
    if (conn_sock == -1) {
        perror("accept failed");
        continue;
    }
    printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    // 创建子进程处理业务
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        close(conn_sock);
        continue;
    } else if (pid == 0) {
        // 子进程
        close(listen_sock); // 子进程不需要监听套接字,关闭!
        // 处理 echo 业务
        char buf[BUF_SIZE];
        ssize_t n = read(conn_sock, buf, BUF_SIZE);
        if (n > 0) {
            write(conn_sock, buf, n);
        }
        close(conn_sock); // 子进程处理完关闭连接
        exit(0); // 子进程退出
    } else {
        // 父进程
        close(conn_sock); // 父进程不需要连接套接字,关闭!
    }
}

2.2 解决僵尸进程问题

多进程模型有个坑:子进程退出后,如果父进程不回收,会变成僵尸进程,占用系统资源。

image

方案 1:注册信号处理函数(推荐)

子进程退出时会给父进程发 SIGCHLD 信号,我们可以注册信号处理函数,用 waitpid 非阻塞回收子进程:

#include <signal.h>
#include <sys/wait.h>

// 信号处理函数:回收僵尸进程
void handle_sigchld(int sig) {
    // WNOHANG:非阻塞,有子进程退出就回收,没有就返回
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

// 在 main 函数的 socket 创建后,注册信号
signal(SIGCHLD, handle_sigchld);

image

方案 2:创建孙子进程(孤儿进程)

核心逻辑:

  1. 父进程 fork 出子进程;
  2. 子进程再 fork 出孙子进程,然后子进程立即退出;
  3. 孙子进程变成孤儿进程,由系统(init/systemd)接管,退出后自动回收;
  4. 父进程不需要阻塞,也不用处理信号。

image

代码实现(修改 fork 部分):

pid_t pid = fork();
if (pid == -1) {
    perror("fork failed");
    close(conn_sock);
    continue;
} else if (pid == 0) {
    // 子进程
    pid_t grand_pid = fork();
    if (grand_pid == -1) {
        exit(1);
    } else if (grand_pid == 0) {
        // 孙子进程
        close(listen_sock);
        // 处理 echo 业务
        char buf[BUF_SIZE];
        ssize_t n = read(conn_sock, buf, BUF_SIZE);
        if (n > 0) write(conn_sock, buf, n);
        close(conn_sock);
        exit(0);
    } else {
        // 子进程
        exit(0); // 子进程立即退出,孙子进程变成孤儿
    }
} else {
    // 父进程
    close(conn_sock); // 父进程不需要处理子进程退出,因为子进程已经退出了
}

2.3 多进程模型的小细节:获取客户端 IP

image

通过 accept 的第二个参数 client_addr,可以轻松获取客户端的 IP 和端口:

// accept 后的代码
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
uint16_t client_port = ntohs(client_addr.sin_port);
printf("Client connected: %s:%d\n", client_ip, client_port);

2.4 多进程模型的痛点

image

多进程虽然解决了并发问题,但缺点很明显:

  • 进程创建和销毁的开销大;
  • 每个进程占用独立的内存空间,连接数多了会导致服务器内存、CPU 压力剧增;
  • 进程间通信复杂(如果需要共享数据)。

三、并发篇 2:多线程版本服务器

线程是轻量级进程,创建 / 销毁开销远小于进程,且线程间共享进程资源(如文件描述符、内存),适合高并发场景。

image

3.1 多线程核心逻辑与代码

核心思路:

  1. 父进程(主线程)循环 accept 获取 conn_sock;
  2. 每次拿到 conn_sock 后,创建新线程处理业务;
  3. 线程处理完业务后关闭 conn_sock 并退出。

核心代码(替换多进程的 fork 逻辑):

#include <pthread.h>

// 线程处理函数(注意参数必须是 void* 类型)
void* echo_handler(void* arg) {
    int conn_sock = *(int*)arg;
    free(arg); // 释放传参的内存
    // 分离线程:线程退出后自动释放资源,不需要 pthread_join
    pthread_detach(pthread_self());
    // 处理 echo 业务
    char buf[BUF_SIZE];
    ssize_t n = read(conn_sock, buf, BUF_SIZE);
    if (n > 0) {
        write(conn_sock, buf, n);
    }
    close(conn_sock);
    return NULL;
}

// accept 后的逻辑
while (1) {
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int conn_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &client_addr_len);
    if (conn_sock == -1) {
        perror("accept failed");
        continue;
    }
    // 为线程传参(不能直接传栈变量,要用堆)
    int* p_conn_sock = malloc(sizeof(int));
    *p_conn_sock = conn_sock;
    // 创建线程
    pthread_t tid;
    if (pthread_create(&tid, NULL, echo_handler, p_conn_sock) != 0) {
        perror("pthread_create failed");
        free(p_conn_sock);
        close(conn_sock);
    }
}

3.2 线程与文件描述符的两个关键问题

image

问题 1:进程打开的 FD,线程能看到吗?

文件描述符(FD)是进程级资源,所有线程共享同一个进程的 FD 表。比如主线程打开的 listen_sock,所有子线程都能看到;子线程创建的 conn_sock,主线程也能操作。

问题 2:线程敢不敢关闭自己不需要的 FD?
  • 可以关闭:比如子线程不需要 listen_sock,可以关闭,不影响主线程的 listen_sock(FD 的引用计数减 1,只有引用计数为 0 时才真正关闭);
  • 需谨慎点:不要关闭其他线程还在使用的 FD!比如主线程还在处理 conn_sock,子线程如果关闭这个 FD,会导致主线程的 IO 操作失败。

3.3 短服务 vs 长服务

多线程模型适配不同的业务场景:

  • 短服务:单次请求 - 响应就断开(如 Echo 服务器),线程创建 / 销毁的开销可以接受;
  • 长服务:连接保持,持续交互(如抢票、翻译服务),线程会长期存在,更能体现线程的优势。

四、并发篇 3:线程池版本服务器

多线程虽然比多进程高效,但如果连接数暴增(比如上万),频繁创建 / 销毁线程的开销依然很大。线程池就是提前创建一批线程,放在池子里,有连接就分配线程处理,处理完线程回到池子,避免频繁创建销毁。

image

4.1 线程池核心设计

线程池的核心是'任务队列 + 互斥锁 + 条件变量':

  1. 提前创建 N 个线程,线程启动后阻塞在条件变量上;
  2. 主线程 accept 拿到 conn_sock 后,把 conn_sock 封装成任务,加入任务队列;
  3. 发送条件变量信号,唤醒一个空闲线程处理任务;
  4. 线程处理完任务后,回到阻塞状态,等待下一个任务。

image

4.2 线程池核心代码(简化版)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define THREAD_POOL_SIZE 4 // 线程池大小
#define BUF_SIZE 1024
#define SERVER_PORT 8080

// 任务结构体:存放连接套接字
typedef struct {
    int conn_sock;
} Task;

// 任务队列
Task task_queue[1024];
int queue_head = 0;
int queue_tail = 0;

// 同步机制
pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t queue_cond = PTHREAD_COND_INITIALIZER;

// 线程处理函数
void* thread_worker(void* arg) {
    while (1) {
        // 加锁
        pthread_mutex_lock(&queue_mutex);
        // 任务队列为空,阻塞等待
        while (queue_head == queue_tail) {
            pthread_cond_wait(&queue_cond, &queue_mutex);
        }
        // 取出任务
        Task task = task_queue[queue_head];
        queue_head = (queue_head + 1) % 1024;
        // 解锁
        pthread_mutex_unlock(&queue_mutex);
        
        // 处理 echo 业务
        char buf[BUF_SIZE];
        ssize_t n = read(task.conn_sock, buf, BUF_SIZE);
        if (n > 0) {
            write(task.conn_sock, buf, n);
        }
        close(task.conn_sock);
    }
    return NULL;
}

// 初始化线程池
void init_thread_pool() {
    pthread_t tid;
    for (int i = 0; i < THREAD_POOL_SIZE; i++) {
        pthread_create(&tid, NULL, thread_worker, NULL);
        pthread_detach(tid);
    }
}

// 添加任务到队列
void add_task(int conn_sock) {
    pthread_mutex_lock(&queue_mutex);
    // 放入任务队列
    task_queue[queue_tail].conn_sock = conn_sock;
    queue_tail = (queue_tail + 1) % 1024;
    // 发送信号唤醒线程
    pthread_cond_signal(&queue_cond);
    pthread_mutex_unlock(&queue_mutex);
}

int main() {
    // 初始化线程池
    init_thread_pool();
    // 创建监听 socket(和基础版一样)
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(listen_sock, 5);
    // 主线程循环 accept
    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_addr_len = sizeof(client_addr);
        int conn_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &client_addr_len);
        if (conn_sock == -1) continue;
        // 添加任务到线程池
        add_task(conn_sock);
    }
    close(listen_sock);
    return 0;
}

image

线程池的优势在于'解耦':

  • 主线程只负责'接连接',不处理业务;
  • 线程池只负责'处理业务',不用关心连接怎么来;
  • 上层业务逻辑(比如把 Echo 改成翻译、抢票)只需要修改 thread_worker 里的处理逻辑即可,不用动底层的连接逻辑。

五、扩展篇:远程命令执行服务器(附安全防护)

把 Echo 的逻辑改成'客户端发命令,服务器执行并返回结果',是很常见的后端需求,但这里有巨大的安全风险,必须做防护。

5.1 远程命令执行核心逻辑

核心思路:客户端发送命令字符串(如 ls、pwd),服务器用 popen 执行命令,读取执行结果并返回给客户端。

核心代码(替换线程池的 Echo 处理逻辑):

// 替换 thread_worker 里的 echo 逻辑
char buf[BUF_SIZE];
ssize_t n = read(task.conn_sock, buf, BUF_SIZE);
if (n > 0) {
    buf[n-1] = '\0'; // 去掉换行符
    // 执行命令并读取结果
    FILE* fp = popen(buf, "r");
    if (fp == NULL) {
        const char* err = "Command execute failed\n";
        write(task.conn_sock, err, strlen(err));
        close(task.conn_sock);
        continue;
    }
    // 读取命令执行结果并返回
    char result[BUF_SIZE];
    while (fgets(result, BUF_SIZE, fp) != NULL) {
        write(task.conn_sock, result, strlen(result));
    }
    pclose(fp);
}
close(task.conn_sock);

5.2 安全防护:白名单机制

image

如果直接执行客户端发来的命令,风险极大——比如客户端发 rm -rf /,服务器直接导致严重安全漏洞。所以必须做白名单:只允许执行指定的安全命令。

image

5.3 白名单核心代码

// 定义白名单命令
const char* whitelist[] = {"ls", "pwd", "date", "whoami", NULL};

// 检查命令是否在白名单中
int is_in_whitelist(const char* cmd) {
    for (int i = 0; whitelist[i] != NULL; i++) {
        if (strcmp(cmd, whitelist[i]) == 0) {
            return 1;
        }
    }
    return 0;
}

// 在执行命令前加检查
if (n > 0) {
    buf[n-1] = '\0';
    // 检查白名单
    if (!is_in_whitelist(buf)) {
        const char* err = "Error: Command not allowed\n";
        write(task.conn_sock, err, strlen(err));
        close(task.conn_sock);
        continue;
    }
    // 执行白名单内的命令
    FILE* fp = popen(buf, "r");
    // ... 后续逻辑不变
}

六、总结

本文从基础的 Echo 服务器出发,一步步优化并发能力(多进程→多线程→线程池),最后扩展到远程命令执行并补充安全防护,核心知识点总结:

  1. 基础 Socket 编程:服务器 bind+listen+accept,客户端 connect,注意 FD 的管理;
  2. 并发优化:多进程解决基础并发但开销大,多线程轻量化但需注意 FD 共享,线程池是高并发最优解;
  3. 安全防护:远程命令执行必须加白名单,限制可执行的命令范围,避免恶意攻击。

image

image

实战建议:

  • 调试时用 netstat -anp | grep 端口 查看端口监听状态,确认服务器是否正常启动;
  • 用 ps -ef | grep 进程名 查看进程 / 线程状态,排查僵尸进程、线程泄漏问题。
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog

更多推荐文章

查看全部
  • Linux 进程替换详解:从 fork 到 exec 的完整链路
  • CentOS 7.9 Docker 安装、配置与实战指南
  • MacOS 使用 Royal TSX 替代 Xshell 进行 SSH/SFTP 管理配置指南
  • 从冯诺依曼体系到操作系统:解析 Linux 底层核心逻辑
  • Linux 基础操作:用户身份、文件权限与修改命令详解
  • Linux 进程地址空间解析:动静态库、信号及多线程
  • Linux 进程控制详解:fork、wait 与退出机制
  • Arm64 麒麟服务器 V10 Docker 环境搭建部署
  • Docker 部署 Redis 并通过内网穿透远程管理
  • Wake-On-LAN 远程唤醒工具使用指南
  • Docker Network 命令:容器网络管理的完整指南
  • Linux 环境下使用 C++ 实现 Shell 基本功能
  • SSH 远程登录指定端口与账号配置指南
  • Windows 权限提升:滥用 Windows 服务提权(上)
  • 时序数据库选型指南:Apache IoTDB 技术创新与优势解析
  • Linux 系统目录结构详解
  • MCP 插件配置实战:browser-tools-mcp 集成指南
  • n8n 开源自动化工作流工具部署与使用指南
  • MCPo 技术解析:MCP 协议转 OpenAPI 代理与集成实践
  • Qwen3+Qwen Agent 智能体开发实战:接入 MCP 工具

相关免费在线工具

  • 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