TCP Server 业务扩展实战:从 Echo 到远程命令执行与词典翻译

1. 从 Echo 到业务扩展的演进

1.1 Echo Server 的局限

之前的 Echo Server 只能做一件事:把客户端发来的消息原样返回。

复制代码
客户端发送: "hello"
服务器返回: "echo# hello"

问题

  • 业务逻辑写死在 Service()

  • 想换个功能(比如翻译、计算)就得改 TcpServer.hpp

  • TcpServer业务逻辑强耦合

1.2 新需求:让服务器"可编程"

我们希望:

核心思路 :把业务逻辑从 TcpServer 中抽出来,通过回调函数注入。


2. func_t ------ 回调函数类型的艺术

2.1 什么是 func_t?

复制代码
using func_t = std::function<std::string(const std::string &, InetAddr &)>;

逐层拆解

为什么用 std::function 而不是函数指针?

特性 函数指针 (*fp)(...) std::function
包装普通函数
包装 Lambda
包装成员函数 ❌(需额外处理) (通过 bind)
包装函数对象
类型安全
可存储在容器中 困难 容易

2.2 func_t 的三种绑定方式

复制代码
// 方式1:普通函数
std::string defaulthhandler(const std::string &word, InetAddr &addr) {
    return "haha, " + word;
}
func_t f1 = defaulthhandler;

// 方式2:Lambda 表达式
func_t f2 = [](const std::string &word, InetAddr &addr) -> std::string {
    return "echo: " + word;
};

// 方式3:bind 绑定成员函数(最常用!)
Command cmd;
func_t f3 = std::bind(&Command::Execute, &cmd, 
                      std::placeholders::_1, std::placeholders::_2);

2.3 std::bind 详解

复制代码
std::bind(&Command::Execute, &cmd, 
          std::placeholders::_1, std::placeholders::_2)
复制代码
// 方式A:先创建对象,再 bind
Command cmd;
func_t f = std::bind(&Command::Execute, &cmd, 
                     std::placeholders::_1, std::placeholders::_2);
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, f);

// 方式B:Lambda 捕获(更灵活)
Dict d;
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, 
    [&d](const std::string &word, InetAddr &addr) {
        return d.Translate(word, addr);
    });

3. TcpServer 改造:注入业务逻辑

3.1 改造前后的对比

改造前(Echo 版本)

复制代码
class TcpServer : public NoCopy {
    // ...
    void Service(int sockfd, InetAddr &peer) {
        // 业务逻辑写死在这里
        std::string echo_string = "echo# " + buffer;
        write(sockfd, echo_string.c_str(), echo_string.size());
    }
};

改造后(回调版本)

复制代码
using func_t = std::function<std::string(const std::string &, InetAddr &)>;

class TcpServer : public NoCopy {
public:
    TcpServer(uint16_t port, func_t func)  // 构造函数接收回调
        : _port(port), 
          _listensockfd(defaultsockfd), 
          _isrunning(false),
          _func(func)  // 保存回调
    {}
    
    void Service(int sockfd, InetAddr &peer) {
        // ...
        std::string echo_string = _func(buffer, peer);  // 调用回调!
        write(sockfd, echo_string.c_str(), echo_string.size());
    }
    
private:
    func_t _func;  // 业务回调函数
};

3.2 调用链完整流程


4. Command 类:安全的远程命令执行

4.1 为什么需要白名单?

4.2 Command 类完整解析

复制代码
class Command {
public:
    Command() {
        // 严格匹配白名单
        _WhiteListCommands.insert("ls");
        _WhiteListCommands.insert("pwd");
        _WhiteListCommands.insert("ls -l");
        _WhiteListCommands.insert("touch haha.txt");
        _WhiteListCommands.insert("who");
        _WhiteListCommands.insert("whoami");
    }
    
    bool IsSafeCommand(const std::string &cmd) {
        auto iter = _WhiteListCommands.find(cmd);
        return iter != _WhiteListCommands.end();
    }
    
