【计网】从零开始使用TCP进行socket编程 ---服务端业务模拟Xshell

news2024/9/20 20:48:22

在这里插入图片描述

最糟糕的情况,
不是你出了错,
而是你没有面对出错的勇气。

从零开始使用TCP进行socket编程

  • 1 通信过程的多版本实现
    • 1.1 多进程版本
    • 1.2 多线程版本
  • 2 服务端业务模拟Xshell
    • 2.1 整体框架设计
    • 2.2 Command类设计

1 通信过程的多版本实现

在前一篇的文章中,实现了基于TCP协议的服务端与客户端的通信过程!当时我们是使用“不靠谱版本”,直接通过service函数执行代码,这样导致服务端只能为一个客户端进行服务,另一个客户端进入时就阻塞住了,只有上一个客户端连接退出,才会再次接入新的连接,这样可不行,服务器需要能够同时接入多个客户端!
那么帮助服务端实现同时接入多个客户端的做法有以下两种:

  1. 多进程版本:接收到连接后,创建子进程去执行任务。
  2. 多线程版本:接收到连接后,创建新线程去执行任务。

1.1 多进程版本

我们来实现多进程版本,多进程之前详细讲过:进程控制
创建的子进程会对父进程的数据进行写时拷贝,父子进程分别拥有独立的地址空间,但是需要注意的是:子进程的数据是根据父进程数据写时拷贝获取的,那么文件描述符也会一同拷贝,但是文件只打开了一份!所以为了避免不必要的问题要及时关闭文件描述符!!!

void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // accept接收sockfd
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                sleep(1);
                continue;
            }
            InetAddr addr(client);
            // 读取数据
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);

            //version 2 --- 多进程版本
            int n = fork();
            //signal(SIGCHLD , SIG_IGN);//忽略子进程退出的信息!
            if(n == 0)
            {
                //child
                ::close(_listensockfd);//关闭listen文件 子进程不需要
                if(fork() > 0) exit(0);
                //孙子进程!!!
                //数据会进行写时拷贝 子进程中直接执行任务就可以!
                Service(sockfd, addr);
                exit(0);
            }
            //parent
            ::close(sockfd); //父进程不需要管连接文件!!!

        }
        _isrunning = false;
    }

来看效果:
在这里插入图片描述
现在就可以适配多个客户端的情况了,但是我们知道切换进程时,CPU会切换上下文和热点数据。在并发场景下多进程的不断切换会消耗大量的性能!

而作为轻量级进程的线程就可以避免这样的问题!

1.2 多线程版本

现在我们来实现多线程的版本,我们先使用原生线程:

//...
// version 3 --- 多线程版本
pthread_t tid;
ThreadData td(sockfd , addr , this);
pthread_create(&tid, nullptr, Execute, &td);
pthread_detach(tid) ;//线程分离!!!
//...

这里需要为线程提供一个void*(void*)类型的函数,新线程就去执行这个任务。这个函数中为了可以执行Service任务,我们就需要传入对应的TcpServer类对象的指针、sockfd文件描述符以及InetAddr addr发送者的信息。

那么我们就设计一个结构体,里面储存着这些数据,一起通过void*传入!

class ThreadData
    {
    public:
        int _sockfd;
        InetAddr _addr;
        TcpServer *_this;
    public:
        ThreadData(int sockfd,  InetAddr addr ,TcpServer *p) : _sockfd(sockfd),
                                                              _this(p),
                                                              _addr(addr)
        {
        }
    };

这样在Execute函数中就可以执行任务了

	// 注意设置为静态函数 , 不然参数默认会有TcpServer* this!!!
    static void *Execute(void *args)
    {
        //执行Service函数
        TcpServer::ThreadData* td = static_cast<TcpServer::ThreadData*>(args);
        td->_this->Service(td->_sockfd , td->_addr);
        delete td;
        return nullptr;
    }

来看效果:
在这里插入图片描述
效果非常的好!!!

