Windows 基于 libssh2 实现 SSH/SFTP 上传文件到 Linux 完整教程(含报错解决方案)

Windows 基于 libssh2 实现 SSH/SFTP 上传文件到 Linux 完整教程(含报错解决方案)

在 Windows 平台开发跨平台文件上传功能时,libssh2 是常用的 SSH/SFTP 协议库,可实现 Windows 到 Linux 服务器的安全文件传输。本文将详细记录从环境搭建、代码实现到报错排查的完整过程,重点解决开发中遇到的类型转换、路径拼接、权限兼容、Release 模式崩溃等核心问题,提供可直接运行的完整代码。

一、环境准备

1. 开发工具与依赖

  • 开发环境:Visual Studio 2019/2022 + Qt 5.15(或更高版本)
  • 核心库:libssh2(SFTP 协议实现)、OpenSSL(加密依赖)
  • 依赖管理:vcpkg(推荐,自动解决库依赖关系)

2. vcpkg 安装 libssh2

# 安装 x64-windows 版本(根据实际平台选择)
vcpkg install libssh2:x64-windows
vcpkg install openssl:x64-windows

3. Qt .pro 文件配置

QT += core gui widgets network

# 配置 libssh2 和 OpenSSL 路径(vcpkg 安装路径)
LIBSSH2_INC = /include
LIBSSH2_LIB = /lib
OPENSSL_INC = /include
OPENSSL_LIB = /lib

INCLUDEPATH += $$LIBSSH2_INC $$OPENSSL_INC
LIBS += -L$$LIBSSH2_LIB -lssh2
LIBS += -L$$OPENSSL_LIB -lcrypto -lssl

# Windows 网络依赖
win32 {
    LIBS += -lws2_32
}

# Release 模式配置(避免优化导致的崩溃)
CONFIG(release, debug|release) {
    DEFINES += NDEBUG
    QMAKE_CXXFLAGS += -O2 -MT
}

# 启用调试输出(Release 模式可选)
DEFINES += QT_MESSAGELOGCONTEXT

二、核心功能实现(SshUploader 类)

1. 头文件 SshUploader.h

cpp

运行

#ifndef SSHUPLOADER_H
#define SSHUPLOADER_H

#include <QObject>
#include <QString>
#include <QFile>
#include <QFileInfo>
#include <QStringList>
#include <atomic>

// libssh2 头文件
#include <libssh2.h>
#include <libssh2_sftp.h>

// Windows 网络头文件
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#endif

class SshUploader : public QObject
{
    Q_OBJECT
public:
    explicit SshUploader(QObject *parent = nullptr);
    ~SshUploader();

    // 开始上传(对外接口)
    void uploadFile(const QString& host, int port, const QString& username, 
                   const QString& password, const QString& localFilePath, 
                   const QString& remoteDir);

    // 取消上传
    void cancelUpload();

signals:
    // 上传进度(已上传字节数,总字节数)
    void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
    // 上传成功
    void uploadSuccess();
    // 上传失败(错误信息)
    void uploadFailed(const QString& errMsg);

private slots:
    // 核心上传逻辑(运行在子线程)
    void doUpload(const QString& host, int port, const QString& username, 
                 const QString& password, const QString& localFilePath, 
                 const QString& remoteDir);

private:
    // 初始化 Windows 网络
    bool initWinsock();
    // 清理资源(套接字、SSH 会话、SFTP 会话)
    void cleanupResource();
    // 获取最后错误信息
    int getLastError();
    // QString 转 const char*(避免编码问题)
    const char* qstrToCStr(const QString& str, QByteArray& byteArray);
    // 递归创建远程目录
    bool createRemoteDir(LIBSSH2_SFTP* sftp, const QString& remoteDir);
    // 执行 Linux 命令(如 touch 创建文件)
    bool execLinuxCommand(const QString& command);
    // 应急方案:通过 SSH 通道写入文件
    bool writeFileViaSshChannel(const QString& remoteFilePath, const QString& localFilePath);

private:
    // 状态标志
    std::atomic<bool> m_isUploading;  // 是否正在上传
    std::atomic<bool> m_isCanceled;   // 是否取消上传