    std::string Execute(const std::string &cmd, InetAddr &addr) {
        // 1. 安全检查
        if (!IsSafeCommand(cmd)) {
            return std::string("坏人");  // 拒绝执行
        }
        
        std::string who = addr.StringAddr();  // "127.0.0.1:8080"
        
        // 2. 执行命令
        FILE* fp = popen(cmd.c_str(), "r");
        if (nullptr == fp) {
            return std::string("要执行的命令不存在: ") + cmd;
        }
        
        // 3. 读取输出
        std::string res;
        char line[1024];
        while (fgets(line, sizeof(line), fp)) {
            res += line;  // 逐行读取命令输出
        }
        pclose(fp);
        
        // 4. 组装结果
        std::string result = who + " execute done, result is:\n" + res;
        LOG(LogLevel::DEBUG) << result;
        return result;
    }
    
private:
    std::set<std::string> _WhiteListCommands;
};

4.3 为什么用 std::set 而不是 std::vector

容器 查找复杂度 适用场景
std::vector O(n) 线性查找 数据量小、频繁遍历
std::set O(log n) 红黑树 需要快速查找、去重
std::unordered_set O(1) 哈希表 大量数据、极致性能

这里用 set 是因为命令数量少,且 find 语义清晰。


5. Dict 类:英汉词典翻译

5.1 设计思路

5.2 核心代码解析

复制代码
#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include "fstream"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _dict_path(path) {};
    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            //"apple: 苹果"
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos); // 前闭后开
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "没有有效内容: " << line;
                continue;
            }
            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line;
        }

        in.close();
        return true;
    }
    std::string Translate(const std::string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " " << client.Port() << "]" << word << "->" << "None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " " << client.Port() << "]" << word << "->" << iter->second;
        return iter->second;
    }
    ~Dict() {};

private:
    std::string _dict_path; // 路径+文件名
    std::unordered_map<std::string, std::string> _dict;
};

5.3 Lambda 捕获 Dict 对象

复制代码
Dict d;
d.LoadDict();  // 先加载字典

// Lambda 以引用方式捕获 d
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, 
    [&d](const std::string &word, InetAddr &addr) -> std::string {
        return d.Translate(word, addr);
    });

捕获方式对比

语法 含义 生命周期注意
[&d] 引用捕获 d 必须在 TcpServer 生命周期内有效
[d] 值捕获(拷贝) 安全但开销大
[=] 隐式值捕获所有 可能不必要的拷贝
[&] 隐式引用捕获所有 需注意所有变量的生命周期

6. popen() / pclose() 深度解析

6.1 为什么不用 system()?

复制代码
// system() 的问题:
int system(const char *command);
// 返回值复杂(包含退出码、信号等)
// 无法直接获取命令输出
// 安全性差(直接执行 shell)

6.2 popen() 原理图

6.3 popen() 参数详解

复制代码
FILE *popen(const char *command, const char *type);
参数 含义
command "ls" 要执行的 shell 命令
type "r" 读取模式:从命令的标准输出读
type "w" 写入模式:向命令的标准输入写

返回值

  • 成功:返回 FILE* 文件指针,可用 fgets() fread() 读取

  • 失败:返回 NULL

6.4 完整读取流程

复制代码
FILE* fp = popen(cmd.c_str(), "r");
if (nullptr == fp) {
    return "命令执行失败";
}

std::string res;
char line[1024];

// fgets 逐行读取,直到 EOF
while (fgets(line, sizeof(line), fp)) {
    res += line;  // line 包含换行符 \n
}

pclose(fp);  // 必须关闭!比 fclose 多做一件事:等待子进程结束

pclose() vs fclose()

函数 作用
fclose() 仅关闭文件流
pclose() 关闭文件流 + waitpid() 等待子进程结束 + 返回子进程退出状态

6.5 安全注意事项

复制代码
// ❌ 危险:直接拼接用户输入
std::string cmd = user_input;
popen(cmd.c_str(), "r");  // 用户输入 "rm -rf /" 就完了

// ✅ 安全:白名单校验 + 严格匹配
if (!IsSafeCommand(cmd)) {
    return "坏人";
}
popen(cmd.c_str(), "r");  // 只允许预设命令

7. 业务解耦的设计哲学

7.1 什么是解耦?

7.2 解耦的好处

方面 耦合设计 解耦设计
修改业务 改核心类 只改业务类
测试 困难 单独测试业务类
复用 无法复用 同一框架不同业务
团队协作 冲突多 各写各的业务模块
扩展 改源码 新增业务类即可

