Windows下基于libssh2实现SSH/SFTP文件传输:从编译到实战
2026/7/22 0:00:44 网站建设 项目流程

1. 项目概述:为什么要在Windows上用libssh2搞SSH/SFTP?

如果你是一个在Windows环境下工作的开发者或运维,十有八九遇到过需要把文件传到Linux服务器的场景。用现成的工具,比如WinSCP、FileZilla,或者VSCode的Remote-SSH插件,当然方便。但有时候,需求会变得“刁钻”起来:你需要把文件传输功能集成到自己的自动化脚本里,或者你的应用需要在后台静默、可靠地上传日志、备份文件到远程服务器,这时候依赖带界面的图形化工具就不太现实了。

这就是libssh2这类库的用武之地。它是一个用C语言实现的SSH2协议客户端库,轻量、高效,而且跨平台。在Windows上基于它来开发,意味着你可以用C、C++、C#甚至Python(通过绑定库)来编写完全受控的文件传输逻辑,摆脱对第三方工具的依赖。这个项目,就是带你从零开始,在Windows上搭建libssh2的开发环境,写一个能稳定工作的SSH/SFTP文件上传程序,并且把过程中那些“坑”和解决方案都摊开来给你看。我踩过的雷,你就别再踩了。

2. 环境准备与核心库编译

在Windows上玩转C/C++的库,第一步往往就是最磨人的:编译。libssh2本身依赖OpenSSL或者WolfSSL来进行加密通信,在Linux上可能一条apt-get install就搞定,在Windows上就得亲自动手。

2.1 工具链选择与获取

我的建议是,别折腾MinGW或者Visual Studio的旧版本了,直接上MSYS2。它提供了一个近乎完整的Linux-like环境(Bash, Pacman包管理器),让你能用类似Linux的方式去编译开源库,痛苦指数直线下降。

  1. 安装MSYS2:去官网下载安装包,默认安装。安装完成后,你会看到三个启动图标:MSYS2 UCRT64MSYS2 MINGW64MSYS2 MSYS我们主要使用MSYS2 MINGW64,因为它能生成原生的Windows可执行文件(.exe)。

  2. 更新系统并安装基础工具:打开MSYS2 MINGW64,依次运行以下命令:

    pacman -Syu # 更新系统核心包,可能会要求关闭窗口再重新打开 pacman -Su # 继续更新其他包 pacman -S --needed base-devel mingw-w64-x86_64-toolchain git cmake

    这安装了GCC编译器、Make、Git、CMake等全套开发工具。

2.2 编译依赖库:OpenSSL

libssh2需要加密库。我们选择更通用的OpenSSL。

  1. 下载源码:在MSYS2 MINGW64中,找一个目录,比如/home/yourname/,克隆OpenSSL源码。

    cd /home/yourname/ git clone https://github.com/openssl/openssl.git cd openssl # 切换到某个长期支持版本,比如3.1.x,更稳定 git checkout openssl-3.1.8
  2. 配置与编译:OpenSSL的配置脚本对Windows很友好。

    # 配置为生成MINGW64版本的静态库(.a)和动态库(.dll.a) ./Configure mingw64 shared --prefix=/usr/local # 开始编译,-j4表示用4个并行任务加速 make -j4 # 安装到MSYS2的系统目录 make install

    编译完成后,关键的库文件(libssl.a,libcrypto.a, 以及对应的.dll.a)和头文件会被安装到/usr/local/lib/usr/local/include下。MSYS2的GCC默认会搜索这些路径。

注意:如果你计划最终的程序要脱离MSYS2环境运行,需要将编译生成的libssl-3-x64.dlllibcrypto-3-x64.dll(具体名字和版本有关)复制到你的可执行文件同级目录,或者放到系统的PATH路径下。静态链接可以避免这个问题,但配置稍复杂。

2.3 编译目标库:libssh2

