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

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

💗系列专栏: 【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); // 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 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); // 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 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/932179.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言控制语句与案例

控制语句与案例 1. 选择结构 1.1 if 语句 if 语句用于根据条件执行不同的代码块。最基本的语法形式如下&#xff1a; // 单分支 if (条件) {// 条件为真时执行的代码 }// 双分支 if (条件) {// 条件为真时执行的代码 } else {// 条件为假时执行的代码 }// 多分支 if (条件1…

【分子材料发现】——GAP:催化过程中吸附构型的多模态语言和图学习(数据集处理详解)(二)

Multimodal Language and Graph Learning of Adsorption Configuration in Catalysis https://arxiv.org/abs/2401.07408Paper Data: https://doi.org/10.6084/m9.figshare.27208356.v2 1 Dataset CatBERTa训练的文本字符串输入来源于Open Catalyst 2020 &#xff08;OC20…

SpringBoot自动配置底层核心源码

SpringBoot底层核心源码 一、工程创建二、进一步改造三、自动配置 探究SpringBoot的自动配置原理&#xff0c;我们可以自己写一个启动类的注解。 一、工程创建 首先创建一个工程&#xff0c;工程目录如下&#xff1a; 自定义一个启动函数&#xff1a; package org.springboo…

【Springboot3+vue3】从零到一搭建Springboot3+vue3前后端分离项目之后端环境搭建

【Springboot3vue3】从零到一搭建Springboot3vue3前后端分离项目&#xff0c;整合knef4j和mybaits实现基础用户信息管理 后端环境搭建1.1 环境准备1.2 数据库表准备1.3 SpringBoot3项目创建1.4 MySql环境整合&#xff0c;使用druid连接池1.5 整合mybatis-plus1.5.1 引入mybatie…

【书生大模型实战营】Linux 基础知识-L0G1000

前言&#xff1a;书生大模型实战营是上海人工智能实验室开展的大模型系列实践活动&#xff0c;提供免费算力平台&#xff0c;学员通过闯关式任务&#xff0c;可获得免费算力和存储&#xff0c;助力项目实践。本期是第4期&#xff0c;时间从十一月份开始&#xff0c;持续到十二月…

JS进阶DAY3|事件(二)事件流

目录 一、事件流说明 1.1 事件流概念 1.2 事件捕获阶段 1.3 事件冒泡阶段 二、事件传播的两个阶段说明 2.1 事件捕获 2.2 事件冒泡 3.3 示例代码 三、阻止冒泡 四、事件解绑 4.1 removeEventListener方法 4.2 使用 DOM0 级事件属性 4.3 使用一次性事件监听器 一、…

【AI工具】强大的AI编辑器Cursor详细使用教程

目录 一、下载安装与注册 二、内置模型与配置 三、常用快捷键 四、项目开发与问答 五、注意事项与技巧 参考资料 近日&#xff0c;由四名麻省理工学院&#xff08;MIT&#xff09;本科生共同创立的Anysphere公司宣布&#xff0c;其开发的AI代码编辑器Cursor在成立短短两年…

【AWR软件】AWR 软件添加电磁结构

文章目录 前言步骤 前言 微波虚拟 实验 步骤 project -> add em struture -> new em structure 输入名称&#xff0c;create. 添加端口&#xff1a;add edge port

uni-app登录界面样式

非常简洁的登录、注册界面模板&#xff0c;使用uni-app编写&#xff0c;直接复制粘贴即可&#xff0c;无任何引用&#xff0c;全部公开。 废话不多说&#xff0c;代码如下&#xff1a; login.vue文件 <template><view class"screen"><view class"…

普通算法——一维前缀和

一维前缀和 题目链接&#xff1a;https://www.acwing.com/problem/content/797/ 题目描述&#xff1a; 输入一个长度为 n 的整数序列。接下来再输入 m 个询问&#xff0c;每个询问输入一对 l,r。对于每个询问&#xff0c;输出原序列中从第 l 个数到第 r 个数的和。 **什么是…

