公司动态

【Linux】C++ 实现可扩展 TCP 服务器|回响、字典、远程命令

📅 2026/9/2 8:57:16
【Linux】C++ 实现可扩展 TCP 服务器|回响、字典、远程命令
一、引言在 Linux 网络编程学习中很多人写服务端项目时都是从零编写套接字代码做回响、字典、计算器功能要重复造轮子。其实我们可以封装一套通用 Tcpserver 框架利用回调函数把底层网络逻辑和上层业务拆分开。同一套服务器代码只要替换业务处理函数就能快速实现回响服务、在线字典查询、远程计算器三种不同功能。本文就带大家感受通用 TCP 服务封装的思路理解代码复用和解耦设计的好处。二、Common.hpp它作为项目公共头文件整合各类系统头文件定义程序退出错误码替换硬编码数字方便定位程序出错位置。其中Nocopy 工具类用来禁用子类的拷贝与赋值防止套接字资源因为对象拷贝发生错乱。CONV 宏用来做结构体强制转换。socket 接口要求传入通用地址结构体指针我们每次不用手写长长的强制转换用宏直接简化代码。例如CONV(peer)等价于(struct sockaddr*)peer少写重复模板代码。#pragma once #include iostream #include iostream #include string #include cstring #include cstdlib #include unistd.h #include memory #include sys/socket.h #include sys/types.h #include netinet/in.h #include arpa/inet.h enum Exitcode { OK0, USAGE_ERR, SOCKET_ERR, BIND_ERR, LISTEN_ERR, ACCEPT_ERR, CONNECT_ERR, FORK_ERR }; //让继承它的子类不能被复制、不能被赋值 class Nocopy { public: Nocopy() {} ~Nocopy() {} Nocopy(const Nocopy ) delete; const Nocopy operator(const Nocopy ) delete; }; #define CONV(addr) ((struct sockaddr*)addr)三、Tcpserver.hpp首先我们先封装可复用的通用Tcpserver框架整合了套接字创建、绑定、监听、连接接收等基础网络逻辑。框架通过回调函数解耦网络层与业务层更换回调逻辑即可快速实现回响、在线字典、远程计算器等功能。代码还预留了单进程、多进程、多线程、线程池四种并发实现方案方便对比不同服务器并发模型的差异。2.1核心流程1.Init( )完成 TCP 服务初始化执行 socket 创建监听套接字、bind绑定端口、listen开启监听。2.Run()服务主循环不停调用 accept 等待客户端接入。3.每连上一个客户端就创建新线程执行 Service() 函数负责和客户端收发数据。多进程版本4.Service()循环读取客户端发来的数据交给回调函数 _func 处理业务逻辑再把处理结果发回客户端。2.2版本version0单进程一次只能服务一个客户端几乎不用version1多进程方案fork 创建子进程version2来一个客户端临时开一条线程通信结束这条线程就没了频繁创建销毁比较耗性能version3线程池方案可开启复用线程减少频繁创建销毁线程开销2.3注意1.func_t _func 回调业务逻辑从外面传进来框架本身不用关心具体是做回响、查字典还是运行计算器。2.封装 ThreadData 结构体在线程函数之间传递套接字、客户端地址和服务器对象这几组数据。3.继承 Nocopy不让服务器对象被拷贝防止出现资源错乱的问题。注Nocopy 是一个工具基类。TcpServer 管理着监听套接字这种独占资源一旦对象被拷贝很容易出现重复释放资源的 bug。让 TcpServer 继承 Nocopy就能直接禁用拷贝构造和赋值代码编译时就拦截非法拷贝操作避免后续资源管理乱套。#pragma once class Nocopy { protected: Nocopy() default; ~Nocopy() default; // 删除拷贝构造 Nocopy(const Nocopy) delete; // 删除赋值重载 Nocopy operator(const Nocopy) delete; };2.4代码#pragma once #include Common.hpp #include Log.hpp #include InetAddr.hpp #include sys/wait.h #include signal.h #include pthread.h #include ThreadPool.hpp #include functional // using task_tstd::functionvoid(); using func_tstd::functionstd::string(const std::string,InetAddr); using namespace ThreadPoolModule; using namespace LogModule; #define backlog 10 const int defaultsockfd-1; 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.创建套接字 _listensockfdsocket(AF_INET,SOCK_STREAM,0); if(_listensockfd0) { LOG(Loglevel::FATAL)socket error; exit(SOCKET_ERR); } LOG(Loglevel::INFO)socket success_listensockfd; //2.绑定 InetAddr localaddr(_port); int nbind(_listensockfd,localaddr.NetAddptr(),localaddr.Netaddlen()); if(n0) { LOG(Loglevel::FATAL)bind error; exit(BIND_ERR); } LOG(Loglevel::INFO)bind success_listensockfd; //3.监听 nlisten(_listensockfd,backlog); if(n0) { LOG(Loglevel::FATAL)listen error; exit(LISTEN_ERR); } LOG(Loglevel::INFO)listen success_listensockfd; } class ThreadData { 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) { ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n0) { buffer[n]0;//设置为字符串 LOG(Loglevel::DEBUG)peer.StringAddr() #buffer; //执行字典业务、远程控制业务 std::string echo_string_func(buffer,peer); //写回数据 // std::string echo_stringecho_say#; // echo_stringbuffer; write(sockfd,echo_string.c_str(),echo_string.size()); } else if(n0) { 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* tdstatic_castThreadData*(args); td-tsvr-Service(td-sockfd,td-addr); delete td; return nullptr; } void Run() { _isrunningtrue; while(_isrunning) { //获取链接 struct sockaddr_in peer;//一个空盒子准备装客户端地址 socklen_t lensizeof(peer); int sockfdaccept(_listensockfd,CONV(peer),len);//干活的 传数据的套接字可以有多个 if(sockfd0) { LOG(Loglevel::FATAL) accept error; exit(ACCEPT_ERR); } InetAddr addr(peer);// 1. 用 peer 构造出一个 InetAddr 对象名字叫 addr LOG(Loglevel::INFO) accept success,peer addr: addr.StringAddr(); //version 0 测试 --单进程版本 // Service(sockfd,addr); //version 1 多进程版本 // pid_t idfork(); // if(id0) // { // LOG(Loglevel::FATAL)fork error; // exit(FORK_ERR); // } // else if(id0) // { // //子进程 // //子进程是来服务客户端的它不需要监听新连接 // close(_listensockfd); // //创建孙子进程来执行 // if(fork()0) // exit(OK); // //孙子进程来执行 // Service(sockfd, addr); // } // else // { // //父进程 // //循环 accept 接新客户端 ,根本不跟客户端通信,所以不需要sockfd // close(sockfd); // pid_t ridwaitpid(id,nullptr,0);//如果这样会阻塞 可以创建孙子进程来执行 也可以执行signal // (void)rid; // } //version2 多线程版本 ThreadData* tdnew ThreadData(sockfd,addr,this);//td 是这个盒子的地址。 pthread_t tid; pthread_create(tid,NULL,Routine,td); //version3 线程池版本 // ThreadPooltask_t::Getinstance()-Equeue([this,sockfd,addr](){ // this-Service(sockfd,addr); // }); } _isrunningfalse; } ~TcpServer() {} private: uint16_t _port; int _listensockfd;//监听 只负责获取链接的套接字 bool _isrunning; func_t _func;//设置回调函数 };四、翻译模块Dict类是字典翻译业务模块专门给 TCP 服务器提供查单词功能属于上层业务代码和网络代码完全解耦。程序启动的时候把本地 dictionary.txt 单词文件加载到内存哈希表客户端发来英文单词服务器调用这个类就能查到对应的中文释义。4.1Loaddict———加载字典文件程序启动之后这个函数就会运行。它打开本地的 dictionary.txt 字典文件然后逐行读取里面的内容。文件里每一行都要遵守sad: 悲伤的这种格式。代码会找到分隔符:把一行文字切开前面一部分是英文单词后面就是对应的中文意思最后把这一组数据存到哈希表里面。如果读到格式错乱、没法分割的脏数据不会直接让服务器崩掉只是跳过这一行并且输出一条警告日志方便我们后期排查文件哪里写错了。4.2Translate———翻译查询函数接收客户端传来的单词去哈希表里查找。找到单词返回中文释义同时打印日志记录哪个 IP 端口查了什么单词找不到单词返回字符串 None#include iostream #include fstream #include string #include unordered_map #include log.hpp #include InetAddr.hpp using namespace LogModule; const std::string defaultdict./dictionary.txt; const std::string sep1: ;//log里也有这个 所以可能存在冲突 要改一下名字 class Dict { public: Dict(const std::string dict_path defaultdict) : _dict_path(dict_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)) // 只要能读到行就继续循环 { // sad: 悲伤的 auto posline.find(sep1); if(posstd::string::npos) { LOG(Loglevel::WARNING)解析line失败; continue; } std::string englishline.substr(0,pos); std::string chineseline.substr(possep1.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::stringword,InetAddr client) { auto iter_dict.find(word); if(iter_dict.end()) { return None; } LOG(Loglevel::INFO)进入翻译模块 [client.Ip():client.Port()]word-iter-second; return iter-second; } ~Dict() { } private: std::string _dict_path; std::unordered_mapstd::string, std::string _dict; };五、远程执行模块Command类的作用就是让 TCP 服务器拥有远程执行 Linux 指令的能力。初始化的时候我们维护了一份命令白名单只有名单里写好的命令才能够被运行。当客户端发送一条指令过来Excute函数首先调用Issafecommand检查这条命令是否合法。要是客户端发来的命令不在白名单里服务器直接就不给它跑拒绝请求。校验通过以后就调用popen执行这条 Linux 命令。(让 Linux 系统帮你执行一条命令然后把命令的输出变成一个 “文件” 给你读)然后一行一行把命令打印出来的内容全部读出来拼成一整段结果。最后把客户端的地址和命令运行后的内容一块打包发回给客户端。注popen 是 Linux 下一个库函数作用在程序内部执行一条 shell 命令并且能拿到命令打印出来的结果。#pragma once #include iostream #include string #include set #include Common.hpp #include InetAddr.hpp class Command { public: Command() { _whitelistcommands.insert(ls); _whitelistcommands.insert(pwd); _whitelistcommands.insert(ls -l); _whitelistcommands.insert(ll); _whitelistcommands.insert(touch); _whitelistcommands.insert(who); _whitelistcommands.insert(whoami); } bool Issafecommand(const std::string cmd) { auto iter_whitelistcommands.find(cmd); return iter!_whitelistcommands.end(); } std::string Excute(const std::string cmd,InetAddr addr) { //1.判断命令是否在白名单 if(!Issafecommand(cmd)) { return std::string(坏人); } std::string whoaddr.StringAddr(); //2.执行命令 FILE* fppopen(cmd.c_str(),r);//让 Linux 系统帮你执行一条命令然后把命令的输出变成一个 “文件” 给你读 if(fpnullptr) { return std::string(你输入的命令不存在:)cmd; } //把命令执行完的所有结果全部读出来拼成一个大字符串 std::string result; char line[1024]; while(fgets(line,sizeof(line),fp))//循环读 { resultline; } return whoexcute done,result is:\nresult; } ~Command() {} private: std::setstd::string _whitelistcommands; };六、Tcpclient.cc这就是我们的TCP 客户端程序。运行时填上服务器 IP 和端口就能主动去连接 TCP 服务端。客户端不需要绑定端口、开启监听操作系统会自动分配临时端口。连接成功之后进入循环我们在控制台输入内容发送给服务器等待服务器处理完毕后接收返回结果并打印出来。回响、字典翻译、远程命令这三套业务都可以用这同一个客户端去测试。#include iostream #include Common.hpp #include InetAddr.hpp void Usage(std::string proc) { std::cerrUsage: procportstd::endl; } //./tcpclient server_ip server_port int main(int argc,char* argv[]) { if(argc!3) { Usage(argv[0]); exit(USAGE_ERR); } std::string serveripargv[1]; uint16_t serverportstd::stoi(argv[2]); //1.创建套接字 int sockfdsocket(AF_INET,SOCK_STREAM,0); if(sockfd0) { std::cerrsocket errostd::endl; exit(SOCKET_ERR); } //2.不需要显示的绑定随机选择端口号也不需要listen、accept //3.进行连接 InetAddr serveraddr(serverip,serverport); int nconnect(sockfd,serveraddr.NetAddptr(),serveraddr.Netaddlen()); if(n0) { std::cerrconnect errostd::endl; exit(CONNECT_ERR); } while(true) { std::string line; std::coutPlease Enter; std::getline(std::cin,line); write(sockfd,line.c_str(),line.size()); char buffer[1024]; ssize_t sizeread(sockfd,buffer,sizeof(buffer)-1); if(size0) { buffer[size]0; std::coutserver echo#bufferstd::endl; } } close(sockfd); return 0; }七、Tcpserver.cc这是 TCP 服务器程序的入口文件。得益于我们之前设计的通用服务器框架网络收发逻辑已经被封装好了我们只需要在这里选择对应的业务回调传给服务器就能切换不同功能。defaulthandler就是我们的回响测试回调。客户端发来的数据存到 word 里面函数把一段固定文字和客户端发送的内容拼在一起最后返回给服务器再由服务器发回客户端。客户端输入什么内容最后就能在收到的结果里看到自己刚才发送的数据以此验证服务器‑客户端通信是否正常。用 lambda 把 远程命令业务/字典业务/回响业务打包成回调函数传给 TcpServer。注意lambda 的形参名字不要和捕获进来的外部变量名字一模一样。一旦重名lambda 内部的参数就会 “盖住” 外部捕获的变量编译器就分不清我们到底想用哪一个容易引发编译错误。#include TcpSever.hpp #include Common.hpp #include Dict.hpp #include Command.hpp std::string defaulthandler(const std::stringword,InetAddr peer) { LOG(Loglevel::INFO)回到了默认的回调函数; std::string szihanhh; sword; return s; } void Usage(std::string proc) { std::cerrUsage: procportstd::endl; } //./tcpclient port int main(int argc,char* argv[]) { if(argc!2) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t portstd::stoi(argv[1]); Enable_Console_LogStrategy(); //创建字典模块 // Dict d; // d.Loaddict(); //执行远程执行模块 Command cmd; //捕获变量 和 形参不能重名 std::unique_ptrTcpServer tsvrstd::make_uniqueTcpServer(port,[cmd](const std::string command ,InetAddr addr){ return cmd.Excute(command,addr); }); // std::unique_ptrTcpServer tsvr std::make_uniqueTcpServer(port, [d](const std::string word, InetAddr addr) // { return d.Translate(word, addr); }); // std::unique_ptrTcpServer tsvrstd::make_uniqueTcpServer(port,defaulthandler); tsvr-Init(); tsvr-Run(); return 0; }八、案例演示8.1回响案例8.2字典案例8.3远程执行案例好啦以上就是本文全部内容啦这三种业务无缝切换通用 TCP 服务器框架实战完成啦。