有了OpenSSL,编译libssh2就顺利多了。

  1. 下载libssh2源码

    cd /home/yourname/ git clone https://github.com/libssh2/libssh2.git cd libssh2
  2. 使用CMake配置并编译:CMake能更好地处理依赖和跨平台选项。在libssh2源码目录下创建一个构建目录。

    mkdir build && cd build # 关键配置:指定使用OpenSSL,并设置安装前缀 cmake .. -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF \ # 我们编译静态库,方便部署 -DCRYPTO_BACKEND=OpenSSL \ -DCMAKE_INSTALL_PREFIX=/usr/local # 编译 cmake --build . --config Release -j4 # 安装 cmake --install .

    这一步完成后,libssh2的静态库libssh2.a和头文件也会被安装到/usr/local下。

实操心得:在Windows上编译开源库,路径和编译器一致性是关键。全程在MSYS2 MINGW64环境下操作,能确保工具链统一。如果编译失败,首先检查错误信息是否关于找不到openssl/ssl.h,这通常意味着OpenSSL没有正确安装到/usr/local,或者CMake没有找到它。可以尝试在CMake命令中显式指定路径:-DOPENSSL_ROOT_DIR=/usr/local

3. 核心代码实现:从连接到SFTP上传

环境搞定,接下来就是写代码了。我们将实现一个简单的命令行程序,接收本地文件路径、远程服务器信息,然后通过SFTP上传。

3.1 建立SSH连接与会话

SSH通信的核心是LIBSSH2_SESSION。初始化库、创建会话、建立TCP连接、完成认证,这是一套标准流程。

#include <libssh2.h> #include <libssh2_sftp.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> // 注意,MSYS2提供了这些POSIX头文件的兼容 #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { const char *hostname = "192.168.1.100"; // 远程服务器IP int port = 22; const char *username = "your_username"; const char *password = "your_password"; // 或使用密钥 const char *local_file = "test.txt"; const char *remote_path = "/tmp/uploaded_test.txt"; int sock; struct sockaddr_in sin; LIBSSH2_SESSION *session = NULL; int rc; // 1. 初始化libssh2库 rc = libssh2_init(0); if (rc != 0) { fprintf(stderr, "libssh2初始化失败: %d\n", rc); return -1; } // 2. 创建TCP套接字并连接 sock = socket(AF_INET, SOCK_STREAM, 0); sin.sin_family = AF_INET; sin.sin_port = htons(port); sin.sin_addr.s_addr = inet_addr(hostname); if (connect(sock, (struct sockaddr*)(&sin), sizeof(struct sockaddr_in)) != 0) { fprintf(stderr, "连接服务器失败\n"); goto shutdown; } // 3. 创建SSH会话并关联到套接字 session = libssh2_session_init(); if (!session) { fprintf(stderr, "创建SSH会话失败\n"); goto shutdown; } libssh2_session_set_blocking(session, 1); // 设置为阻塞模式,简化逻辑 rc = libssh2_session_handshake(session, sock); if (rc != 0) { fprintf(stderr, "SSH握手失败: %s\n", libssh2_session_last_error(session, NULL, NULL, 0)); goto shutdown; } printf("SSH连接已建立。\n"); // 4. 用户认证(密码方式示例) rc = libssh2_userauth_password(session, username, password); if (rc != 0) { fprintf(stderr, "密码认证失败: %s\n", libssh2_session_last_error(session, NULL, NULL, 0)); goto shutdown; } printf("用户认证成功。\n");

这段代码完成了网络连接和SSH会话建立。libssh2_session_set_blocking(session, 1)将会话设为阻塞模式,这意味着每个libssh2函数调用都会等待操作完成才返回,对于简单的顺序执行脚本来说更易于理解和调试。在生产环境中,非阻塞模式配合事件循环效率更高。

3.2 初始化SFTP子系统并上传文件

认证成功后,我们需要在SSH会话上启动SFTP子系统,然后进行文件操作。

