基于libssh2的ssh远程执行/sftp传输C++库pssh

6 篇文章 1 订阅
2 篇文章 0 订阅

平时维护一大堆linux服务器,如何无需交互带密码远程执行命令?其实之前也调研过几种方案,比如直接调用plink获取其结果--后来发现这玩意非线程安全,无法多线程使用且bug很多。又比如QT自带的QSSH代码,我搞下来试了一下,一是也是非线程安全,二是这货是非阻塞式调用,门槛比较高,难受的很,所以放弃了,最终选定了libssh2。

不用脚本纯代码的话,libssh2无疑是一个好的选择。但这个库对新手不是很友好,网络上的例子也都是把官方给的几个例子抄来抄去毫无新意,不但繁琐而且有很多缺点,总结几个如下:

  1. 纯C库,用起来不是那么方便
  2. 非线程安全,无法线程执行
  3. 连接/执行没有断线重连功能
  4. sftp传输没有上传/下载文件夹的功能
  5. sftp传输没有短点续传的功能
  6. windows与linux之间sftp传输不支持路径以及文件名含有中文以及空格问题
  7. 远程执行返回You have mail/You have new mail的问题
  8. ssh命令过长偶尔导致\b \b \b..乱码的问题

等一系列问题,实在无法满足我想简单粗暴阻塞式执行命令以及传输文件、目录的需求,于是自己花了几个月封装了一个跨平台pssh库,致力于解决这些问题,主要功能以及更新记录如下:

  1.  增加了带空格的目录以及文件名支持
  2.  增加了中文支持
  3.  对断点续传上传和下载文件增加速度提示功能
  4.  增加了下载文件/上传文件/下载目录/上传目录的断点续传功能
  5.  已改造windows上传文件,下载文件,上传目录,下载目录,支持回调函数,目录支持进度
  6.  传输相关要从windows下psshsingle考,从linux考来的不对,因为有中文问题
  7.  已改造为单线程断线重连,不要卡住,目前实现思路是,send失败后,只重连一次,失败再返回,因为xsh一般都不是只执行一次,多次执行,上一次失败的话,重连一次,这次失败,下一次就会成功
  8. 本版本是多个channel版本,以后多线程用这个作为模板,原来的套一个pssh太臃肿,每次要写两遍函数
  9.  修改了recv处理,兼容centos系和ubuntu系列,ubuntu系列没有[,可连接127.0.0.1
  10.  单线程重连版,主要是为了防止线程过多,线程重连如果pssh多的话2倍线程,而且稳定一些
  11.  xsh是send失败直接阻塞重连,recv因为有参数返回,所以还是返回值,在xsh里面阻塞重连
  12.  xshlastinng是成功尝试多个channel,在循环lastting的时候用statuschannel判断阻塞重连
  13.  已经改造了close,xsh和xshlasting都可以closetrue
  14.  已经改造了xshlasting,可以做到closetrue后再open依然可以
  15.  已经改造了xsh,可以做到closetrue后再open依然可以
  16.  解决了深度openssl线程安全问题
  17.  增加了获取是否生成core文件功能,增加在/etc/profile增加配置等功能
  18.  增加切换目录,获取当前目录,查看远程进程,获取根目录使用比例是否在等常用功能
  19.  增加获取远程core文件"Core文件"<<"服务器地址"<<"所属进程"<<"生成时间"的list信息
  20.  增加了远程ini模式配置文件的增删改查功能
  21.  增加了给/etc/profile添加配置功能,内置判断是否已有配置
  22.  增加了close(true)功能,true情况下关闭自动重连,彻底关闭(两种模式);
  23.   修复了在高延迟情况下判断远程文件是否存在时间过长的bug(不再用sftp模式获取,使用ssh模式直接ll获取)
  24.  增加xshlasting模式自动重连功能,使用另一个xsh一直发送pwd探测,如果断开,则线程自动重连,重连成功后继续返回正确结果
  25.  增加了自动重连的功能,如果连接断开,则当时xsh返回错误,线程自动重连,如果下次已经连接上则下次获取正确结果
  26.  增加了与服务器连接断开时返回错误的功能,不会一直卡住
  27.  阻塞调用,xsh等结果获取完成才返回
  28.  增加了上传文件/下载文件/上传目录/下载目录功能
  29.  修复了xsh有时会返回You have mail/You have new mail的问题,过滤
  30.  修复了xsh有时命令过长会出现结果返回\b\b\b..乱码的问题,过滤
  31.  修改了xsh功能,任务错误都字符串返回#fail,包括连接断开
  32.  修改了connect,增加了连接超时功能,默认超时时间1秒,原来是20秒太长
  33.  增加了同一个ssh close再open功能
  34.  测试了多线程功能C++11线程,线程安全
  35.  增加了探测连接的功能.另起一个chanel一直探测,用于判断连接状态,该功能是xshlasting的基础,因为xshlasting不能判断断了,用这个bconnected判断
  36.  增加了支持域名功能(为了支持端口映射)
  37.  增加两种模式,一种正常获取xsh结果,一种持续获取xshlasting,xshlasting可用于tail -f收集日志到统一服务器
  38. 增加了xsh获取结果超时10秒返回错误功能
  39. 对于xsh获取结果失败(原因不限于连接断开,超时等),均返回#fail开头的字符串

主要代码如下:

/*
 * 作者:李海龙2021-02-26
*/
class pssh {
public:
    pstring strhost;
    int iport;
    pstring strpwd;
    pstring struser;
    pstring strerr;//最后一次错误描述

    pssh();
    pssh(pstring host,int port=22,pstring pwd="scfwq_325",pstring user="root");
    void init(pstring host,int port=22,pstring pwd="scfwq_325",pstring user="root");
    virtual ~pssh();
    int open();//open一次就行,返回结果,不用自动重连,因为后边执行xsh会自动重连
    int close(bool breal=false);//close(true)停止重连
    /*
     * xsh是send和recv的合集,获取top结果用top -b n 1
     * 获取过程中如果断了,无论是send还是recv会自动重连一次,然后返回失败,如果重连成功,则下次会正确,这样能保证返回失败提示
     * 如果是主动closetrue则会返回#fail
    */
    pstring xsh(pstring cmd,pstring type="data");
    //send失败一次,自带一次重连,反正失败了也不在乎那几秒了,如果重连成功,重新发送一次命令,如果失败,返回,这样既重连了,也能返回看结果
    int send(pstring cmd,pstring type="data");
    //接收有多种状态,返回值,在xsh中统一处理自动重连,带失败自动重连一次,与send一样,有10秒超时
    int recv(pstring &data,pstring type="data");
    /* 使用方式
     * ptrans->uploadThread(pathFull,this->strpwdremote,
     * std::bind(&MainWindow::showstr,this,placeholders::_1));
     * //停止时用close(true)
     */
    //持续结果输出,例如tail -f,过程输出,也可以执行别的命令
    //停止时用close(true)
    int xshlasting(pstring cmd, std::function<void(pstring)> fun=funShow);
    /* 用两个channel实现的,使用方式
     * ptrans->uploadThread(pathFull,this->strpwdremote,
     * std::bind(&MainWindow::showstr,this,placeholders::_1));
     */
    int xshlastingThread(pstring cmd, std::function<void(pstring)> fun=funShow);
    //判断远程文件是否有key,比如判断core是否启用bHasKeyInRemoteFile("/etc/profile","ulimit");
    bool bHasKeyInRemoteFileBridge(pstring host,pstring path = "/etc/profile", pstring key = "ulimit");
    bool ping(pstring host);
    presult getTimeLastReboot();
    //"Core文件"<<"服务器地址"<<"所属进程"<<"生成时间"的list
    pvector<pliststring> getCoreInfo(pstring strpath="/");
    //判断远程文件是否有key,比如判断core是否启 bHasKeyInRemoteFile("/etc/profile","ulimit");
    bool bHasKeyInRemoteFile(pstring path = "/etc/profile", pstring key = "ulimit");
    //获取根目录占用比例
    presult getUsageRoot();
    bool cd(pstring path);
    presult getContentFromRemoteFile(pstring path);
    pstring getIPRemote();
    pstring pwd();
    //从源码目录传过去,改权限,获取结果,已包括断开连接的判断,四个
    pstring getconf(pstring path, pstring section, pstring key, bool bupload = false);
    bool delconf(pstring path, pstring section, pstring key, bool bupload = false);
    bool addconf(pstring path, pstring section, pstring key, pstring val, bool bupload = false);
    bool setconf(pstring path, pstring section, pstring key, pstring val, bool bupload = false);
    ppair<int,ptime> getTimeNow();
    //重命名
    int mv(pstring strold,pstring strnew);
    //获取文件大小
    longlong getFileSize(pstring strPathFull);
    //获取类型 dir file lnk noaccess,失败返回空用sftp用以提高速度
    pstring getPathTypeRemote(pstring strPath);
    //删除文件或目录,支持带空格的
    int rm(pstring strPathFullRemote);
    //sftp创建文件 支持带空格
    int touch(pstring strPathFullRemote);
    //远程递归创建目录sftp速度快//错误返回小于0,已存在也返回错误 -31表示已存在,但是
    //根据一般业务情况,改进为,如果有同名目录,返回创建成功,如要判断之前是否已存在,单独用isExsistdir
    int mkdirp(pstring strPathFullRemote);
    //远程创建目录sftp速度快//错误返回小于0,已存在也返回错误 -31表示已存在,但是
    //根据一般业务情况,改进为,如果有同名目录,返回创建成功,如要判断之前是否已存在,单独用isExsistdir
    int mkdir(pstring strPathFullRemote);
    //判断有没有上述目录
    bool isExsistDirRemote(pstring strPathDirRemote);
    //判断有没有上述文件
    bool isExsistFileRemote(pstring strPathFullFile);
    //获取名字和类型
    pmap<pstring, pstring> getAllInDirRemote(pstring strPathDirRemote);
    //获取名字,类型和大小,最后修改时间,sftp方式
    plist<pliststring> getAllWithSizeInDirRemote(pstring strPathDirRemote);
    //这获取的是全路径
    void getAllFilesAndPathsRecursionRemote(pstring path, pliststring &lfile, pliststring &ldir);
    //lnk也要下载,下载后就是原来指向的文件,scp也是这样的,所以会与原来大小不一,但是要与scp一样就行,目前与scp不一样大小查原因
    int downloadDir(pstring strPathFullLocal,pstring strPathFullRemote,
                    std::function<void(pliststring)> fun=showProcessDir);
    //上传libssh2目录慢很多,两分钟,但下载很快,查原因
    int uploadDir(pstring strPathFullLocal,pstring strPathFullRemote,
                  std::function<void(pliststring)> fun=showProcessDir);

    //两个都是全路径--经测试windows版的上传超过10秒后有时就不支持内置断点续传,老老实实写seek吧!,断点续传的逻辑已经写好了,llpos=0是从头传
    int uploadFileOnce(pstring strPathFullLocal,pstring strPathFullRemote,
                   std::function<void(pliststring)> fun=showProcessFile,
                   longlong llpos=0);
    //uploadfile断点续传逻辑已经写好了,但是在centos6.8下sftpappend会失效,可能就像提示一样:APPEND doesn't have any effect on OpenSSH servers
    //测试在centos7下没问题,但是centos6就不行,所以考虑uploadfile时,断了的话,直接从头开始,因为本来断点续传也是偶发事件,改为断线重传,下载还是断点续传
    //自动断点续传直到正确传完为止--在centos6.8下有时候断点续传有问题---查原因
    int uploadFile(pstring strPathFullLocal,pstring strPathFullRemote,
                   std::function<void(pliststring)> fun=showProcessFile);
    //两个都是全路径--经测试只有windows版的download过了10秒后不支持断点续传,10秒以内可以,所以要改造,断点续传的逻辑已经写好了,llpos=0是从头传
    int downloadFileOnce(pstring strPathFullLocal, pstring strPathFullRemote,
                     std::function<void(pliststring)> fun=showProcessFile,
                     longlong llpos=0);
    //自动断点续传直到正确传完为止
    int downloadFile(pstring strPathFullLocal, pstring strPathFullRemote,
                         std::function<void(pliststring)> fun=showProcessFile);
    int showDir(pstring strPathDirRemote);
    //获取类型 dir file lnk noaccess,失败返回空用xsh速度太慢,200ms
    pstring getTypePathRemoteUseXsh(pstring strPath);
    //远程递归创建目录,速度慢,要改成sftp创建目录
    int mkdirpUseXsh(pstring strPathFullRemote);
    bool getStatusConnect();
    friend ostream &operator<<(ostream &os, pssh x) {
        os << x.strhost << ":" << x.iport << "@" << x.struser
           << " connect status: " << x.bConnected;
        return os;
    }
    //下边这两个是为了持续获取搞的
    void recvLasting(std::function<void(pstring)> fun);
    LIBSSH2_CHANNEL *pchannelData;
    LIBSSH2_CHANNEL *pchannelStatus;
    //新加的
    bool bConnected;
private:
    LIBSSH2_SFTP *sftp_session;
    pstring strCmdNow;
    bool bstop;
    //原来的
//    int m_socket;
    ptcp tcp;
    LIBSSH2_SESSION *psession;
    bool connect(const char *szIp, int nPort = 22);
    bool login(const char *szUserName, const char *szPassword);
    // 返回值表示渠道的序号。如果返回-1,创建失败;
    LIBSSH2_CHANNEL *createChannel(const char *szChannelTerm = "vanilla");
    bool write(LIBSSH2_CHANNEL *channel, const char *szData);
    int waitsocket(int socket_fd, LIBSSH2_SESSION *session);
};

其实我也知道大家对我拙劣的封装并不感兴趣,只关心好不好用,怎么用的问题,下边给大家展示一下:

普通远程执行:

    //初始化 地址 端口 密码 用户名
    pssh ssh("192.168.133.129",22,"123.asdf","root");
    //连接linux服务器
    hlog(ssh.open());
    //同时做个多线程一直执行pwd命令
    std::thread([]{
        pssh ssh2("192.168.133.129",22,"123.asdf","root");
        hlog(ssh2.open());
        while(1)
        {
            hlog(ssh2.xsh("pwd"));
            plib::sleep(1000);
        }
    }).detach();
    //获取结果
    pstring stres=ssh.xsh("ifconfig|grep inet");\
    //日志打印
    hlog(stres);
    stres=ssh.xsh("pwd");
    hlog(stres);
    while(1)
    {
        plib::sleep(1000);
    }

 结果如下:

 持续远程执行获取(获取tail -f 这种)

 //同时做个多线程一直往远程服务器/root/test.log下一秒写一次数据,即模拟日渐增长的日志文件
    std::thread([]{
        pssh ssh2("192.168.133.129",22,"123.asdf","root");
        hlog(ssh2.open());
        //先把原来的删掉
        hlog(ssh2.xsh("rm -f /root/test.log"));
        int count=0;
        while(1)
        {
            pstring strcmd="echo '"+plib::toString(count+1)+": qq1415532825'>>/root/test.log";
            (ssh2.xsh(strcmd));
            plib::sleep(1000);
            count++;
        }
    }).detach();
    plib::sleep(1000);
    //初始化 地址 端口 密码 用户名
    pssh ssh("192.168.133.129",22,"123.asdf","root");
    //连接linux服务器
    hlog(ssh.open());
    //新线程内持续获取日志,将结果通过回调函数的形式回放到需要的函数内
    ssh.xshlastingThread("tail -f /root/test.log",funShowInfo);
    //5秒后关闭持续获取
    plib::sleep(5000);
    //close true代表停止自动重连,彻底停止,close false代表服务器异常断开后会断线重连继续获取
    hlog(ssh.close(true));
    while(1)
    {
        plib::sleep(1000);
    }

 

 关于此种模式,不用说大家也知道可以用来干什么,日志同步啊,日志收集,日志监控啊!甚至都不用部署客户端,简直不要太爽!

 sftp文件上传、下载

        pssh ssh("192.168.133.129",22,"123.asdf","root");
        hlog(ssh.open());
        //测试上传下载,顺便把中文也测试了

        hlog(ssh.uploadFile("d:/setups/协和2.17.0.apk","/home/xlfd/"));
        hlog(ssh.downloadFile("d:/test/协和下载.apk","/home/xlfd/协和2.17.0.apk"));

结果如下:

 sftp目录递归上传、下载

测试目录:

 

        pssh ssh("192.168.133.129",22,"123.asdf","root");
        hlog(ssh.open());
        //测试上传下载,顺便把中文也测试了

//        hlog(ssh.uploadFile("d:/setups/协和2.17.0.apk","/home/xlfd/"));
//        hlog(ssh.downloadFile("d:/test/协和下载.apk","/home/xlfd/协和2.17.0.apk"));
        hlog(ssh.uploadDir("d:/setups/Debuggers","/root"));
        hlog(ssh.downloadDir("D:/","/root/Debuggers/"));

结果如下:

 

当然以上功能都有断点续传功能,传输功能都带回调函数,方便与QT界面外部调用等集成,以后我会基于该库专门写一个sftp传输工具。

  • 10
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
好的,以下是基于libssh远程执行命令的C语言代码,适用于Linux系统: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <libssh/libssh.h> int ssh_execute(ssh_session session, const char *command) { ssh_channel channel = ssh_channel_new(session); if (channel == NULL) { fprintf(stderr, "Error: Failed to create SSH channel.\n"); return SSH_ERROR; } if (ssh_channel_open_session(channel) != SSH_OK) { fprintf(stderr, "Error: Failed to open SSH channel session: %s.\n", ssh_get_error(session)); ssh_channel_free(channel); return SSH_ERROR; } if (ssh_channel_request_exec(channel, command) != SSH_OK) { fprintf(stderr, "Error: Failed to execute command: %s.\n", ssh_get_error(session)); ssh_channel_close(channel); ssh_channel_free(channel); return SSH_ERROR; } char buffer[1024]; ssize_t bytes_received; while ((bytes_received = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) { fwrite(buffer, 1, bytes_received, stdout); } if (bytes_received < 0) { fprintf(stderr, "Error: Failed to receive command output: %s.\n", ssh_get_error(session)); ssh_channel_close(channel); ssh_channel_free(channel); return SSH_ERROR; } ssh_channel_send_eof(channel); ssh_channel_close(channel); ssh_channel_free(channel); return SSH_OK; } int main(int argc, char **argv) { if (argc != 5) { fprintf(stderr, "Usage: %s <host> <port> <user> <password>\n", argv[0]); return EXIT_FAILURE; } const char *host = argv[1]; const int port = atoi(argv[2]); const char *user = argv[3]; const char *password = argv[4]; ssh_session session = ssh_new(); if (session == NULL) { fprintf(stderr, "Error: Failed to create SSH session.\n"); return EXIT_FAILURE; } ssh_options_set(session, SSH_OPTIONS_HOST, host); ssh_options_set(session, SSH_OPTIONS_PORT, &port); ssh_options_set(session, SSH_OPTIONS_USER, user); if (ssh_connect(session) != SSH_OK) { fprintf(stderr, "Error: Failed to connect: %s.\n", ssh_get_error(session)); ssh_free(session); return EXIT_FAILURE; } if (ssh_userauth_password(session, NULL, password) != SSH_AUTH_SUCCESS) { fprintf(stderr, "Error: Failed to authenticate: %s.\n", ssh_get_error(session)); ssh_disconnect(session); ssh_free(session); return EXIT_FAILURE; } const char *command = "ls -l"; if (ssh_execute(session, command) != SSH_OK) { ssh_disconnect(session); ssh_free(session); return EXIT_FAILURE; } ssh_disconnect(session); ssh_free(session); return EXIT_SUCCESS; } ``` 请注意将 `<host>`, `<port>`, `<user>` 和 `<password>` 替换为实际的值,以及将 `"ls -l"` 替换为实际的命令。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙猫哪里跑

谢大爷赏赐

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值