    // 网络相关
    SOCKET m_socket;                  // 网络套接字
    LIBSSH2_SESSION* m_session;       // SSH 会话句柄
    LIBSSH2_SFTP* m_sftp;             // SFTP 会话句柄

    // 字符串转换缓冲区(避免重复分配)
    QByteArray m_hostBytes;
    QByteArray m_userBytes;
    QByteArray m_passBytes;
    QByteArray m_remoteFileBytes;
};

#endif // SSHUPLOADER_H

2. 源文件 SshUploader.cpp(完整实现)

#include "SshUploader.h"
#include <QDebug>
#include <QCoreApplication>
#include <cstring>
#include <cstdint>
#include <atomic>

// 手动定义 Linux 权限宏(Windows 下未定义)
#ifndef S_IRWXU
#define S_IRWXU 0700  // 所有者:读、写、执行
#endif
#ifndef S_IRGRP
#define S_IRGRP 0400  // 组用户:读
#endif
#ifndef S_IXGRP
#define S_IXGRP 0100  // 组用户:执行
#endif
#ifndef S_IROTH
#define S_IROTH 0040  // 其他用户:读
#endif
#ifndef S_IXOTH
#define S_IXOTH 0010  // 其他用户:执行
#endif

// Release 模式禁用激进优化(避免变量提前释放)
#ifdef _MSC_VER
#pragma optimize("", off)
#endif

SshUploader::SshUploader(QObject *parent)
    : QObject(parent),
      m_isUploading(false),
      m_isCanceled(false),
      m_socket(INVALID_SOCKET),
      m_session(nullptr),
      m_sftp(nullptr)
{
    // 初始化 libssh2
    int ret = libssh2_init(0);
    if (ret != 0) {
        qCritical() << "[libssh2] 初始化失败,错误码:" << ret;
    }
}

SshUploader::~SshUploader()
{
    cancelUpload();
    cleanupResource();
    libssh2_exit(); // 释放 libssh2 资源
}

void SshUploader::uploadFile(const QString& host, int port, const QString& username, 
                           const QString& password, const QString& localFilePath, 
                           const QString& remoteDir)
{
    if (m_isUploading) {
        emit uploadFailed("已有上传任务正在执行");
        return;
    }

    m_isUploading = true;
    m_isCanceled = false;

    // 在子线程执行上传逻辑(避免阻塞 UI)
    QMetaObject::invokeMethod(this, "doUpload", Qt::QueuedConnection,
                              Q_ARG(QString, host),
                              Q_ARG(int, port),
                              Q_ARG(QString, username),
                              Q_ARG(QString, password),
                              Q_ARG(QString, localFilePath),
                              Q_ARG(QString, remoteDir));
}

void SshUploader::cancelUpload()
{
    m_isCanceled = true;
}

void SshUploader::doUpload(const QString& host, int port, const QString& username, 
                          const QString& password, const QString& localFilePath, 
                          const QString& remoteDir)
{
    qDebug() << "[上传] 开始执行上传任务:"
             << "主机:" << host
             << "端口:" << port
             << "本地文件:" << localFilePath
             << "远程目录:" << remoteDir;

    // 1. 初始化 Windows 网络


    // 2. 解析服务器地址(兼容 IP 和域名)


    // 3. 创建网络套接字


    // 4. 连接 Linux 服务器


    // 5. 初始化 SSH 会话



    // 6. SSH 握手


    // 7. 验证服务器主机密钥(简化跳过校验,生产环境需校验)


    // 8. 用户名密码登录


    // 9. 初始化 SFTP 会话


    // 10. 处理远程路径(修复重复文件名问题)


    // 提取纯目录路径(避免 remoteDir 包含文件名)

    // 11. 创建远程目录(递归创建,确保目录存在)


    // 验证远程目录是否存在


    // 12. 构建最终远程文件路径(静态缓冲区避免 Release 模式释放)


    // 13. 用 Linux touch 命令创建文件(绕过 SFTP 路径兼容问题)


    // 14. Release 模式稳定方案:直接用 SSH 通道写入(避免 SFTP 重新初始化崩溃)


    // 15. 上传成功
    emit uploadSuccess();
    qDebug() << "[上传] 文件上传成功!";
    cleanupResource();
    m_isUploading = false;
}