7.3 架构图


8. 完整代码汇总

8.1 改造后的 TcpServer.hpp

复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <memory> //可能会用到智能指针
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/wait.h>
#include <pthread.h>
#include "ThreadPool.hpp"
#include <signal.h>

using namespace LogModule;
using namespace ThreadPoolModule;

// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string &, InetAddr &)>;

const static int defaultsockfd = -1;
const static int backlog = 8;
// 服务器往往是禁止拷贝的
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port, func_t func)
        : _port(port),
          _listensockfd(defaultsockfd),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // signal(SIGCHLD,SIG_IGN); //忽略

        // 1.创建套接字
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::DEBUG) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3

        // 2.bind总所周知的端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3

        // 3.设置socket状态为listen
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success: " << _listensockfd;
    }

    class ThreadData
    {
        // InetAddr无需构造
    public:
        ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
        {
        }

    public:
        int sockfd;
        InetAddr addr;
        TcpServer *tsvr;
    };

    // 长服务 -- 你不退出,它不退:多进程多线程合适
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1.先读取数据
            // a.n > 0 读取成功
            // b.n < 0 读取失败
            // c.n ==0 对端把链接关闭,读到了文件结尾 -- pipe
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                // buffer是一个英文单词 or 是一个命令字符串

                buffer[n] = 0; // 设置为C风格的字符串  n<=sizeof(buffer)-1
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " say#" << buffer;
                // 2.写回数据
                std::string echo_string = _func(buffer, peer); // 交给上层(回调-出去处理,处理完回来)
                // std::string echo_string = "echo# ";
                // echo_string += buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel ::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel ::DEBUG) << peer.StringAddr() << "异常了...";
                close(sockfd);
                break;
            }
        }
    }

    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->addr);
        delete td;
        return nullptr;
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // a.服务器必须先获取链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 如果没有连接,accept就会阻塞
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel::INFO) << "accept success, peer addr: " << addr.StringAddr();

            // version0 -- test version -- 单进程程序 -- 不会存在的
            // Service(sockfd, addr);

            // version1 -- 多进程版本
            // pid_t id = fork(); //父进程
            // if (id < 0)
            // {
            //     LOG(LogLevel::FATAL) << "fork error";
            //     exit(FORK_ERR);
            // }
            // else if (id == 0)
            // {
            //     // 子进程,子进程除了看到 sockfd,能看到listensocket?
            //     //我们不想让子进程访问listensocket
            //     close(_listensockfd);

            //     if(fork() > 0) //再次fork,子进程退出
            //         exit(OK);

            //     Service(sockfd,addr);  //孙子进程,孤儿进程,1,系统回收
            //     exit(OK);  //正确处理完任务
            // }
            // else
            // {
            //     // 父进程
            //     close(sockfd);

            //     //父进程是不是要等待子进程?否则会僵尸
            //     pid_t rid = waitpid(id,nullptr,0); //阻塞?不会,因为子进程立马退出了
            //     (void)rid;
            // }

            // version2:多线程版本
            ThreadData *td = new ThreadData(sockfd, addr, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);

            // version3: 线程池版本,线程池一般适合处理短服务
            // 将新链接和客户端构建一个新任务,push到线程池中
            // ThreadPool<task_t>::GetInstance()->Enqueue([this, &sockfd, &addr]()
            //                                            { this->Service(sockfd, addr); });
        }
        _isrunning = false;
    }
    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd; // 监听套接字

    bool _isrunning;
    func_t _func; // 设置回调处理
};

8.2 Command.hpp

复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <set>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"

using namespace LogModule;

class Command {
public:
    Command() {
        _WhiteListCommands.insert("ls");
        _WhiteListCommands.insert("pwd");
        _WhiteListCommands.insert("ls -l");
        _WhiteListCommands.insert("touch haha.txt");
        _WhiteListCommands.insert("who");
        _WhiteListCommands.insert("whoami");
    }
    
    bool IsSafeCommand(const std::string &cmd) {
        return _WhiteListCommands.find(cmd) != _WhiteListCommands.end();
    }
    