// 5. 初始化SFTP会话 LIBSSH2_SFTP *sftp_session = libssh2_sftp_init(session); if (!sftp_session) { fprintf(stderr, "初始化SFTP失败: %s\n", libssh2_session_last_error(session, NULL, NULL, 0)); goto shutdown; } printf("SFTP会话已初始化。\n"); // 6. 打开远程文件用于写入(创建或截断) LIBSSH2_SFTP_HANDLE *sftp_handle = libssh2_sftp_open(sftp_session, remote_path, LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC, LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IROTH); // 权限:644 if (!sftp_handle) { fprintf(stderr, "打开远程文件失败: %s\n", libssh2_session_last_error(session, NULL, NULL, 0)); goto shutdown_sftp; } // 7. 打开本地文件并读取内容 FILE *local_fp = fopen(local_file, "rb"); if (!local_fp) { perror("打开本地文件失败"); goto shutdown_sftp_handle; } fseek(local_fp, 0, SEEK_END); long file_size = ftell(local_fp); fseek(local_fp, 0, SEEK_SET); char *buffer = (char*)malloc(file_size); if (!buffer) { fprintf(stderr, "内存分配失败\n"); goto cleanup_local_file; } fread(buffer, 1, file_size, local_fp); // 8. 将本地文件内容写入远程文件 size_t total_written = 0; while (total_written < file_size) { ssize_t nwritten = libssh2_sftp_write(sftp_handle, buffer + total_written, file_size - total_written); if (nwritten < 0) { fprintf(stderr, "SFTP写入错误: %s\n", libssh2_session_last_error(session, NULL, NULL, 0)); break; } total_written += nwritten; printf("已上传: %zu / %ld 字节\n", total_written, file_size); } if (total_written == file_size) { printf("文件上传成功!\n"); } // 9. 清理资源 free(buffer); cleanup_local_file: fclose(local_fp); shutdown_sftp_handle: libssh2_sftp_close(sftp_handle); shutdown_sftp: libssh2_sftp_shutdown(sftp_session); shutdown: if (session) { libssh2_session_disconnect(session, "Normal shutdown"); libssh2_session_free(session); } if (sock != -1) { close(sock); } libssh2_exit(); return 0; }

关键点解析

  • libssh2_sftp_open:这个函数用于打开远程文件。第二个参数是标志位,LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC组合意味着:以写入方式打开,如果文件不存在则创建,如果存在则清空。第三个参数是Unix风格的文件权限位,这里0644表示所有者可读写,组和其他用户只读。
  • libssh2_sftp_write:在循环中调用,直到所有数据写完。在阻塞模式下,它会一次性写入尽可能多的数据,但返回值nwritten可能小于请求的字节数,所以需要循环。
  • 资源释放顺序:非常重要!必须按照关闭文件句柄->关闭SFTP会话->断开并释放SSH会话->关闭套接字->清理库的顺序进行。顺序错了可能导致内存泄漏或连接未正确关闭。

3.3 编译与链接你的程序

将上面的代码保存为ssh2_upload.c。在MSYS2 MINGW64中,使用以下命令编译:

gcc -o ssh2_upload.exe ssh2_upload.c -I/usr/local/include -L/usr/local/lib -lssh2 -lssl -lcrypto -lws2_32 -static

参数解释

  • -I/usr/local/include:指定libssh2和OpenSSL头文件路径。
  • -L/usr/local/lib:指定库文件路径。
  • -lssh2 -lssl -lcrypto:链接libssh2、libssl和libcrypto库。
  • -lws2_32:链接Windows的Winsock2库,因为我们的代码用了socketconnect等网络函数。
  • -static:进行静态链接。这会把libssh2和OpenSSL的代码都打包进你的.exe文件,生成的可执行文件会变大(可能几MB到十几MB),但好处是部署时不需要携带额外的DLL,真正做到“开箱即用”。如果追求小巧,可以去掉-static,但运行时需要将libssh2.dlllibssl-3-x64.dlllibcrypto-3-x64.dll放在同目录或系统路径。

编译成功后,你会得到一个ssh2_upload.exe。在运行前,请确保你的Windows防火墙允许该程序出站访问,并且远程Linux服务器的SSH服务(端口22)是可达的。

4. 进阶:密钥认证与目录操作

密码认证不够安全,也不利于自动化。更专业的做法是使用密钥对。