// 初始化 Windows 网络
bool SshUploader::initWinsock()
{
#ifdef _WIN32
    WSADATA wsaData;
    int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (ret != 0) {
        emit uploadFailed(QString("初始化网络失败(错误码: %1)").arg(ret));
        return false;
    }
#endif
    return true;
}

// 清理资源
void SshUploader::cleanupResource()
{
    // 关闭 SFTP 会话
    if (m_sftp) {
        libssh2_sftp_shutdown(m_sftp);
        m_sftp = nullptr;
    }

    // 关闭 SSH 会话
    if (m_session) {
        libssh2_session_disconnect(m_session, "Upload completed");
        libssh2_session_free(m_session);
        m_session = nullptr;
    }

    // 关闭套接字
#ifdef _WIN32
    if (m_socket != INVALID_SOCKET) {
        closesocket(m_socket);
        m_socket = INVALID_SOCKET;
        WSACleanup();
    }
#else
    if (m_socket != -1) {
        close(m_socket);
        m_socket = -1;
    }
#endif

    qDebug() << "[资源] 清理完成";
}

// 获取最后错误信息
int SshUploader::getLastError()
{
#ifdef _WIN32
    return WSAGetLastError();
#else
    return errno;
#endif
}

// QString 转 const char*(UTF-8 编码)
const char* SshUploader::qstrToCStr(const QString& str, QByteArray& byteArray)
{
    byteArray = str.toUtf8();
    return byteArray.constData();
}

// 递归创建远程目录
bool SshUploader::createRemoteDir(LIBSSH2_SFTP* sftp, const QString& remoteDir)
{
    QString dir = remoteDir.trimmed().replace('\\', '/');
    if (dir.isEmpty() || dir == "/") return true;

    QStringList dirLevels = dir.split('/', Qt::SkipEmptyParts);
    QString currentDir = "/";

    foreach (const QString& level, dirLevels) {
        currentDir += level + "/";
        const char* dirCStr = qstrToCStr(currentDir, m_remoteFileBytes);

        // 检查目录是否存在
        LIBSSH2_SFTP_HANDLE* dirHandle = libssh2_sftp_opendir(sftp, dirCStr);
        if (dirHandle) {
            libssh2_sftp_closedir(dirHandle);
            qDebug() << "[目录创建] 目录已存在:" << currentDir;
            continue;
        }

        // 创建目录(libssh2_sftp_mkdir_ex 4 参数,兼容新版本)
        unsigned long dirMode = 0755; // 目录权限 rwxr-xr-x
        int ret = libssh2_sftp_mkdir_ex(sftp, dirCStr, dirMode, 0);
        if (ret != 0) {
            char errBuf[1024] = {0};
            char* errMsg = errBuf;
            libssh2_session_last_error(m_session, &errMsg, nullptr, 1);
            qDebug() << "[目录创建] 失败:" << currentDir << " 错误:" << errBuf;
            return false;
        }
        qDebug() << "[目录创建] 成功:" << currentDir;
    }
    return true;
}