    std::string Execute(const std::string &cmd, InetAddr &addr) {
        if (!IsSafeCommand(cmd)) {
            return std::string("坏人");
        }
        
        std::string who = addr.StringAddr();
        FILE* fp = popen(cmd.c_str(), "r");
        if (nullptr == fp) {
            return std::string("要执行的命令不存在: ") + cmd;
        }
        
        std::string res;
        char line[1024];
        while (fgets(line, sizeof(line), fp)) {
            res += line;
        }
        pclose(fp);
        
        std::string result = who + " execute done, result is:\n" + res;
        LOG(LogLevel::DEBUG) << result;
        return result;
    }
    
    ~Command() {}

private:
    std::set<std::string> _WhiteListCommands;
};

8.3 Dict.hpp

复制代码
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

class Dict {
public:
    Dict(const std::string &path = defaultdict) : _dict_path(path) {}
    
    bool LoadDict() {
        std::ifstream in(_dict_path);
        if (!in.is_open()) {
            LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
            return false;
        }
        std::string line;
        while (std::getline(in, line)) {
            auto pos = line.find(sep);
            if (pos == std::string::npos) {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty()) continue;
            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line;
        }
        in.close();
        return true;
    }
    
    std::string Translate(const std::string &word, InetAddr &client) {
        auto iter = _dict.find(word);
        if (iter == _dict.end()) {
            LOG(LogLevel::DEBUG) << "[" << client.Ip() << " " << client.Port() 
                               << "] " << word << " -> None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "[" << client.Ip() << " " << client.Port() 
                           << "] " << word << " -> " << iter->second;
        return iter->second;
    }
    
    ~Dict() {}

private:
    std::string _dict_path;
    std::unordered_map<std::string, std::string> _dict;
};

8.4 TcpServer.cc(命令执行版本)

复制代码
#include "Tcpserver.hpp"
#include "Log.hpp"
#include "Command.hpp"

using namespace LogModule;

void Usage(std::string proc) {
    std::cerr << "Usage: " << proc << " port" << std::endl;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Stratege();

    Command cmd;
    // bind 绑定成员函数,生成 func_t 回调
    func_t f = std::bind(&Command::Execute, &cmd, 
                         std::placeholders::_1, std::placeholders::_2);
    
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, f);
    tsvr->Init();
    tsvr->Run();
    return 0;
}

8.5 TcpServer.cc(词典翻译版本)

复制代码
#include "Tcpserver.hpp"
#include "Log.hpp"
#include "Dict.hpp"

using namespace LogModule;

void Usage(std::string proc) {
    std::cerr << "Usage: " << proc << " port" << std::endl;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Stratege();

    Dict d;
    d.LoadDict();  // 加载字典文件

    // Lambda 捕获 Dict 对象
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, 
        [&d](const std::string &word, InetAddr &addr) -> std::string {
            return d.Translate(word, addr);
        });
    
    tsvr->Init();
    tsvr->Run();
    return 0;
}

回调函数是 C++ 中实现解耦的核心机制。通过 std::functionstd::bind,我们可以将网络框架与业务逻辑彻底分离,实现"同一个 TcpServer,不同的业务"的灵活架构。popen() 则为我们提供了在程序中安全执行外部命令的能力,但务必注意安全性校验。

相关推荐
运维老郭1 小时前
K8S 容器独占 CPU(CPU 绑核)最佳实践,解锁极致性能所需的 3 个核心条件及其代价
运维·云原生·kubernetes
香菜农民2 小时前
域名证书管理
运维·docker
剑神一笑2 小时前
Linux awk 命令:文本处理的瑞士军刀
linux·运维·chrome
江湖有缘2 小时前
从零开始:基于Docker Compose的Kener监控面板部署全记录
运维·docker·容器
原来是猿2 小时前
TCP Echo Server 深度解析:从单进程到线程池的演进之路(上)
服务器·网络·tcp/ip
躺不平的理查德2 小时前
Shell逻辑判断备忘录
运维·服务器·git
月光技术杂谈2 小时前
国内环境下安装 docker-ce 的完整步骤
运维·docker·容器
skywalk81632 小时前
Trae生成的中文编程语言关键字(如“定“、“函“、“印“等)需要和标识符之间用 空格 隔开,以确保正确识别
服务器·开发语言·编程
焦糖玛奇朵婷3 小时前
健身房预约小程序开发、设计
java·大数据·服务器·前端·小程序