4.1 使用SSH密钥进行认证

假设你已经在Linux服务器上配置了公钥(~/.ssh/authorized_keys),本地有私钥文件(如id_rsa)。

修改认证部分的代码,替换掉libssh2_userauth_password

const char *private_key_path = "/home/yourname/.ssh/id_rsa"; const char *public_key_path = NULL; // 通常私钥文件也包含公钥信息,可设为NULL const char *key_passphrase = NULL; // 如果你的私钥有密码,在此填写 rc = libssh2_userauth_publickey_fromfile(session, username, public_key_path, private_key_path, key_passphrase); if (rc != 0) { fprintf(stderr, "公钥认证失败: %s\n", libssh2_session_last_error(session, NULL, NULL, 0)); // 可以尝试回退到密码认证 // rc = libssh2_userauth_password(session, username, password); goto shutdown; } printf("公钥认证成功。\n");

重要提示:在Windows上,私钥文件的路径最好使用绝对路径,并且注意MSYS2的路径转换(C:\Users\...在MSYS2中可能是/c/Users/...)。为了兼容性,可以将私钥文件放到MSYS2的家目录下(如/home/yourname/.ssh/)。

4.2 SFTP目录遍历与批量上传

单文件上传只是开始,更常见的需求是上传整个目录。这需要用到SFTP的目录遍历API。

