云计算百科
云计算领域专业知识百科平台

【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; // 监听socket
uint16_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_join
pthread_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+"表示读写
// 返回文件指针,失败返回NULL
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; // 有些命令创建无返回值
}
}

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 8

using 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. 绑定 socket
if(::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,失败返回 -1
if(::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_join
pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
td->_self->_handler(td->_sockfd, td->_addr);
delete td;
return nullptr;
}

// 析构函数
~TcpServer(){}

private:
int _listensockfd; // 监听socket
uint16_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+"表示读写
// 返回文件指针,失败返回NULL
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; // 有些命令创建无返回值
}
}
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服务器 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【Linux网络】构建类似XShell功能的TCP服务器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!