在这里插入图片描述

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

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的模拟项目!!!

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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