初识Linux · TCP的基本使用 · 远程执行命令

目录

前言:

InetAddr.hpp

TcpServer.hpp

Command.hpp

tcpserverMain.cc


前言:

前文我们使用TCP实现了一个基本的回显功能,本文我们基于TCP再实现一个比较进阶的功能,预想的结果是主机AB通信,A作为服务器,B作为客户端,B输入常见的命令,能收到该命令在A主机的结果。

 也就是说B可以作为A的分身执行命令了,那么这里我们是不是还要重新写一遍自定义XShell?当然不用,我们这里使用的函数可以完美解决自定义Xshell的问题。

那么话不多说,直接开始编写代码吧!


InetAddr.hpp

该头文件只是为了让我们后来打印更加方便,并且它没有实质性的干货内容,它要做的工作只是用来返回某个主机的IP地址,或者是端口号,并且因为是网络转主机,所以用到的函数和我们之前用到的还有点差别。

首先,对于IP地址来说,网络转主机,我们之前用的函数是inet_ntoa,参数列表为:

返回值是char*,那么因为它的返回值是静态缓冲区的指针,所以并不是线程安全的,举个例子,因为每次调用都会覆盖这个区域的内容,所以:

#include <iostream>
#include <arpa/inet.h>

int main() {
    struct in_addr addr1, addr2;
    inet_aton("192.168.1.1", &addr1);
    inet_aton("10.0.0.1", &addr2);

    // 第一次调用
    char* s1 = inet_ntoa(addr1); // -> "192.168.1.1"

    // 第二次调用会覆盖上一次的返回内容
    char* s2 = inet_ntoa(addr2); // -> "10.0.0.1"

    std::cout << "s1: " << s1 << std::endl; // ❗ 输出是 "10.0.0.1",而不是预期的 "192.168.1.1"
    std::cout << "s2: " << s2 << std::endl;

    return 0;
}

那么因为不是线程安全的,所以不太推荐使用,接下来是第二个函数,inet_ntop,之所以inet_ntop是线程安全是因为它不依赖于任何的内部静态缓冲区,由调用者提供对应的缓冲区,而每个线程都有自己的缓冲区,不共享内存,所以不存在竞争。

对于端口号什么的也没有干货了,以下是完整代码:

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr(sockaddr_in addr)
        :_addr(addr)
    {
        ToHost(addr);
    }

    std::string IP()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }

    std::string Info()
    {
        return "[" + std::to_string(_port) + "]:" + _ip;
    }

private:
    void ToHost(sockaddr_in addr)
    {
        _port = ntohs(addr.sin_port);
        char ip_buff[32];
        inet_ntop(AF_INET, &addr.sin_addr, ip_buff, sizeof(ip_buff));
        _ip = ip_buff;
    }    


private:
    std::string _ip;
    uint16_t _port;
    sockaddr_in _addr;
};

TcpServer.hpp

对于TcpServer.hpp来说,我们要修改的只是处理业务的核心逻辑,所以对于TcpServer.hpp其他部分,同学们可以移步这篇文章:初识Linux · TCP基本使用 · 回显服务器_linux使用tcp-CSDN博客

那么,对于服务器处理业务的时候,我们依然要求低耦合,所以对于处理业务部分,我们放在另一个头文件中,那么服务器要做的事情只是调用对应的方法即可,对于怎么实现的,它不用关心。

所以我们这里就要用到C++11的function,包装一下对应的方法,因为在原来处理逻辑的上,我们的基本参数是sockfd,用于两个进程通信,那么为了方便打印,我们也可以加上对应的Inetaddr。

所以对于ThreadData部分我们就要改一下:

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

那么在Loop里面处理函数的时候,我们保留多线程的方法,源代码几乎没改动,只是加了一个InetAddr:

 // service
            InetAddr addr(client);
            std::cout << "client" << "[" << addr.Port() << "]:" << addr.IP() << std::endl;
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this, addr);
            pthread_create(&tid, nullptr, Excute, td); // 1.成员函数默认有this指针

对于处理函数的时候,也不再是指向某个具体的函数了,而是TcpServer的成员变量:

    static void *Excute(void *args)
    {
        pthread_detach(pthread_self()); // 为了让主线程不等待
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_self->_service(td->_sockfd, td->_addr);
        ::close(td->_sockfd);
        delete td;
        return nullptr;
    }
using command_service_t = std::function<void(int, InetAddr)>;
private:
    uint16_t _port;
    int _listensocket;

    command_service_t _service;