小程序项目的基本组成结构

分类介绍 项目根目录下的文件及文件夹 pages文件夹 用来存放所有小程序的页面&#xff0c;其中每个页面都由4个基本文件组成&#xff0c;它们分别是&#xff1a; .js文件&#xff1a;页面的脚本文件&#xff0c;用于存放页面的数据、事件处理函数等 .json文件&#xff1a;…

【Go 基础】并发相关

并发相关 CAS CAS算法&#xff08;Compare And Swap&#xff09;&#xff0c;是原⼦操作的⼀种,&#xff0c;CAS 算法是⼀种有名的⽆锁算法。⽆锁编程&#xff0c;即不使⽤锁的情况下实现多线程之间的变量同步。可⽤于在多线程编程中实现不被打断的数据交换操作&#xff0c;从…

【H2O2|全栈】Node.js与MySQL连接

目录 前言 开篇语 准备工作 初始配置 创建连接池 操作数据库 封装方法 结束语 前言 开篇语 本节讲解如何使用Node.js实现与MySQL数据库的连接&#xff0c;并将该过程进行函数封装。 与基础部分的语法相比&#xff0c;ES6的语法进行了一些更加严谨的约束和优化&#…

基于人工智能的新中高考综合解决方案

1. 引言 近年来&#xff0c;随着人工智能技术的飞速发展&#xff0c;教育领域也迎来了深刻的变革。针对新中高考改革的需求&#xff0c;本解决方案集成了科大讯飞在人工智能领域的核心技术&#xff0c;旨在通过智能化手段提升教育教学效率与质量&#xff0c;助力学生全面发展。…

【Linux基础】yum 与 vim 的操作

目录 Linux 应用商店——yum yum和yum源是什么 关于镜像的简单理解 yum 的基本操作 yum的安装 yum install 命令 yum查看软件包 yum list 命令 yum的卸载 yum remove 命令 关于 rzsz 软件 安装 rzsz 软件&#xff1a; rz 命令 sz 命令 yum 源拓展 Linux 编辑器…

Elasticsearch数据迁移(快照)

1. 数据条件 一台原始es服务器&#xff08;192.168.xx.xx&#xff09;&#xff0c;数据迁移后的目标服务器&#xff08;10.2.xx.xx&#xff09;。 2台服务器所处环境&#xff1a; centos7操作系统&#xff0c; elasticsearch-7.3.0。 2. 为原始es服务器数据创建快照 修改elas…

【MySQL】数据类型的注意点和应用

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

首次打开韦东山提供的Ubuntu-18.04镜像后,该做哪些事?

目录 01-测试有无网络02-配置最基本的嵌入式开发环境(安装tftp-nfs等)03-缩短关机强制结束进行时间04-关闭软件的自动更新05-未完待续... 01-测试有无网络 ping www.baidu.com 02-配置最基本的嵌入式开发环境(安装tftp-nfs等) 需要安装 tftp&#xff0c;nfs&#xff0c;vim …

2030. gitLab A仓同步到B仓

文章目录 1 A 仓库备份 到 B 仓库2 B 仓库修改main分支的权限 1 A 仓库备份 到 B 仓库 #!/bin/bash# 定义变量 REPO_DIR"/home/xhome/opt/git_sync/zz_xx_xx" # 替换为你的本地库A的实际路径 REMOTE_ORIGIN"http://192.168.1.66:8181/zzkj_software/zz_xx_xx.…

Python与C++混合编程的优化策略与实践

在现代软件开发中&#xff0c;混合编程已成为一种普遍的开发模式。这种模式能够充分发挥不同编程语言的优势&#xff0c;实现性能与开发效率的最佳平衡。本文将深入探讨Python和C混合编程的策略与实践经验。 混合编程就像建造一座现代化的大厦&#xff0c;C就像大厦的钢筋混凝…