✨个人主页: 熬夜学编程的小林
💗系列专栏: 【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); // TODOif (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 8888
int 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); // 3struct 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 addrif(::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;}// 无法调用类内成员 无法看到sockfdstatic 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); // TODOif (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 8888
int 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;
}