【Linux网络】构建类似XShell功能的TCP服务器
📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨
文章目录
- 🏳️🌈一、TcpServer.hpp
- 1.1 基本结构
- 1.2 多线程处理函数
- 🏳️🌈二、Common.hpp
- 2.1 基本结构
- 2.2 构造、析构函数
- 2.3 SafeCheck()
- 2.4 Excute()
- 2.5 HandlerCommand()
- 🏳️🌈三、TcpServer.cpp
- 🏳️🌈四、测试
- 🏳️🌈五、整体代码
- 5.1 TcpServer.hpp
- 5.2 TcpServer.cpp
- 5.3 Command.hpp
- 👥总结
在上一篇文章中,笔者带大家实现了TCP服务器的 4种 客户端与服务端通信的模式 ,分别是单执行流模式、多进程模式、多线程模式、以及线程池模式
这一篇,我将带大家进一步理解TCP,就从用TCP实现类XShell功能服务器 ,采用的是多线程版本
哦
🏳️🌈一、TcpServer.hpp
其他部分保持不变,我们添加一个处理回调函数
,用来判断和执行相应的 xshell
命令
1.1 基本结构
using handler_t = std::function<std::string(int sockfd, InetAddr addr)>;class TcpServer{public:// 构造函数TcpServer(handler_t handler,uint16_t port = gport):_handler(handler), _port(port), _isrunning(false){}// 初始化void InitServer(){}// server - 2 多线程版本void Loop(){}// 析构函数~TcpServer(){}private:int _listensockfd; // 监听socketuint16_t _port;bool _isrunning;handler_t _handler;
};
1.2 多线程处理函数
我们之前使用 Server
进行回显处理,这里将 Execute 函数的处理方法从 Server 改成 _handler就行了
// 线程函数static void* Execute(void* args){ThreadDate* td = static_cast<ThreadDate*>(args);// 子线程结束后由系统自动回收资源,无需主线程调用 pthread_joinpthread_detach(pthread_self()); // 分离新线程,无需主线程回收td->_self->_handler(td->_sockfd, td->_addr);delete td;return nullptr;}
🏳️🌈二、Common.hpp
2.1 基本结构
Command
类实现类似于XShell的功能,但是需要注意要让所有命令都可以执行,因为可能会导致删库等相关的问题。不成员变量可以使用set容器存储允许执行命令的前缀!
class Command{public:Command(){}bool SafeCheck(const std::string& cmdstr){}std::string Excute(const std::string& cmdstr){}void HandlerCommand(int sockfd, InetAddr addr){}~Command(){}private:std::set<std::string> _safe_command; // 允许执行的命令
};
2.2 构造、析构函数
根据自己的需要,在构造函数种将允许使用的命令插入到容器,析构函数无需处理!
Command() {// 白名单_safe_command.insert("ls");_safe_command.insert("pwd");_safe_command.insert("touch");_safe_command.insert("whoami");_safe_command.insert("which");
}
~Command(){}
2.3 SafeCheck()
检查当前命令是否在白名单中
bool SafeCheck(const std::string& cmdstr){for(auto& cmd : _safe_command){// 值比较命令开头if(strncmp(cmd.c_str(), cmdstr.c_str(), cmd.size()) == 0){return true;}}return false;}
2.4 Excute()
写这部分代码时,我们需要用到一个函数 - popen
popen 的作用是 创建子进程执行系统命令,并通过管道(Pipe)与其通信,他会启动子进程,调用 /bin/sh(或其他默认 shell)解析并执行指定的命令(如 ls -l、grep “error” 等)。
std::string Excute(const std::string& cmdstr) {// 检查是否安全,不安全返回if (!SafeCheck(cmdstr)) {return "unsafe";}std::string result;// popen 创建子进程执行系统命令,并通过管道(Pipe)与其通信// popen(const char* command, const char* type)// command: 命令字符串// type: 管道类型,"r"表示读,"w"表示写,"r+"表示读写// 返回文件指针,失败返回NULLFILE* fp = popen(cmdstr.c_str(), "r");if (fp) {// 读取子进程的输出// 一行读取char line[1024];while (fgets(line, sizeof(line), fp)) {result += line;}return result.empty() ? "success" : result; // 有些命令创建无返回值}
}
2.5 HandlerCommand()
命令处理函数是一个长服务(死循环),先接收客户端的信息,如果接收成功则处理收到的消息(命令),并将处理的结果发送给客户端,如果读到文件结尾或者接收失败则退出循环!
void HandlerCommand(int sockfd, InetAddr addr) {// 我们把它当作一个长服务while (true) {char commandbuffer[1024]; // 接收命令的缓冲区// 1. 接收消息// recv(int sockfd, void* buf, size_t len, int flags)// sockfd: 套接字描述符// buf: 接收缓冲区// len: 接收缓冲区大小// flags: 接收标志 0表示阻塞,非0表示非阻塞ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1, 0);if (n > 0) {commandbuffer[n] = 0;LOG(LogLevel::INFO) << "get command from client" << addr.AddrStr()<< ":" << commandbuffer;std::string result = Excute(commandbuffer);// 2. 发送消息// send(int sockfd, const void* buf, size_t len, int flags)// sockfd: 套接字描述符// buf: 发送缓冲区// len: 发送缓冲区大小// flags: 发送标志 0表示不阻塞,非0表示阻塞::send(sockfd, result.c_str(), result.size(), 0);}// 读到文件结尾else if (n == 0) {LOG(LogLevel::INFO) << "client " << addr.AddrStr() << " quit";break;} else {LOG(LogLevel::ERROR) << "read error from client " << addr.AddrStr();break;}}
}
🏳️🌈三、TcpServer.cpp
服务端主函数使用智能指针构造Server对象(参数需要加执行方法) ,然后调用初始化与执行函数,调用主函数使用该可执行程序 + 端口号!
需要注意的是,Command::HandlerCommand 是 非静态成员函数,调用时必须通过 Command 类的实例(如 cmdservice)来访问。
#include "TcpServer.hpp"
#include "Command.hpp"int main(int argc, char* argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;Die(1);}uint16_t port = std::stoi(argv[1]);Command cmdservice;std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(// &Command::HandlerCommand:成员函数指针。// &cmdservice:Command 对象的实例指针(this 指针)// _1 和 _2:占位符,表示回调函数接受两个参数(int sockfd 和 InetAddr addr)std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2),port);tsvr->InitServer();tsvr->Loop();return 0;
}
🏳️🌈四、测试
🏳️🌈五、整体代码
5.1 TcpServer.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"#define BACKLOG 8using namespace LogModule;
using namespace ThreadPoolModule;static const uint16_t gport = 8080;using handler_t = std::function<void(int sockfd, InetAddr addr)>;class TcpServer{public:// 构造函数TcpServer(handler_t handler,uint16_t port = gport):_handler(handler), _port(port), _isrunning(false){}// 初始化void InitServer(){// 1. 创建 socket_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){LOG(LogLevel::ERROR) << "create socket error: " << strerror(errno);Die(2);}LOG(LogLevel::INFO) << "create sockfd success: " << _listensockfd;struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = htonl(INADDR_ANY);// 2. 绑定 socketif(::bind(_listensockfd, CONV(&local), sizeof(local)) < 0){LOG(LogLevel::ERROR) << "bind socket error: " << strerror(errno);Die(3);}LOG(LogLevel::INFO) << "bind sockfd success: " << _listensockfd;// 3. 因为 tcp 是面向连接的,tcp需要未来不断地获取连接// listen 就是监听连接的意思,所以需要设置一个队列,来保存等待连接的客户端// 队列的长度为 8,表示最多可以有 8 个客户端等待连接// listen(int sockfd, int backlog)// sockfd 就是之前创建的 socket 句柄// backlog 就是队列的长度// 返回值:成功返回 0,失败返回 -1if(::listen(_listensockfd, BACKLOG) < 0){LOG(LogLevel::ERROR) << "listen socket error: " << strerror(errno);Die(4);}LOG(LogLevel::INFO) << "listen sockfd success: " << _listensockfd;}// server - 2 多线程版本// 为每个新连接分配独立的线程处理业务逻辑// (Loop 函数)通过 accept 循环监听新连接,为每个新连接创建线程(pthread_create)传递连接信息给子线程(通过 ThreadDate 结构体)// (Execute 函数)调用 pthread_detach 分离自身(避免主线程调用 pthread_join)执行 Server 函数处理具体业务逻辑线程结束后自动释放资源(通过 delete td)// (Server 函数)循环读取客户端数据(read),处理业务逻辑(示例中的回显服务),发送响应(write)关闭连接(close(sockfd))void Loop(){_isrunning = true;while(_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// 1. 获取新连接int sockfd = ::accept(_listensockfd, CONV(&client), &len);if(sockfd < 0){LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);continue;}InetAddr cli(client);LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr() << " sockfd: " << sockfd;// 获取成功pthread_t tid;ThreadDate* td = new ThreadDate(sockfd, this, cli);// pthread_create 第一个参数是线程id,第二个参数是线程属性,第三个参数是线程函数,第四个参数是线程函数参数pthread_create(&tid, nullptr, Execute, td);}_isrunning = false;}// 线程函数参数对象class ThreadDate{public:int _sockfd;TcpServer* _self;InetAddr _addr;public:ThreadDate(int sockfd, TcpServer* self, const InetAddr& addr): _sockfd(sockfd), _self(self), _addr(addr){}};// 线程函数static void* Execute(void* args){ThreadDate* td = static_cast<ThreadDate*>(args);// 子线程结束后由系统自动回收资源,无需主线程调用 pthread_joinpthread_detach(pthread_self()); // 分离新线程,无需主线程回收td->_self->_handler(td->_sockfd, td->_addr);delete td;return nullptr;}// 析构函数~TcpServer(){}private:int _listensockfd; // 监听socketuint16_t _port;bool _isrunning;handler_t _handler;
};
5.2 TcpServer.cpp
#include "TcpServer.hpp"
#include "Command.hpp"int main(int argc, char* argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;Die(1);}uint16_t port = std::stoi(argv[1]);Command cmdservice;std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(// &Command::HandlerCommand:成员函数指针。// &cmdservice:Command 对象的实例指针(this 指针)// _1 和 _2:占位符,表示回调函数接受两个参数(int sockfd 和 InetAddr addr)std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2),port);tsvr->InitServer();tsvr->Loop();return 0;
}
5.3 Command.hpp
#pragma once#include <iostream>
#include <set>
#include <cstring>
#include <cstdio>#include "InetAddr.hpp"
#include "Log.hpp"using namespace LogModule;class Command{public:Command(){// 白名单_safe_command.insert("ls");_safe_command.insert("pwd");_safe_command.insert("touch");_safe_command.insert("whoami");_safe_command.insert("which");}bool SafeCheck(const std::string& cmdstr){for(auto& cmd : _safe_command){// 值比较命令开头if(strncmp(cmd.c_str(), cmdstr.c_str(), cmd.size()) == 0){return true;}}return false;}std::string Excute(const std::string& cmdstr){// 检查是否安全,不安全返回if(!SafeCheck(cmdstr)){return "unsafe";}std::string result;FILE* fp = popen(cmdstr.c_str(), "r");if(fp){char line[1024];while(fgets(line, sizeof(line), fp)){result += line;}return result.empty() ? "success" : result; // 有些命令创建无返回值}return "Execute error";}std::string Excute(std::string& cmdstr){// 检查是否安全,不安全返回if(!SafeCheck(cmdstr)){return "unsafe";}std::string result;// popen 创建子进程执行系统命令,并通过管道(Pipe)与其通信// popen(const char* command, const char* type)// command: 命令字符串// type: 管道类型,"r"表示读,"w"表示写,"r+"表示读写// 返回文件指针,失败返回NULLFILE* fp = popen(cmdstr.c_str(), "r");if(fp){// 读取子进程的输出// 一行读取char line[1024];while(fgets(line, sizeof(line), fp)){result += line;}return result.empty() ? "success" : result; // 有些命令创建无返回值}}void HandlerCommand(int sockfd, InetAddr addr){// 我们把它当作一个长服务while(true){char commandbuffer[1024]; // 接收命令的缓冲区// 1. 接收消息// recv(int sockfd, void* buf, size_t len, int flags)// sockfd: 套接字描述符// buf: 接收缓冲区// len: 接收缓冲区大小// flags: 接收标志 0表示阻塞,非0表示非阻塞ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1, 0); if(n > 0){commandbuffer[n] = 0;LOG(LogLevel::INFO) << "get command from client" << addr.AddrStr() << ":" << commandbuffer;std::string result = Excute(commandbuffer);// 2. 发送消息// send(int sockfd, const void* buf, size_t len, int flags)// sockfd: 套接字描述符// buf: 发送缓冲区// len: 发送缓冲区大小// flags: 发送标志 0表示不阻塞,非0表示阻塞::send(sockfd, result.c_str(), result.size(), 0);}// 读到文件结尾else if(n == 0){LOG(LogLevel::INFO) << "client " << addr.AddrStr() << " quit";break;}else{LOG(LogLevel::ERROR) << "read error from client " << addr.AddrStr();break;}}}~Command(){}private:std::set<std::string> _safe_command; // 允许执行的命令
};
👥总结
本篇博文对 【Linux网络】构建类似XShell功能的TCP服务器 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~