✨个人主页: 熬夜学编程的小林
?系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
1、TcpServer.hpp
1.1、TcpServer类基本结构
1.2、 Execute()
2、Command.hpp
2.1、Command类基本结构
2.2、构造析构函数
2.3、SafeCheck()
2.4、HandlerCommand()
3、TcpServerMain.cc
4、完整代码
4.1、TcpServer.hpp
4.2、Command.hpp
4.3、TcpServerMain.cc
上一弹使用TCP协议实现客户端与服务端的通信,此弹实现一个类似于XShell的功能,客户端发出命令,服务端执行命令,并将执行结果返回给客户端!基本结构还是上一弹的结构,此处使用多线程版本即可,无需线程池的代码!
因为客户端的方法需要在TcpServer.hpp.hpp中声明,因此先讲解TcpServer.hpp!
1、TcpServer.hpp
TcpServer.hpp封装TcpServer类!
TcpServer类相较于上一弹只需要稍微修改即可,首先因为需要执行XShell的功能,必不可少的就是函数方法!
函数方法声明:
// sockfd 用于接收消息和发送消息,addr 用于查看是谁发送的using command_service_t = std::function<void(int sockfd,InetAddr addr)>;
1.1、TcpServer类基本结构
TcpServer类基本结构只需加一个方法成员变量,构造函数加该方法初始化即可!
using command_service_t = std::function<void(int sockfd,InetAddr addr)>;// 面向字节流class TcpServer{public: TcpServer(command_service_t service,uint16_t port = gport) :_service(service), _port(port),_listensockfd(gsockfd),_isrunning(false) {} void InitServer(); void Loop(); ~TcpServer();private: uint16_t _port; int _listensockfd; bool _isrunning; command_service_t _service;};
1.2、 Execute()
Execute()执行回调函数!
static void *Execute(void *args){ ThreadData *td = static_cast<ThreadData *>(args); pthread_detach(pthread_self()); // 分离新线程,无需主线程回收 td->_self->_service(td->_sockfd,td->_addr); // 执行回调 ::close(td->_sockfd); delete td; return nullptr;}
2、Command.hpp
Command类实现类似于XShell的功能,但是需要注意不要让所有命令都可以执行,因为可能会导致删库等相关的问题,因此成员变量可以使用set容器存储允许执行命令的前缀!
2.1、Command类基本结构
成员变量使用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("touch"); // touch filename _safe_command.insert("pwd"); _safe_command.insert("whoami"); _safe_command.insert("which"); // which pwd}~Command(){}
2.3、SafeCheck()
安全检查函数检查字符串的前缀,如果与set容器中的其中一个内容相同则返回true,不相同则返回false!
此处用到C语言的字符串比较函数,比较前n个字节,相等则返回0!
strncmp()
#include <string.h>int strncmp(const char *s1, const char *s2, size_t n);
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()函数直接执行C语言字符串的命令,如果执行成功(返回值不为空)以行读取的方式将结果拼接到result字符串中,但是有些命令没有执行结果,此时打印success,执行失败(返回值为空)则返回Execute error。
注意:前提需要判断命令是否安全,不安全直接返回unsafe!
popen()
创建一个管道,并将该管道与一个命令(通过shell执行)的输入或输出连接起来。
#include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);
参数
command
: 一个指向以null结尾的字符串的指针,该字符串包含了要执行的命令。type
: 一个指向以null结尾的字符串的指针,该字符串决定了管道的方向。它可以是 "r"
(表示读取命令的输出)或 "w"
(表示向命令写入输入)。 返回值
成功时,popen
返回一个指向FILE
对象的指针(与fopen函数一样),该对象可用于fread
、fwrite
、fprintf
、fscanf
等标准I/O函数。失败时,返回nullptr
,并设置errno
以指示错误。 Excute()
// 安全执行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";}
2.4、HandlerCommand()
命令处理函数是一个长服务(死循环),先接收客户端的信息,如果接收成功则处理收到的消息(命令),并将处理的结果发送给客户端,如果读到文件结尾或者接收失败则退出循环!
此处换一批接收消息和发送的函数,与read和write还是基本一致的,有稍微差别!
recv()
与套接字(sockets)一起使用,用于从连接的对等端接收数据。
#include <sys/types.h>#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数
sockfd
: 套接字描述符,标识一个打开的套接字。buf
: 指向一个缓冲区的指针,该缓冲区用于存储接收到的数据。len
: 指定缓冲区的长度(以字节为单位),即recv
函数最多可以接收的数据量。flags
: 通常设置为0,但也可以指定一些特殊的标志来修改recv
的行为。例如,MSG_PEEK
标志允许程序查看数据而不从套接字缓冲区中移除它。 返回值
成功时,recv
返回实际接收到的字节数。如果连接已经正常关闭,返回0。失败时,返回-1,并设置errno
以指示错误类型。 send()
与套接字(sockets)一起使用,用于向连接的对等端发送数据。
#include <sys/types.h>#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数
sockfd
: 套接字描述符,标识一个打开的套接字。buf
: 指向包含要发送数据的缓冲区的指针。len
: 指定要发送的数据的字节数。flags
: 通常设置为0,但也可以指定一些特殊的标志来修改send
的行为。例如,MSG_DONTWAIT
标志可以使send
函数在非阻塞套接字上立即返回,如果无法立即发送数据则返回错误。 返回值
成功时,send
返回实际发送的字节数。这个值可能小于len
,特别是当套接字是非阻塞的或发送缓冲区已满时。失败时,返回-1,并设置errno
以指示错误类型。 void HandlerCommand(int sockfd, InetAddr addr){ // 我们把他当做一个长服务 while (true) { char commandbuffer[1024]; // 当做字符串 // 1.接收消息(read) ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1,0); // TODO if (n > 0) { commandbuffer[n] = 0; LOG(INFO, "get command from client [%s],command: %s\n", addr.AddrStr().c_str(), commandbuffer); std::string result = Excute(commandbuffer); // 2.发送消息(write) ::send(sockfd, result.c_str(), result.size(),0); } // 读到文件结尾 else if (n == 0) { LOG(INFO, "client %s quit\n", addr.AddrStr().c_str()); break; } else { LOG(ERROR, "read error\n", addr.AddrStr().c_str()); break; } }}
3、TcpServerMain.cc
服务端主函数使用智能指针构造Server对象(参数需要加执行方法),然后调用初始化与执行函数,调用主函数使用该可执行程序 + 端口号!
注意:声明的函数方法只有两个参数,而Command类的命令行处理函数有this指针,因此需要使用bind()绑定函数!
// ./tcpserver 8888int main(int argc, char *argv[]){ if (argc != 2) { std::cerr << "Usage: " << argv[0] << " local-post" << std::endl; exit(0); } uint16_t port = std::stoi(argv[1]); Command cmdservice; std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>( std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2), port); // 绑定函数 tsvr->InitServer(); tsvr->Loop(); return 0;}
运行结果
4、完整代码
4.1、TcpServer.hpp
#pragma once#include <iostream>#include <functional>#include <cstring>#include <unistd.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <sys/wait.h>#include <pthread.h>#include "Log.hpp"#include "InetAddr.hpp"using namespace log_ns;enum { SOCKET_ERROR, BIND_ERROR, LISTEN_ERROR};const static uint16_t gport = 8888;const static int gsockfd = -1;const static int gblcklog = 8;using command_service_t = std::function<void(int sockfd,InetAddr addr)>;// 面向字节流class TcpServer{public: TcpServer(command_service_t service,uint16_t port = gport) :_service(service), _port(port),_listensockfd(gsockfd),_isrunning(false) {} void InitServer() { // 1.创建socket _listensockfd = ::socket(AF_INET,SOCK_STREAM,0); if(_listensockfd < 0) { LOG(FATAL,"socket create eror\n"); exit(SOCKET_ERROR); } LOG(INFO,"socket create success,sockfd: %d\n",_listensockfd); // 3 struct sockaddr_in local; memset(&local,0,sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; // 2.bind sockfd 和 socket addr if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)) < 0) { LOG(FATAL,"bind eror\n"); exit(BIND_ERROR); } LOG(INFO,"bind success\n"); // 3.因为tcp是面向连接的,tcp需要未来不短地获取连接 // 老板模式,随时等待被连接 if(::listen(_listensockfd,gblcklog) < 0) { LOG(FATAL,"listen eror\n"); exit(LISTEN_ERROR); } LOG(INFO,"listen success\n"); } // 内部类 class ThreadData { public: int _sockfd; TcpServer* _self; InetAddr _addr; public: ThreadData(int sockfd,TcpServer* self,const InetAddr &addr) :_sockfd(sockfd),_self(self),_addr(addr) {} }; void Loop() { _isrunning = true; while(_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 1.获取新连接 int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len); // 获取失败继续获取 if(sockfd < 0) { LOG(WARNING,"sccept reeor\n"); continue; } InetAddr addr(client); LOG(INFO,"get a new link,client info: %s,sockfd:%d\n",addr.AddrStr().c_str(),sockfd); // 4 // 获取成功 // version 2 -- 多线程版 -- 不能关闭fd了,也不需要 pthread_t tid; ThreadData *td = new ThreadData(sockfd, this,addr); pthread_create(&tid,nullptr,Execute,td); // 新线程分离 } _isrunning = false; } // 无法调用类内成员 无法看到sockfd static void *Execute(void *args) { ThreadData *td = static_cast<ThreadData *>(args); pthread_detach(pthread_self()); // 分离新线程,无需主线程回收 td->_self->_service(td->_sockfd,td->_addr); // 执行回调 ::close(td->_sockfd); delete td; return nullptr; } ~TcpServer() {}private: uint16_t _port; int _listensockfd; bool _isrunning; command_service_t _service;};
4.2、Command.hpp
#pragma once#include <iostream>#include <set>#include <string>#include <cstring>#include <cstdio>#include "Log.hpp"#include "InetAddr.hpp"using namespace log_ns;class Command{public: Command() { // 白名单 _safe_command.insert("ls"); _safe_command.insert("touch"); // touch filename _safe_command.insert("pwd"); _safe_command.insert("whoami"); _safe_command.insert("which"); // which pwd } 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"; } void HandlerCommand(int sockfd, InetAddr addr) { // 我们把他当做一个长服务 while (true) { char commandbuffer[1024]; // 当做字符串 // 1.接收消息(read) ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1,0); // TODO if (n > 0) { commandbuffer[n] = 0; LOG(INFO, "get command from client [%s],command: %s\n", addr.AddrStr().c_str(), commandbuffer); std::string result = Excute(commandbuffer); // 2.发送消息(write) ::send(sockfd, result.c_str(), result.size(),0); } // 读到文件结尾 else if (n == 0) { LOG(INFO, "client %s quit\n", addr.AddrStr().c_str()); break; } else { LOG(ERROR, "read error\n", addr.AddrStr().c_str()); break; } } } ~Command() { }private: std::set<std::string> _safe_command; // 只允许执行的命令};
4.3、TcpServerMain.cc
#include "TcpServer.hpp"#include "Command.hpp"#include <memory>// ./tcpserver 8888int main(int argc, char *argv[]){ if (argc != 2) { std::cerr << "Usage: " << argv[0] << " local-post" << std::endl; exit(0); } uint16_t port = std::stoi(argv[1]); Command cmdservice; std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>( std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2), port); // 绑定函数 tsvr->InitServer(); tsvr->Loop(); return 0;}