对于TcpServer主要的变动只是处理业务逻辑上的方法调用,对于真正的调用方法,我们单独启一个.hpp文件。


Command.hpp

对于Command.hpp来说,是处理业务的核心逻辑,那么它要执行的操作就是收到B主机输入的字符串,并且将结果返回B主机。

那么涉及到的函数是popen:

popen 函数用于在程序中打开一个进程,通过管道与该进程进行通信。这个函数的基本操作就是输入一个命令,我们可以给它设置对应的选项,其中有w r a,分别是写,读,追加。

那么我们这里就写一个简单的读取操作之类的操作就可以了,并且我们实验完之后,我们也能发现我们的Xshell也是这么工作的。即客户端为长服务,我们输入命令然后它执行完给我们返回:

当然了具体怎么工作的,涉及到的协议是哪个我们不关心,我们了解即可。

而对于常见的命令来说,比如rm which whoami pwd,它们中有的十分危险,比如rm,有的毫无威胁,比如pwd,所以我们就可以设置一个白名单,像pwd这种命令,不用检查,直接使用就可以了,像rm这种危险命令,我们应该拉入黑名单,那么这里我们就以文件操作,设置一个白名单,黑名单同理设置:

class Command
{
public:
    Command()
    {
        SafeCommandLoad();
    }

private:
    void SafeCommandLoad()
    {
        std::fstream file("command.txt");
        if (!file)
        {
            perror("fstream");
        }
        std::string line;

        while (std::getline(file, line))
        {
            if (line.empty())
                continue;
            else
                _cmd_str.insert(line);
        }

        file.close();
    }

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

那么有了白名单之后,也是处理不了对应的业务的,我们需要专门的处理函数,那么针对于处理函数,我们先单独写一个逻辑,然后在另外的函数调用这个函数即可,因为对于service函数来说可能处理的业务不止这一个,所以我们分两个函数进行。

对于Excute函数,我们使用popen函数接受命令行字符串,如果fp不为空,那么文件打开成功,然后是正常的IO,我们定义一个缓冲区,使用getline获取到缓冲区中每行的内容,然后使用字符串接受,最后返回即可。并且不要忘记pclose

这里有一个很有意思的电,比如touch filename,touch是不会返回任意的字符串的,那么如果我们在返回值不加处理,比如不使用三目操作符,返回的就是空字符串,对于后面使用的send来说,直接发送长度为0的字符串没有关系,但是客户端会一直read,而长度为0的字符串,在TCP中实际上是不会发送任何数据,所以会一直阻塞住。

功能read()recv()
阻塞行为
用于 TCP
用于 UDP❌(不建议)
支持 flags✅(如 MSG_PEEK
    std::string Excute(std::string cmd_str)
    {
        FILE *fp = popen(cmd_str.c_str(), "r");
        if (fp)
        {
            std::string result;
            char line[1024];
            while (std::fgets(line, sizeof(line), fp))
            {
                result += line;
            }
            pclose(fp);
            return result.empty() ? "success" : result;
            // return result.empty() ? "success\n[END]" : result + "[END]";

        }
        return "excute error";
    }

那么在Service函数中要执行的操作就是调用Excute函数,执行主机B输入的命令请求,然后通过sent发送出去,这里的操作就非常类似于之前回显功能的函数了,也没有什么干货。

    void Service(int sockfd, InetAddr addr)
    {
        while (true)
        {
            char buffer[1024]; // command buffer
            ssize_t n = ::recv(sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                std::string result = Excute(buffer);
                send(sockfd, result.c_str(), result.size(), 0);
            }
            else if (n == 0)
            {
                std::cout << "client quit: " << addr.Info() << std::endl;
                break;
            }
            else
            {
                std::cout << "read error " << addr.Info() << std::endl;
                break;
            }
        }
    }

tcpserverMain.cc

因为我们在TcpsServer.hpp和Command.hpp中选择了低耦合,所以我们分别进行了服务器和业务的之间不同的处理,那么如何绑定服务器和业务呢?

使用bind函数就可以:

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        perror("parameter error");
        exit(-1);
    }

    uint16_t port = std::stoi(argv[1]);
    Command command;

    std::unique_ptr<TcpServer> tsver = std::make_unique<TcpServer>(std::bind(&Command::Service, &command, std::placeholders::_1, std::placeholders::_2), port);
    tsver->Init();
    tsver->Loop();

    return 0;
}

本次的功能也没有太多亮点,主要是使用recv和send为之后埋下铺垫,并且了解函数popen的基本使用。


感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值