【Linux】TCP协议【2】: 从 echo 到远程命令执行:Linux TCP 服务器的并发与安全实践

【Linux】TCP协议【2】: 从 echo 到远程命令执行:Linux TCP 服务器的并发与安全实践

作为后端开发的核心技能,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的核心代码,一看就懂:

c

运行

// 初始化服务器地址结构 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。

核心代码片段:

c

运行

#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 端口 → 监听 → 接受连接 → 读取数据 → 原样返回 → 关闭连接。

核心代码片段:

c

运行

#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 后的逻辑):

c

运行

// 替换基础版中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非阻塞回收子进程:

c

运行

#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 部分):

c

运行

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 和端口:

c

运行

// 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 逻辑):

c

运行

#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,线程能看到吗?(答案:1)

文件描述符(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 线程池核心代码(简化版)

c

运行

#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 远程命令执行核心逻辑

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

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

c

运行

// 替换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 白名单核心代码

c

运行

// 定义白名单命令 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 进程名查看进程 / 线程状态,排查僵尸进程、线程泄漏问题。

Read more

2026最新|国内可用 Docker 镜像加速源大全(2月持续更新):DockerHub 镜像加速与限速避坑全指南(适配 Windows / macOS / Linux / containerd /

2026最新|国内可用 Docker 镜像加速源大全(2月持续更新):DockerHub 镜像加速与限速避坑全指南(适配 Windows / macOS / Linux / containerd /

2026最新|国内可用 Docker 镜像加速源大全(2月持续更新):DockerHub 镜像加速与限速避坑全指南(适配 Windows / macOS / Linux / containerd / k3s / BuildKit) 摘要:本指南面向国内服务器与办公网络用户,系统梳理 2026年2月可用 DockerHub 镜像加速源,覆盖 Docker Desktop、dockerd、containerd、k3s、BuildKit 等场景的一键配置、多源回退与测速排障方案,帮助规避 429/Too Many Requests 与拉取超时问题。 最后更新:2026-2 适用对象:国内云服务器/办公网络拉取 DockerHub 镜像慢、易触发限速(429/“Too Many Requests”)的场景 用途:一键配置镜像加速、

By Ne0inhk

Flutter 三方库 jaguar 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全能的工业级嵌入式 HTTP 服务端框架与 REST API 交互引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 jaguar 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全能的工业级嵌入式 HTTP 服务端框架与 REST API 交互引擎 在鸿蒙(OpenHarmony)系统的端侧服务器化、分布式设备互联监控、或者是需要将鸿蒙应用转变为一个能够提供 API 服务的微型网关(如鸿蒙版物联网中枢)场景中,如何通过一套 Dart 代码构建出极致稳健、带路由拦截、支持 Session 且完全透明的 HTTP 服务?jaguar 为开发者提供了一套工业级的、基于生产环境优化的服务端处理方案。本文将深入实战其在鸿蒙端侧服务化中的应用。 前言 什么是 Jaguar?它不是一个普通的 HTTP 监听器,而是一个专为“速度”与“扩展性”

By Ne0inhk
Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战

Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战 前言 在进行 Flutter for OpenHarmony 的去中心化应用(DApp)或加密货币钱包开发时,支持标准的 WalletConnect 协议是链接用户钱包的关键。wallet_connect 是该协议的 Dart 实现,它能让你的鸿蒙 App 安全地与 MetaMask、Trust Wallet 等钱包建立双向加密连接。本文将探讨如何在鸿蒙系统下构建安全、稳定的 Web3 授权流程。 一、原理解析 / 概念介绍 1.1 基础原理

By Ne0inhk
Flutter 组件 simple_cluster 的适配 鸿蒙Harmony 实战 - 驾驭轻量级集群分发架构、实现鸿蒙端多节点任务调度与高性能负载均衡方案

Flutter 组件 simple_cluster 的适配 鸿蒙Harmony 实战 - 驾驭轻量级集群分发架构、实现鸿蒙端多节点任务调度与高性能负载均衡方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 simple_cluster 的适配 鸿蒙Harmony 实战 - 驾驭轻量级集群分发架构、实现鸿蒙端多节点任务调度与高性能负载均衡方案 前言 在鸿蒙(OpenHarmony)生态迈向“万物互联、万物协同”的深水区后,单一设备孤岛式的算力模式已经无法满足复杂的工业控制、分布式协同办公以及大规模 IoT 设备管理的需求。面对需要将一个繁重的计算任务(如:海量 Hex 数据的指纹比对)分发给附近的 5 台鸿蒙平板协同处理;面对需要管理数十个传感器节点的实时状态同步。 如果依靠传统的手动 Socket 连接管理。那么不仅会导致通讯代码极其臃肿且难以维护。更会因为缺乏确定性的负载均衡(Load Balancing)与节点心跳(Heartbeat)逻辑。引发整个系统的雪崩式失效方案。 我们需要一种“逻辑集群化、操作极简化”的算力平衡艺术。

By Ne0inhk