C++开发者天天跟远程服务器打交道,SSH 几乎是绕不开的协议。早期要么直接调system("ssh ...")凑合,要么自己拼 socket 手搓协议,都不太靠谱。后来我需要在 C++ 程序里内嵌一个 SSH 客户端,做远程命令下发和文件拉取,于是开始认真对比 libssh 和 libssh2。这两个库名字相似,功能重叠,网上却很少有把两者掰开揉碎讲清楚的实战文章。这篇就围绕“怎么选、怎么写、怎么避坑”展开,附上完整可编译的代码示例,给正在做技术选型的 C++ 开发者一个参考。
先说结论:libssh 和 libssh2 不是同一个库的版本分支,而是两个互相独立的开源项目,API 设计理念差别很大。libssh 走的是“高内聚、易用好懂”的路线,封装了很多开箱即用的能力;libssh2 更强调“轻量、可移植、嵌入友好”,API 细碎但灵活。理解了这个本质差异,很多对比项就顺理成章了。
1. 选型前先看清本质:libssh 与 libssh2 的出身和架构差异
1.1 它们不是同一个项目的两个版本
我见过不少同事以为libssh2是libssh的升级版,实际上完全是两个独立社区维护的项目。libssh 主要由 libssh 项目组维护,发展历史更老一点,社区活跃度也一直在线;libssh2 则由 libssh2 项目组维护,和 libcurl 生态有着千丝万缕的关系,比如 curl 的 SFTP 支持底层用的就是 libssh2。
这一点直接决定了它们的定位。libssh 更像一个“完整的 SSH 工具包”,自带认证、隧道、SFTP、服务器端支持,甚至可以拿来写一个简单的 SSH 服务端;libssh2 则偏向“协议栈”,只提供客户端能力,而且很多高层逻辑需要开发者自己拼装。如果你需要的不仅仅是一个远程执行命令的函数,还有端口转发、密钥代理、SFTP 流式读写等复杂场景,这两者的使用感受会非常不一样。
1.2 许可证和依赖取舍
许可证方面,libssh 使用 LGPL-2.1,libssh2 使用 BSD 风格许可证。如果你的项目要做商业闭源分发,libssh2 在许可证上更省心;如果只是内部工具或者愿意遵守 LGPL 公示义务,libssh 也没问题。
依赖上两者都支持多种加密后端。常见的选择是 OpenSSL 或 mbedTLS,也有纯算法库选项。libssh2 对 mbedTLS 的支持做得比较扎实,很多嵌入式设备、路由器固件里能看到它的身影。libssh 也可以搭配 OpenSSL,但如果你在交叉编译环境里折腾过,会发现 libssh2 的配置脚本和 CMake 选项更简单直接。
提示:如果你在 Windows 上编译,libssh2 默认需要 WinSock,记得在工程里链接
ws2_32;libssh 则提供了更完善的 CMake 集成,用 vcpkg 装一下就很省事。
1.3 构建链路差异带来的连锁影响
我在 Linux 上通常直接用系统包管理器装开发库,Ubuntu 下是libssh-dev和libssh2-1-dev。但到了 CentOS 老版本上,libssh2 的版本可能比较旧,导致某些新 API 不可用。libssh 也遇到过类似问题,不过它的版本迭代节奏相对稳。
如果你的业务要同时依赖 OpenSSL 的某个特性和 SSH 库,务必检查两者链接的是同一个 OpenSSL 版本,否则运行时可能因为符号冲突出诡异问题。我自己就踩过:程序里同时链接了 libcrypto 1.1 和 libssh2 静态包里的旧版 crypto,导致 RSA 认证时偶发崩溃。这种问题排查起来非常隐蔽,建议在构建脚本里强制统一依赖路径。
2. API 设计风格对比:高层封装与底层细节
2.1 libssh 的会话式 API,读起来像“在讲故事”
libssh 的 API 设计非常友好,核心概念是ssh_session。它把连接、握手、认证、通道都串在同一个对象上,代码读起来接近自然语言的顺序。比如建立连接再认证,再开通道执行命令,每一步都有明确的返回值,看得懂SSH_OK和SSH_ERROR基本就能上手。
这种风格对团队协作很友好。新成员看 libssh 的示例代码就能理解流程,不用翻协议文档。尤其当你需要处理主机密钥验证时,libssh 提供了ssh_session_is_known_server()这种高层函数,直接封装了 known_hosts 检查流程,省去很多底层比对代码。
2.2 libssh2 的纯 C 回调风格,自由度更高代价是琐碎
libssh2 的 API 则更贴近传统 C 库,命名是libssh2_xxx_xxx的一大串。它的 session 初始化、握手、认证、通道创建这些步骤必须严格按顺序调用,每一步都要自己处理返回值,尤其是需要自己分配 socket fd 再交给 libssh2 使用。好处是你能完全控制底层 socket 行为,比如自定义超时时间、连接代理、非阻塞事件循环;坏处是代码里容易出现大量错误处理分支。
我用 libssh2 写第一个可用的执行命令程序时,代码量比 libssh 版本多不少,主要多在sockaddr构造、socket 连接、握手循环、非零返回值的处理上。不过 libssh2 的手册和示例代码很全,本质上属于“第一次写着累,写好了后面反而好复用”的类型。
2.3 从代码量看上手成本
为了直观一点,我对比过同一个功能点分别在两个库里的代码行数(不含错误处理)。连接加密码认证加执行uname -a打印输出,libssh 版本大约 60 行核心代码,libssh2 版本需要 80 到 90 行。差距不算巨大,但体现出的思维模式完全不同:libssh 的ssh_channel_request_exec一步到位,libssh2 需要先libssh2_channel_open_session再libssh2_channel_exec。
如果你只是写一次性脚本工具,我更推荐 libssh;如果是在资源受限的嵌入式环境里做长期维护,libssh2 的轻量运行时可能更合适。
3. 功能地图与实测心得:不止是远程执行命令
3.1 认证方式的覆盖度对比
两者都支持密码认证、公钥认证、键盘交互认证。但 API 层面的体验有区别。libssh 的ssh_userauth_password和ssh_userauth_publickey_auto用起来比较顺,尤其公钥自动认证,它会自动读取默认路径的私钥,并处理 known_hosts 校验,适合快速连通。
libssh2 的公钥认证需要手动指定私钥路径,甚至要设置公钥内容。比如libssh2_userauth_publickey_fromfile需要传 username、publickey、privatekey、passphrase 几个参数,少了哪个都用不了。从安全性角度看这其实更可控,但第一次用的人容易摸不着头脑。
3.2 SFTP 和 SCP 支持差异
如果需要传输文件,两个库都提供 SFTP 子系统支持。libssh 的 SFTP API 命名是sftp_new、sftp_open、sftp_read,和 POSIX 文件操作函数高度相似,写起来很顺手。libssh2 则需要在 session 上先libssh2_sftp_init拿到LIBSSH2_SFTP句柄,然后再用libssh2_sftp_open等函数操作。
我在实际项目里用 libssh2 做多文件批量下载时发现,它的libssh2_sftp_read返回非零值可能不是错误,可能是读取长度,也可能是阻塞等待重试,必须用libssh2_session_last_error和是否 EAGAIN 来区分。libssh 在这点上处理得更清晰,错误信息直接挂在 session 或 sftp 对象的 error 字段上。
3.3 多线程和会话复用的大坑
另一个常见需求是并发连接多个服务器。两个库的 session 都不是线程安全的,每个线程必须创建自己的 session,不能多个线程共享一个 session 去轮发命令。另一种做法是单线程事件循环,配合非阻塞模式处理多个会话。libssh 官方文档对非阻塞模式有专门说明,libssh2 也支持非阻塞,但要小心处理LIBSSH2_ERROR_EAGAIN。
我踩过的坑是:用同一 socket 句柄在多个线程里同时调用 libssh2 的读和写,结果数据包直接错乱,服务端断连。后来改成每线程独立 session 和独立 socket 才稳定。如果你的并发量很大,建议先把连接池模型设计好。
4. 实战代码示例:用 libssh 和 libssh2 各写一个 SSH 客户端
4.1 编译准备与环境说明
下面的示例都在 x86_64 Linux 上测试过,编译器是 GCC 11,C++17。先安装开发库,Ubuntu/Debian 下执行:
sudo apt-get install libssh-dev libssh2-1-devCMake 里可以这样引入:
find_package(PkgConfig REQUIRED) pkg_check_modules(LIBSSH REQUIRED IMPORTED_TARGET libssh) pkg_check_modules(LIBSSH2 REQUIRED IMPORTED_TARGET libssh2) target_link_libraries(ssh_demo PkgConfig::LIBSSH) target_link_libraries(ssh2_demo PkgConfig::LIBSSH2)如果你使用 vcpkg,直接vcpkg install libssh libssh2也不复杂。注意两个库的头文件都可以同时存在,但建议分开编译链接,避免预处理宏冲突。
4.2 libssh 实现:连接、密码认证、执行命令
直接用 C++ 简单封装,核心代码如下:
#include <libssh/libssh.h> #include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> #include <string> bool execute_remote(ssh_session session, const std::string& cmd) { ssh_channel channel = ssh_channel_new(session); if (!channel) return false; if (ssh_channel_open_session(channel) != SSH_OK) { std::cerr << "open session failed: " << ssh_get_error(session) << std::endl; ssh_channel_free(channel); return false; } if (ssh_channel_request_exec(channel, cmd.c_str()) != SSH_OK) { std::cerr << "exec failed: " << ssh_get_error(session) << std::endl; ssh_channel_close(channel); ssh_channel_free(channel); return false; } char buffer[4096]; int nbytes; while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) { fwrite(buffer, 1, nbytes, stdout); } ssh_channel_send_eof(channel); ssh_channel_close(channel); ssh_channel_free(channel); return true; } int main(int argc, char** argv) { if (argc < 4) { std::cerr << "usage: " << argv[0] << " host user password" << std::endl; return 1; } const char* host = argv[1]; const char* user = argv[2]; const char* password = argv[3]; int port = 22; ssh_session session = ssh_new(); if (!session) { std::cerr << "ssh_new failed" << std::endl; return 1; } ssh_options_set(session, SSH_OPTIONS_HOST, host); ssh_options_set(session, SSH_OPTIONS_USER, user); ssh_options_set(session, SSH_OPTIONS_PORT, &port); if (ssh_connect(session) != SSH_OK) { std::cerr << "connect failed: " << ssh_get_error(session) << std::endl; ssh_free(session); return 1; } // 生产环境务必校验服务器公钥指纹,这里略过 if (ssh_userauth_password(session, nullptr, password) != SSH_AUTH_SUCCESS) { std::cerr << "auth failed: " << ssh_get_error(session) << std::endl; ssh_disconnect(session); ssh_free(session); return 1; } execute_remote(session, "uname -a && whoami"); ssh_disconnect(session); ssh_free(session); return 0; }这段代码里有两个细节值得注意。第一,SSH_OPTIONS_PORT接收的是int*,所以要本地定义一个port变量,不能直接传&(const_cast<int&>(port))之类的临时值。第二,ssh_channel_read的最后一个参数是 stderr 标志,传 0 表示读 stdout,传 1 表示读 stderr,如果你希望同时捕获错误输出,需要分别读两个通道或自行合并。
4.3 libssh2 实现:连接、密码认证、执行命令
libssh2 需要自己创建 socket,完整示例代码如下:
#include <libssh2.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #include <unistd.h> #include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> int main(int argc, char** argv) { if (argc < 4) { std::cerr << "usage: " << argv[0] << " host user password" << std::endl; return 1; } const char* host = argv[1]; const char* user = argv[2]; const char* password = argv[3]; int port = 22; int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { perror("socket"); return 1; } sockaddr_in sin{}; sin.sin_family = AF_INET; sin.sin_port = htons(port); if (inet_pton(AF_INET, host, &sin.sin_addr) <= 0) { std::cerr << "invalid address" << std::endl; close(sock); return 1; } if (connect(sock, reinterpret_cast<sockaddr*>(&sin), sizeof(sin)) != 0) { perror("connect"); close(sock); return 1; } if (libssh2_init(0) != 0) { std::cerr << "libssh2_init failed" << std::endl; close(sock); return 1; } libssh2_session* session = libssh2_session_init(); if (!session) { std::cerr << "session init failed" << std::endl; close(sock); return 1; } if (libssh2_session_handshake(session, sock) != 0) { std::cerr << "handshake failed: " << libssh2_session_last_error(session, nullptr, nullptr, 0) << std::endl; libssh2_session_free(session); close(sock); return 1; } if (libssh2_userauth_password(session, user, password) != 0) { std::cerr << "auth failed: " << libssh2_session_last_error(session, nullptr, nullptr, 0) << std::endl; libssh2_session_disconnect(session, "auth failed"); libssh2_session_free(session); close(sock); return 1; } libssh2_channel* channel = libssh2_channel_open_session(session); if (!channel) { std::cerr << "channel open failed" << std::endl; libssh2_session_disconnect(session, "channel failed"); libssh2_session_free(session); close(sock); return 1; } const char* cmd = "uname -a && whoami"; if (libssh2_channel_exec(channel, cmd) != 0) { std::cerr << "exec failed" << std::endl; libssh2_channel_free(channel); libssh2_session_disconnect(session, "exec failed"); libssh2_session_free(session); close(sock); return 1; } char buffer[4096]; int n; while ((n = libssh2_channel_read(channel, buffer, sizeof(buffer))) > 0) { fwrite(buffer, 1, n, stdout); } libssh2_channel_free(channel); libssh2_session_disconnect(session, "bye"); libssh2_session_free(session); libssh2_exit(); close(sock); return 0; }libssh2 的代码明显更“啰嗦”:socket 建连要自己写,握手要自己调,错误码要通过libssh2_session_last_error去拿。但好处是 socket 层完全可控,你可以很方便地接入自己的连接池、加代理、做流量统计。
4.4 代码逐段解析与踩坑记录
执行命令后,两个库的读取逻辑都有一个共同点:不能只调用一次read就认为命令输出结束了。远程命令产生的输出可能分多包到达,必须循环读取直到返回 0 或负值。libssh 的ssh_channel_read返回 0 表示通道读到 EOF,libssh2 的libssh2_channel_read返回 0 同样表示 EOF,但负值可能是错误也可能是 EAGAIN。
非阻塞模式下,如果使用默认 socket,两个库都会阻塞在 read 上,直到数据到达或超时。如果你要命令执行后立刻知道退出码,libssh 可以调用ssh_channel_get_exit_status;libssh2 则没有直接的退出码查询函数,需要先关掉 channel 再读取exit-status扩展,操作起来比较绕。
我在 libssh2 上第一次执行命令后没关闭 channel 就去拿退出码,结果调试了一下午才明白要先libssh2_channel_close。这个细节在官方示例里时有提及,但新手很容易忽略。
5. 深入:SFTP 文件传输完整示例
5.1 libssh 版本的上传下载代码
SFTP 是日常运维里的高频功能。以 libssh 为例,上传一个本地文件到远程,核心代码大致如下:
#include <libssh/libssh.h> #include <libssh/sftp.h> #include <cstdio> #include <cstring> bool upload_file(ssh_session session, const char* local_path, const char* remote_path) { sftp_session sftp = sftp_new(session); if (!sftp) { std::cerr << "sftp_new failed" << std::endl; return false; } if (sftp_init(sftp) != SSH_OK) { std::cerr << "sftp_init failed: " << ssh_get_error(session) << std::endl; sftp_free(sftp); return false; } sftp_file file = sftp_open(sftp, remote_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (!file) { std::cerr << "sftp_open failed: " << sftp_get_error(sftp) << std::endl; sftp_free(sftp); return false; } FILE* local = fopen(local_path, "rb"); if (!local) { sftp_close(file); sftp_free(sftp); return false; } char buffer[8192]; size_t n; while ((n = fread(buffer, 1, sizeof(buffer), local)) > 0) { ssize_t written = sftp_write(file, buffer, n); if (written < 0) { std::cerr << "sftp_write failed" << std::endl; break; } } fclose(local); sftp_close(file); sftp_free(sftp); return true; }值得说明的是sftp_write的返回值是 ssize_t。官方文档建议每次写入后检查返回值,如果小于请求长度可能需要重试。实测中大文件传输时偶尔会遇到部分写入,使用循环确保全部写出非常关键。
5.2 libssh2 版本的 SFTP 关键片段
libssh2 的 SFTP 初始化需要两步:先libssh2_sftp_init(session),再执行文件操作。上传逻辑如下:
#include <libssh2_sftp.h> bool upload_file_v2(libssh2_session* session, const char* local_path, const char* remote_path) { LIBSSH2_SFTP* sftp = libssh2_sftp_init(session); if (!sftp) { std::cerr << "sftp init failed" << std::endl; return false; } LIBSSH2_SFTP_HANDLE* handle = libssh2_sftp_open(sftp, remote_path, LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC, 0644); if (!handle) { std::cerr << "sftp open failed" << std::endl; libssh2_sftp_shutdown(sftp); return false; } FILE* local = fopen(local_path, "rb"); if (!local) { libssh2_sftp_close(handle); libssh2_sftp_shutdown(sftp); return false; } char buffer[8192]; size_t n; while ((n = fread(buffer, 1, sizeof(buffer), local)) > 0) { ssize_t written = libssh2_sftp_write(handle, buffer, n); while (written < 0 && written == LIBSSH2_ERROR_EAGAIN) { written = libssh2_sftp_write(handle, buffer, n); } if (written < 0) { std::cerr << "sftp write failed: " << libssh2_session_last_error(session, nullptr, nullptr, 0) << std::endl; break; } } fclose(local); libssh2_sftp_close(handle); libssh2_sftp_shutdown(sftp); return true; }注意我在循环里额外处理了LIBSSH2_ERROR_EAGAIN,这是非阻塞模式下最容易遗漏的分支。即使你使用的是阻塞 socket,在高负载或远程端窗口不足时也可能遇到 EAGAIN,忽略它会导致文件传输意外中断。
5.3 文件传输的进度回调与分段策略
如果你要做一个带进度条的上传下载工具,建议把大文件切成固定大小的缓冲块,循环调用 read/write,同时每处理完一块调用一次回调。实测 8192 字节缓冲区在大部分网络环境下表现稳定,太小会导致系统调用过于频繁,太大又可能受 SFTP 远端窗口限制。
另一个经验:下载文件时,不要用libssh2_sftp_read的返回值当作“文件结束”的唯一依据,应该判断是否已达到远端文件大小,或者读取到 0 字节。某些服务器实现可能在文件尾返回短读而不是 0,严格遵守“读取到 0 才结束”会卡在死循环里。
6. 选型建议:什么场景选哪个?
6.1 快速对比表
| 对比维度 | libssh | libssh2 |
|---|---|---|
| 许可证 | LGPL-2.1 | BSD |
| 上手难度 | 较低,高层封装完善 | 较高,需要自己处理更多底层细节 |
| socket 控制权 | 内部管理,逻辑简单 | 外部传入,灵活可控 |
| 服务端支持 | 支持 | 主要客户端 |
| SFTP API | 接近 POSIX,直观 | 句柄操作繁琐但要处理 EAGAIN |
| 嵌入式适配 | 相对重 | 轻量,交叉编译友好 |
| 文档与示例 | 官方文档较丰富 | 官方示例多,但组织略散 |
| 商业闭源分发 | 需注意 LGPL | 更友好 |
这张表不是绝对的,却基本反映了两者在真实项目里的倾向。如果你要快速交付一个内部工具,选 libssh;如果你在做一个嵌入式模块或希望完全掌控网络层,选 libssh2。
6.2 我在实际项目里的选择
我在做内部资产巡检工具时,一开始用的是 libssh2,原因很简单:它的运行时更轻,静态链接产物体积小。但开发过程中逐渐发现总是要自己封装 SFTP、反复处理 EAGAIN,迭代效率不高。后来切换到 libssh,代码量立刻降下来,而且它自带的ssh_session_is_known_server等实用函数帮我省了不少主机信任管理的功夫。
如果你问我“从零开始学,先用哪个”,我的建议是先写一个 libssh 版本的执行命令工具,快速建立信心;再尝试用 libssh2 重写一遍,体会底层细节。这个过程比只看文档有效得多。
6.3 避坑清单:这些坑我替你踩过了
第一,无论选哪个库,都要在生产环境配置主机指纹校验。不少示例代码为了图省事跳过这一步,结果被中间人攻击。libssh 提供了ssh_session_is_known_server,libssh2 需要自己读取服务器公钥再比对,细节较多但必须做。
第二,注意静态链接带来的符号冲突。如果程序里同时依赖 libssh 和 OpenSSL,并且 libssh 是静态编译的,要确保 OpenSSL 版本唯一。否则某些机器上会随机出现内存错误或 TLS 握手失败。
第三,不要跨线程共享 session。一定要每个线程独立建连,或者使用独立的 session 对象。线程池模型里最好把 session 作为线程局部变量管理。
第四,内存释放顺序不能错。libssh 要先释放 channel 再断开 session,libssh2 要先 close channel 再 disconnect,再 free session,最后关闭 socket。顺序反了轻则泄漏,重则崩溃。
最后再分享一个小技巧:调试时可以把两个库的日志开关打开。libssh 通过ssh_set_log_callback或者环境变量LIBSSH_LOG输出调试日志;libssh2 用libssh2_trace(session, LIBSSH2_TRACE_CONN)可以查看连接层详细记录。遇到握手失败或者通道断开,先开日志看协议层面发生了什么,往往比盯着业务代码猜得快很多。
这两个库我都深度用过,谈不上谁更好,只能说不同场景下各有侧重。拿到需求先问自己三个问题:是不是简单工具?要不要移植到嵌入式?需不需要完全掌控 socket?答案清晰了,选择自然就出来了。