说到多线程了,那为什么不来使用线程池来实现呢???
线程池实际上并不适合当前场景,TCP通信是长服务,那么这个线程就会长时间运行,不能做到高效率的高并发
也就是说线程池在长服务场景不会提高效率!

但是我们也来实现一下线程池版本,帮助我们巩固知识!

  1. 首先我们设置一个task_t类型,这是线程池中需要执行的任务!
  2. 通过bind包装器将Service函数包装为task_t类型!
  3. 之后就等线程池分配线程执行任务即可!
using task_t = std::function<void()>;
//...
void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // accept接收sockfd
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                sleep(1);
                continue;
            }
            InetAddr addr(client);
            // 读取数据
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);

            // version 4 --- 线程池版本
            task_t t = std::bind(&TcpServer::Service , this , sockfd , addr);
            ThreadPool<task_t>::GetInstance()->Equeue(t);
        }
        _isrunning = false;
    }

来看效果:
在这里插入图片描述

2 服务端业务模拟Xshell

我们实现服务端与客户端的通信逻辑,接下来就来加入业务逻辑!

这次选择的业务逻辑是模拟实现Xshell远程控制主机,之前我们实现过一个本地操作的shell程序在这里我们就实现过识别字符串指令然后进行进程替换执行任务!今天我们不再需要自己编写,我们直接使用popen接口:

NAME
       popen, pclose - pipe stream to or from a process

SYNOPSIS
       #include <stdio.h>
       FILE *popen(const char *command, const char *type);
       int pclose(FILE *stream);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

popen函数中会自动帮我们识别字符串指令,并创建进程去执行,然后将结果通过文件返回!
我们来逐步实现一下!

2.1 整体框架设计

首先我们要做到工作是将各个模块进行解耦:

  1. TcpServer类只负责获取客户端与服务端的连接。进行accept接收客户端连接,然后去执行回调函数任务,再将结果返回给客户端。
  2. Command类负责对字符串指令进行执行,并将结果返回!

为了做到这样的效果,TcpServer类中需要加入回调函数,在构造时就确定好回调函数,然后通过新线程去执行回调函数!回调函数的类型和Service一致:

using command_service_t = std::function<void(int sockfd, InetAddr addr)>;

2.2 Command类设计

Command类首先需要一个对外的HandlerHelper接口,这个接口是作为TcpServer类对象构造时的回调函数。函数中执行的任务就去从连接流中获取客户端传入的数据,通过Execute函数去执行指令任务,并返回对应的结果!

HandlerHelper执行的逻辑其实和原本的Service是一致的:

  1. sockfd文件中获取客户端传入的数据!
  2. 然后传给核心函数去执行任务!
  3. 最后将结果发送回去!

需要注意的是:不是所有这里都可以让客户端执行,如果客户端可以执行rm -rf这样的指令,那么破坏性是很强的,这里可以采用白名单(或黑名单)的方法去规避一下!如果要做到无敌防御就要麻烦的多,这里只是简单模拟一下!

#include <set>
#include <iostream>
#include <string>
#include <cstring>
#include <stdio.h>

#include "InetAddr.hpp"
#include "Log.hpp"

using namespace log_ns;