void upload_directory(LIBSSH2_SFTP *sftp_session, const char *local_dir, const char *remote_dir) { LIBSSH2_SFTP_HANDLE *sftp_handle; LIBSSH2_SFTP_ATTRIBUTES attrs; char buffer[1024]; struct dirent *entry; DIR *dir = opendir(local_dir); if (!dir) { perror("打开本地目录失败"); return; } // 尝试在远程创建目录(如果不存在) // 注意:libssh2_sftp_mkdir 可能因为目录已存在而失败,需要处理 libssh2_sftp_mkdir(sftp_session, remote_dir, 0755); while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; // 跳过当前目录和上级目录 } char local_path[2048]; char remote_path[2048]; snprintf(local_path, sizeof(local_path), "%s/%s", local_dir, entry->d_name); snprintf(remote_path, sizeof(remote_path), "%s/%s", remote_dir, entry->d_name); // 获取本地文件信息,判断是文件还是目录 if (entry->d_type == DT_DIR) { // 如果是目录,递归上传 upload_directory(sftp_session, local_path, remote_path); } else if (entry->d_type == DT_REG) { // 如果是普通文件,调用之前实现的单文件上传函数 printf("上传文件: %s -> %s\n", local_path, remote_path); // 这里需要调用一个封装好的 upload_file 函数 // upload_file(sftp_session, local_path, remote_path); } } closedir(dir); }

这个递归函数展示了思路。你需要一个statlstat的替代品来在Windows/MSYS2环境下可靠地获取文件类型信息(direntd_type在Windows上可能不可靠),可能需要使用_stat函数。此外,需要将单文件上传的逻辑封装成一个独立的函数供其调用。

5. 编译与运行中的典型报错与解决方案

即使代码看起来没问题,编译和运行阶段也常常会遇到各种错误。下面是我总结的几个高频问题。

5.1 编译链接错误

错误信息可能原因解决方案
undefined reference tolibssh2_init'`链接器找不到libssh2库。1. 检查-L/usr/local/lib路径是否正确。
2. 检查是否安装了mingw-w64-x86_64-libssh2包(如果用pacman安装的二进制包,库名可能是-llibssh2)。
3. 确保编译命令中-lssh2在源文件之后。
cannot find -lssl-lcrypto链接器找不到OpenSSL库。1. 确认OpenSSL已成功编译并make install
2. 检查/usr/local/lib下是否存在libssl.alibcrypto.a
3. 如果是动态链接,运行时需要对应的DLL。
socket‘, ’connect‘, ’inet_addr‘ 未定义的引用没有链接Windows的Winsock库。在编译命令末尾添加-lws2_32
error: ‘LIBSSH2_SFTP_ATTRIBUTES’ has no member named ‘flags’库版本与API不匹配。libssh2的API在不同版本间可能有细微变化。检查你使用的libssh2头文件版本,并对照官方文档或示例代码调整你的结构体成员访问方式。

5.2 运行时连接与认证错误

错误现象/信息可能原因排查步骤与解决方案
连接服务器失败网络不通,服务器IP/端口错误,防火墙阻止。1. 用pingtelnet(或Test-NetConnectionin PowerShell)测试服务器可达性和22端口是否开放。
2. 检查Windows防火墙和服务器防火墙(如firewalldufw)规则。
SSH握手失败服务器不支持客户端提供的协议版本或算法。1. 检查libssh2_session_last_error的详细错误信息。
2. 尝试在创建会话后,握手前设置更兼容的算法列表:
libssh2_session_method_pref(session, LIBSSH2_METHOD_KEX, "diffie-hellman-group14-sha1,diffie-hellman-group1-sha1");(注意:弱算法仅用于测试兼容性,生产环境应使用更强算法)。
密码认证失败用户名/密码错误,服务器禁止密码登录。1. 核对用户名和密码。
2. 检查服务器/etc/ssh/sshd_configPasswordAuthentication是否为yes
3. 考虑使用密钥认证。
公钥认证失败私钥格式不对、路径错误、权限问题、公钥未部署。1.路径问题:确保私钥路径在MSYS2环境中可访问。使用绝对路径。
2.格式问题:libssh2主要支持OpenSSH格式的私钥(以-----BEGIN RSA PRIVATE KEY----------BEGIN OPENSSH PRIVATE KEY-----开头)。如果是PuTTY的.ppk格式,需要先用PuTTYgen工具转换为OpenSSH格式。
3.权限问题:在Linux上,私钥文件权限需为600。在Windows上,libssh2对此不敏感,但路径访问权限要对。
4.服务器配置:确认公钥已正确添加到服务器的~/.ssh/authorized_keys文件中,并且该文件权限为600,上级目录.ssh权限为700。
初始化SFTP失败服务器SFTP子系统未启用或配置问题。1. 确保服务器sshd_config中有Subsystem sftp /usr/lib/openssh/sftp-server或类似配置。
2. 有些服务器可能将SFTP限制在特定用户组或ChrootDirectory内,检查相关配置。

5.3 文件操作错误

错误信息可能原因解决方案
打开远程文件失败: Permission denied远程目录不存在,或用户对该路径没有写权限。1. 确保远程目录存在。可以用sftp命令手动测试。
2. 检查目标路径的权限:ls -ld /remote/path
3. 尝试先在远程创建一个有权限的目录(如/tmp下)。
SFTP写入错误: Failure磁盘空间不足,或网络中断。1. 检查远程服务器磁盘空间:df -h
2. 增加网络稳定性检查,考虑实现断点续传逻辑(对于大文件)。
上传大文件时程序卡住或崩溃可能触发了服务器的流量控制或超时限制;或者本地读取文件方式有问题。1. 检查服务器sshd_config中的ClientAliveIntervalClientAliveCountMax设置。
2. 在代码中,对于大文件,不要一次性fread到内存,应该分块读取并上传,例如每次读取64KB。
3. 考虑使用非阻塞模式并配合libssh2_sftp_write的返回值进行更精细的控制。

一个关键的调试技巧:充分利用libssh2_session_last_error函数。当任何libssh2函数调用返回错误时,立即调用这个函数获取详细的错误描述、错误码,这是定位问题的第一手资料。把它打印出来,很多问题就一目了然了。

6. 性能优化与生产环境考量

一个能跑通的Demo和一個能在生产环境稳定运行的工具之间,还有不少距离。

6.1 连接池与会话复用

如果你的程序需要频繁上传多个小文件,为每个文件都建立一次SSH连接和SFTP会话开销巨大。正确的做法是复用连接

  1. 初始化并保持一个全局的LIBSSH2_SESSIONLIBSSH2_SFTP *
  2. 设计一个上传函数,它接收本地文件路径和远程路径,但复用已有的会话和SFTP会话指针。
  3. 在所有上传任务完成后,再统一关闭和释放资源。
  4. 需要考虑网络异常断开的处理,实现重连机制。可以通过定期发送空请求(如libssh2_keepalive_send)来保持连接活跃,并检测连接状态。

6.2 非阻塞模式与事件循环

我们之前的例子用的是阻塞模式,简单但效率低,在上传大文件时整个线程会被挂起。对于需要高并发或响应用户界面的程序,应该使用非阻塞模式

libssh2_session_set_blocking(session, 0); // 设置为非阻塞

设置为非阻塞后,libssh2_sftp_write等函数可能返回LIBSSH2_ERROR_EAGAIN,表示“请稍后再试”。这时你需要:

  1. 使用select()poll()等系统调用监控套接字(sock)的可读/可写状态。
  2. 当套接字准备好时,再次调用之前返回EAGAIN的函数。
  3. 这需要实现一个状态机来管理整个上传流程(连接、认证、打开文件、写入循环、关闭),代码复杂度会显著增加,但能获得更好的性能和响应能力。

6.3 传输进度与断点续传

对于大文件,显示上传进度和实现断点续传是提升用户体验的关键。

  • 进度:在libssh2_sftp_write的循环中,你已经记录了total_written,用它除以总文件大小就能计算进度百分比。
  • 断点续传:思路是,上传前先检查远程文件是否存在及其大小。
    1. 使用libssh2_sftp_stat获取远程文件属性。
    2. 如果文件存在,将其大小作为本地文件读取的起始偏移量(fseek)。
    3. 使用libssh2_sftp_open时,使用LIBSSH2_FXF_WRITE | LIBSSH2_FXF_APPEND标志以追加模式打开文件。
    4. 从本地文件的偏移位置开始读取并上传。

6.4 错误处理与日志记录

生产代码必须有健壮的错误处理和详尽的日志。

  • 返回值检查:每一个libssh2、socket、文件IO函数的返回值都必须检查。
  • 资源释放:使用goto标签进行集中式的错误处理和资源清理(如示例代码所示),确保在任何错误路径下,已分配的资源(内存、文件句柄、会话、套接字)都能被正确释放,避免泄漏。
  • 日志分级:将日志分为DEBUG、INFO、WARN、ERROR等级别。在开发阶段开启DEBUG,记录详细的函数调用和网络数据包信息;在生产环境只记录ERROR和关键的INFO信息。可以将日志输出到文件,并包含时间戳、进程ID、错误码和libssh2_session_last_error的信息。

7. 封装与集成:走向实用工具

掌握了核心原理后,你可以将这个功能封装起来,集成到更大的系统中。

  • 封装成独立函数库:将SSH连接管理、SFTP上传、下载、目录列表等功能封装成清晰的C API或C++类。对外提供简单的接口,如ssh2_upload(const char* host, ...),隐藏内部复杂的资源管理和错误处理。
  • 制作命令行工具:使用getopt解析命令行参数(主机、端口、用户名、认证方式、本地路径、远程路径),做成一个类似scp但功能定制的工具。
  • 集成到其他语言:如果你主要用Python、C#等,可以寻找这些语言对libssh2的绑定(如pysftp底层可能是paramiko,但也可以找libssh2-python这样的绑定),或者使用其内置的SSH/SFTP库(如Python的paramiko)。本教程的C语言实现为你理解这些高级库的底层原理提供了坚实基础。
  • 与CI/CD集成:在Windows的CI/CD流水线(如Jenkins on Windows, GitHub Actions的Windows Runner)中,你可以编译这个工具,用于将构建产物(如.exe安装包、文档)自动上传到Linux发布服务器或存储仓库。

走到这一步,你已经不仅仅是一个工具的使用者,而是具备了根据实际需求定制和开发底层文件传输能力的技术专家。从解决“怎么传文件”这个具体问题出发,深入到网络协议、加密认证、跨平台编译、系统编程和错误处理的层面,这才是这个项目带来的最大价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询