【Linux网络编程】第七弹---构建类似XShell功能的TCP服务器:从TcpServer类到主程序的完整实现

ops/2024/12/12 11:02:45/

个人主页: 熬夜学编程的小林

💗系列专栏: 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函数一样),该对象可用于freadfwritefprintffscanf等标准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;
}


http://www.ppmy.cn/ops/141231.html

相关文章

HDFS(Hadoop Distributed File System)

HDFS&#xff08;Hadoop Distributed File System&#xff09;工作原理简介 HDFS 是 Hadoop 的核心组件&#xff0c;设计用于在大规模分布式环境中存储和处理海量数据。以下是其主要工作原理&#xff1a; 1. 架构组成 HDFS 采用主从架构&#xff0c;由以下两类关键节点组成&a…

ragflow连不上ollama的解决方案

由于前期wsl默认装在C盘&#xff0c;后期部署好RagFlow后C盘爆红&#xff0c;在连接ollama的时候一直在转圈圈&#xff0c;问其他人没有遇到这种情况&#xff0c;猜测是因为内存不足无法加载模型导致&#xff0c;今天重新在E盘安装wsl 使用wsl装Ubuntu Win11 wsl-安装教程 如…

windows 脚本批量管理上千台服务器实战案例

如果你们有接触服务器&#xff0c;都是知道服务器有BMC管理界面的&#xff0c;这几天我在做项目中&#xff0c;需要不断的开关机服务器&#xff0c;如果一两台服务器登录BMC界面重启服务器还好&#xff0c;如果服务器数量非常的庞大&#xff0c;成百上千台&#xff0c;我们不可…

JWT报CORSFilter错误原因(以Java SpringBoot为例)

JWT 报 CorsFilter 原因&#xff0c;通常是因为跨域请求未通过浏览器的同源策略检查&#xff0c;而 CorsFilter 是用来处理跨域问题的过滤器。如果后端未正确配置 CORS 或 JWT 的传递方式不符合跨域要求&#xff0c;可能导致此类问题。 以下是具体原因及解决方法&#xff1a; …

Spring Boot读取配置文件的六种方案

从配置文件中获取属性应该是SpringBoot开发中最为常用的功能之一&#xff0c;但就是这么常用的功能&#xff0c;仍然有很多开发者在这个方面踩坑&#xff0c;以下是我整理的几种获取配置属性的方式。 一、Environment 使用 Environment 方式来获取配置属性值非常简单&#xf…

location规则和rewrite重定向

location匹配规则 在nginx当中&#xff0c;匹配的对象一般是uri来匹配 location匹配的分类&#xff1a; 多个location一旦匹配其中之一&#xff0c;就不在匹配其他的location 1、精确匹配 location / {…} :完全相同&#xff0c;一个字符错都匹配不到 2、正则匹配 location ~…

迭代器模式的理解和实践

引言 在软件开发中&#xff0c;我们经常需要遍历容器对象&#xff08;如数组、列表、集合等&#xff09;中的元素。如果每个容器对象都实现自己的遍历算法&#xff0c;那么代码将会变得冗余且难以维护。为了解决这个问题&#xff0c;迭代器模式应运而生。迭代器模式是一种行为型…

Linux之cpu性能分析(Analysis of CPU Performance in Linux)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 本人主要分享计算机核心技…