// 执行 Linux 命令
bool SshUploader::execLinuxCommand(const QString& command)
{
    LIBSSH2_CHANNEL* channel = libssh2_channel_open_session(m_session);
    if (!channel) {
        char errBuf[1024] = {0};
        char* errMsg = errBuf;
        libssh2_session_last_error(m_session, &errMsg, nullptr, 1);
        qDebug() << "[命令执行] 打开通道失败:" << errBuf;
        return false;
    }

    const char* cmdCStr = qstrToCStr(command, m_remoteFileBytes);
    qDebug() << "[命令执行] 执行命令:" << cmdCStr;
    int ret = libssh2_channel_exec(channel, cmdCStr);
    if (ret != 0) {
        char errBuf[1024] = {0};
        char* errMsg = errBuf;
        libssh2_session_last_error(m_session, &errMsg, nullptr, 1);
        qDebug() << "[命令执行] 失败:" << errBuf;
        libssh2_channel_close(channel);
        libssh2_channel_free(channel);
        return false;
    }

    // 等待命令执行完成
    libssh2_channel_wait_closed(channel);
    int exitCode = libssh2_channel_get_exit_status(channel);
    if (exitCode != 0) {
        qDebug() << "[命令执行] 退出码非 0:" << exitCode;
        libssh2_channel_close(channel);
        libssh2_channel_free(channel);
        return false;
    }

    qDebug() << "[命令执行] 成功";
    libssh2_channel_close(channel);
    libssh2_channel_free(channel);
    return true;
}

// 应急方案:通过 SSH 通道写入文件
bool SshUploader::writeFileViaSshChannel(const QString& remoteFilePath, const QString& localFilePath)
{
    QFile localFile(localFilePath);
    if (!localFile.open(QIODevice::ReadOnly | QIODevice::Unbuffered)) {
        qDebug() << "[应急写入] 打开本地文件失败:" << localFile.errorString();
        return false;
    }

    LIBSSH2_CHANNEL* channel = libssh2_channel_open_session(m_session);
    if (!channel) {
        qDebug() << "[应急写入] 打开 SSH 通道失败";
        localFile.close();
        return false;
    }

    // 执行 cat 命令:从标准输入读取数据写入文件
    QString catCommand = QString("cat > \"%1\"").arg(remoteFilePath);
    const char* cmdCStr = qstrToCStr(catCommand, m_remoteFileBytes);
    qDebug() << "[应急写入] 执行命令:" << cmdCStr;
    int ret = libssh2_channel_exec(channel, cmdCStr);
    if (ret != 0) {
        char errBuf[1024] = {0};
        char* errMsg = errBuf;
        libssh2_session_last_error(m_session, &errMsg, nullptr, 1);
        qDebug() << "[应急写入] 命令执行失败:" << errBuf;
        libssh2_channel_close(channel);
        libssh2_channel_free(channel);
        localFile.close();
        return false;
    }

    // 分块传输文件(4KB 块,避免内存占用过大)
    const qint64 chunkSize = 4096;
    char buffer[chunkSize];
    qint64 bytesTotal = localFile.size();
    qint64 bytesSent = 0;
    qDebug() << "[应急写入] 开始传输(大小:" << bytesTotal / 1024 << "KB)";

    while (!m_isCanceled) {
        qint64 bytesRead = localFile.read(buffer, chunkSize);
        if (bytesRead <= 0) {
            if (bytesRead < 0) {
                qDebug() << "[应急写入] 读取本地文件失败:" << localFile.errorString();
                ret = -1;
            }
            break;
        }

        // 写入 SSH 通道
        ret = libssh2_channel_write(channel, buffer, bytesRead);
        if (ret <= 0) {
            qDebug() << "[应急写入] 写入通道失败(错误码:" << ret << ")";
            break;
        }

        bytesSent += ret;
        emit uploadProgress(bytesSent, bytesTotal);
        qDebug() << "[应急写入] 进度:" << bytesSent / 1024 << "KB /" << bytesTotal / 1024 << "KB";

        // 给事件循环留时间(避免 UI 卡死)
        QCoreApplication::processEvents();
    }

    // 发送 EOF,告知服务器传输完成
    libssh2_channel_send_eof(channel);
    libssh2_channel_wait_closed(channel);
    int exitCode = libssh2_channel_get_exit_status(channel);

    // 清理资源
    localFile.close();
    libssh2_channel_close(channel);
    libssh2_channel_free(channel);

    // 校验传输结果
    if (m_isCanceled) {
        qDebug() << "[应急写入] 上传被取消";
        return false;
    }
    if (ret < 0 || exitCode != 0) {
        qDebug() << "[应急写入] 传输失败(退出码:" << exitCode << ")";
        return false;
    }
    if (bytesSent != bytesTotal) {
        qDebug() << "[应急写入] 传输不完整(已传:" << bytesSent << ",总大小:" << bytesTotal << ")";
        return false;
    }

    qDebug() << "[应急写入] 传输成功!";
    return true;
}