class Command
{
private:
	//指令白名单 保证安全!
    void InitCommand()
    {
        _command.insert("ls");
        _command.insert("pwd");
        _command.insert("mkdir");
        _command.insert("sleep");
        _command.insert("clear");
        _command.insert("touch");
    }
    bool CheckCommand(std::string &command)
    {
        for (auto &e : _command)
        {
            // LOG( DEBUG , "%s : %s", command.c_str(), e.c_str() );
            if (strncmp(command.c_str(), e.c_str(), e.size()) == 0)
            {
                return true;
            }
        }
        return false;
    }

public:
    Command()
    {
        InitCommand();
    }
    std::string Execute(std::string command)
    {
        // 先进行安全检查
        if (!CheckCommand(command))
        {
            return "Unsafe command!!!";
        }
        // 开始执行指令
        FILE *fp = popen(command.c_str(), "r"); // 以读方式进行
        // 读取结果
        std::string result;
        char line[1024];
        if (fp)
        {
            while (fgets(line, sizeof(line), fp))
            {
                result += line;
            }
            pclose(fp);
             return result.empty() ? "success" : result;
        }
        
        return "execute error";
    }
    void HandlerHelper(int sockfd, InetAddr addr)
    {
        LOG(INFO, "service start!!!\n");
        while (true)
        {
            char buffer[1024];
            ssize_t n = ::recv(sockfd, buffer, sizeof(buffer) - 1, 0);

            if (n > 0)
            {
                buffer[n] = 0;
                LOG(INFO, "sockfd read success!!! buffer: %s\n", buffer);
                std::string str = Execute(buffer);

                send(sockfd, str.c_str(), str.size(), 0);
            }
            else if (n == 0)
            {
                LOG(INFO, "client %s quit!\n", addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
                break;
            }
        }
        ::close(sockfd);
    }
    ~Command()
    {
    }

private:
    std::set<std::string> _command;
};

来看效果:
在这里插入图片描述
非常好!这样我们就完成了Xshell的模拟项目!!!

后续我们来学习序列化与反序列化!!!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鸿蒙手势交互(三:组合手势)

三、组合手势 由多种单一手势组合而成&#xff0c;通过在GestureGroup中使用不同的GestureMode来声明该组合手势的类型&#xff0c;支持顺序识别、并行识别和互斥识别三种类型。 GestureGroup(mode:GestureMode, gesture:GestureType[]) //- mode&#xff1a;为GestureMode枚…

美元降息,对普通人有哪些影响?

美元降息&#xff0c;对普通人有哪些影响&#xff1f; 美元降息了。很多朋友都说我又不炒股&#xff0c;我手里又没有美金&#xff0c;美元跟我有啥关系啊&#xff1f;那我们就来聊聊美元降息&#xff0c;对我们国内经济到底有哪些影响&#xff1f;你再来看看跟你有没有关系&a…

计算机毕业设计 美发管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

MySQL高阶1890-2020年最后一次登录

目录 题目 准备数据 分析数据 题目 编写解决方案以获取在 2020 年登录过的所有用户的本年度 最后一次 登录时间。结果集 不 包含 2020 年没有登录过的用户。 返回的结果集可以按 任意顺序 排列。 准备数据 Create table If Not Exists Logins (user_id int, time_stamp …

数据库-约束与多表查询

1.约束 例子&#xff1a; 外键约束 例子&#xff1a; 2.多表查询 多表关系 概述 内连接 外连接 自连接 联合查询 子查询 介绍 标量子查询 仅有一个值 列子查询 行子查询 表子查询 练习

【应用开发三】 input子系统介绍

文章目录 1 名词解释2 输入设备编程框架2.1 input子系统2.2 读取数据流程2.3 input_event结构体2.3.1 type&#xff08;哪类事件&#xff09;2.2 code&#xff08;具体事件&#xff09;2.3 value&#xff08;数值&#xff09; 2.4 数据同步2.5 读取start input_event数据 1 名词…

微信小程序如何引入第三方插件

前言 微信的文档不行&#xff0c;我这个&#xff0c;行 如何找到插件管理的页面 扫码登录微信小程序的后台设置页面&#xff0c;点击小程序信息的查看详情&#xff0c;然后点第三方设置 修改app.json 在插件管理的页面添加好要用的插件之后&#xff0c;在插件的详情页面找到…

C++学习指南(六)----list

欢迎来到繁星的CSDN。本期内容主要包括&#xff0c;list的介绍、使用以及与vector的优缺点。 一、什么是list 在先前的C语言学习中&#xff0c;我们接触到了顺序表和链表&#xff0c;而在C中&#xff0c;这正好对应了vector&#xff08;动态增长顺序表&#xff09;和l…

机器学习(西瓜书)第 10 章 降维与度量学习

10.1 k近邻学习kNN k 近邻(k-Nearest Neighbor,简称kNN)学习是一种常用的监督学习方法,其工作机制非常简单&#xff1a;给定测试样本&#xff0c;基于某种距离度量找出训练集中与其最靠近的k个训练样本&#xff0c;然后基于这k个 “邻居”的信息来进行预测.通常&#xff0c;在…

常用排序算法时间复杂度和稳定性

以下是常用排序算法时间复杂度和稳定性&#xff0c;也是常考的&#xff1a;

如何衡量企业品牌力?判断指标有哪些?

企业品牌力是指品牌在市场中的竞争力和影响力&#xff0c;它反映了品牌的价值、知名度、忠诚度、感知质量、差异化以及市场表现等方面。要去衡量一个企业的品牌力&#xff0c;大多从品牌的知名度、忠诚度、所占市场份额、顾客口碑、社媒影响力、品牌资产价值等多方面去判断。我…

【计网】从零开始使用TCP进行socket编程 --- 客户端与服务端的通信实现

阵雨后放晴的天空中&#xff0c; 出现的彩虹很快便会消失。 而人心中的彩虹却永不会消失。 --- 太宰治 《斜阳》--- 从零开始使用TCP进行socket编程 1 TCP与UDP2 TCP服务器类2.1 TCP基础知识2.2 整体框架设计2.3 初始化接口2.4 循环接收接口与服务接口 3 服务端与客户端测试…

Jboss CVE-2015-7501 靶场攻略

漏洞介绍 这是经典的JBoss反序列化漏洞&#xff0c;JBoss在/invoker/JMXInvokerServlet请求中读取了⽤户传⼊的对象&#xff0c;然后我们利⽤Apache Commons Collections中的 Gadget 执⾏任意代码 影响范围 JBoss Enterprise Application Platform 6.4.4,5.2.0,4.3.0_CP10 …

使用API有效率地管理Dynadot域名,为域名进行隐私保护设置

前言 Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮箱&…

欧美海外仓系统有哪些服务商选择?

在跨境电商的全球化浪潮中&#xff0c;欧美市场以其成熟的电商生态和庞大的消费群体&#xff0c;成为了众多跨境卖家竞相争夺的高地。为了提升物流效率、降低成本并增强客户体验&#xff0c;海外仓成为了不可或缺的一环。而海外仓系统的选择&#xff0c;则直接关系到仓库的运营…

qt--Qml控件库如何从外部导入

文章目录 两种方案方案1 给项目添加子项目方案2 使用pri文件 综合来说 &#xff1a; 两种方案 方案1 给项目添加子项目 利用git的特性 对应的子项目就是我们的控件库 然后需要哪个控件 在父项目的qrc路径进行导入 即可将控件库里面的控件给导入项目 在使用的时候 使用模…

tomcat中间件漏洞CVE-2017-12615,后台弱口令部署war包,CVE-2020-1938

一.CVE-2017-12615 环境搭建 cd vulhub-master/tomcat/CVE-2017-12615 docker-compose up -d 漏洞复现 http://172.16.1.22 1.⾸⻚抓包&#xff0c;修改为 PUT ⽅式提交 PUT /shell.jsp/ 2.上传成功进行访问&#xff0c;使用Webshell客户端⼯具进⾏连接 二.后台弱口令部…

二、电源滤波器

电源滤波器 1、电源滤波的过程分析! 波形形成过程: 2、计算: 滤波电容的容量和耐压值选择。 学习心得

mysql-死锁

文章目录 1、概念1.1、创建表 account1.2、id 自动创建 主键索引 primary1.3、name 没有创建索引 2、产生死锁的必要条件2.1、此时 name 没有创建 索引 3、如何处理死锁3.1、方式1&#xff1a;等待&#xff0c;直到超时&#xff08;innodb_lock_wait_timeout50s&#xff09;3.2…

软件测试分类篇(上)

目录 引言&#xff1a; 一、为什么要对软件测试进行分类 二、按照测试目标分类 1. 界面测试 2. 功能测试 3. 性能测试 4. 可靠性测试 5. 安全性测试 6. 易用性测试 三、按照执行方式分类 1. 静态测试 2. 动态测试 四、按照测试方法分类 1. 白盒测试 2. 黑盒测试 …