三、开发过程中遇到的核心报错及解决方案

报错 1:error C2440: “初始化”: 无法从 “int” 转换为 “const char *”

const char* sshErr = libssh2_session_last_error(m_session, nullptr, nullptr, 0);

原因

libssh2_session_last_error 返回 int 错误码,而非直接返回字符串,隐式转换被 C++ 禁止。

解决方案

通过缓冲区接收错误字符串,显式处理返回值:

char errBuf[1024] = {0};
char* errMsg = errBuf;
int errCode = libssh2_session_last_error(m_session, &errMsg, nullptr, 1);
const char* sshErr = (errBuf[0] != '\0') ? errBuf : "未知错误";

报错 2:error C2664: “int libssh2_session_last_error (...)”: 无法将参数 2 从 “char [1024]” 转换为 “char **”

报错场景

int errCode = libssh2_session_last_error(m_session, errBuf, nullptr, 1);

原因

函数第 2 个参数要求 char**(指针的指针),直接传入数组 char[1024] 类型不匹配。

解决方案

用中间指针过渡,确保类型匹配:

char errBuf[1024] = {0};
char* errMsg = errBuf; // 中间指针:char* → &errMsg 为 char**
int errCode = libssh2_session_last_error(m_session, &errMsg, nullptr, 1);

报错 3:创建远程文件失败(错误码: 2,原因: No such file or directory)

报错场景

目录已通过 createRemoteDir 创建,但 libssh2_sftp_open 仍提示路径不存在。

原因

  1. 远程路径拼接错误(重复文件名);
  2. libssh2 SFTP 会话路径缓存未同步;
  3. Linux 权限不足。

解决方案

  1. 修复路径拼接逻辑,提取纯目录路径;
  2. 用 Linux touch 命令创建文件,绕过 SFTP 兼容问题;
  3. 递归创建目录时设置正确权限(0755)。

报错 4:Release 模式下 SFTP 重新初始化后崩溃

报错场景

Debug 模式正常,Release 模式在 libssh2_sftp_init 后崩溃。

原因

  1. Release 模式优化导致变量提前释放(如 remoteFileCStr 依赖的缓冲区被释放);
  2. m_sftp 野指针未彻底置空;
  3. 链接了 Debug 版本的 libssh2 库。

解决方案

  1. 用静态缓冲区存储路径,避免内存提前释放;
  2. 禁用当前文件的激进优化(#pragma optimize("", off));
  3. 直接使用 SSH 通道写入文件,绕过 SFTP 重新初始化;
  4. 确保 Release 模式链接 Release 版本的库。

四、使用示例

五、注意事项

  1. 权限问题:Linux 服务器目录需给用户写入权限(可通过 chmod -R 755  授权);
  2. 编码兼容:所有字符串转换使用 UTF-8 编码,避免中文路径乱码;
  3. 线程安全:上传逻辑运行在子线程,避免阻塞 UI 线程;
  4. 生产环境优化:主机密钥校验、断点续传、超时重连等功能需根据实际需求添加;
  5. 库版本:推荐使用 libssh2 1.10+ 版本,修复了多个跨平台兼容 Bug。

本文提供的代码已解决 Windows 到 Linux SSH/SFTP 上传的核心问题,经过 Debug/Release 模式测试,可直接集成到项目中使用。如需扩展功能(如批量上传、权限校验),可基于现有框架进行二次开发。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值