
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.1libssh2 使用 BSD 风格许可证。如果你的项目要做商业闭源分发libssh2 在许可证上更省心如果只是内部工具或者愿意遵守 LGPL 公示义务libssh 也没问题。依赖上两者都支持多种加密后端。常见的选择是 OpenSSL 或 mbedTLS也有纯算法库选项。libssh2 对 mbedTLS 的支持做得比较扎实很多嵌入式设备、路由器固件里能看到它的身影。libssh 也可以搭配 OpenSSL但如果你在交叉编译环境里折腾过会发现 libssh2 的配置脚本和 CMake 选项更简单直接。提示如果你在 Windows 上编译libssh2 默认需要 WinSock记得在工程里链接ws2_32libssh 则提供了更完善的 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 11C17。先安装开发库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_castint(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_castsockaddr*(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 表示通道读到 EOFlibssh2 的libssh2_channel_read返回 0 同样表示 EOF但负值可能是错误也可能是 EAGAIN。非阻塞模式下如果使用默认 socket两个库都会阻塞在 read 上直到数据到达或超时。如果你要命令执行后立刻知道退出码libssh 可以调用ssh_channel_get_exit_statuslibssh2 则没有直接的退出码查询函数需要先关掉 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 快速对比表对比维度libsshlibssh2许可证LGPL-2.1BSD上手难度较低高层封装完善较高需要自己处理更多底层细节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_serverlibssh2 需要自己读取服务器公钥再比对细节较多但必须做。第二注意静态链接带来的符号冲突。如果程序里同时依赖 libssh 和 OpenSSL并且 libssh 是静态编译的要确保 OpenSSL 版本唯一。否则某些机器上会随机出现内存错误或 TLS 握手失败。第三不要跨线程共享 session。一定要每个线程独立建连或者使用独立的 session 对象。线程池模型里最好把 session 作为线程局部变量管理。第四内存释放顺序不能错。libssh 要先释放 channel 再断开 sessionlibssh2 要先 close channel 再 disconnect再 free session最后关闭 socket。顺序反了轻则泄漏重则崩溃。最后再分享一个小技巧调试时可以把两个库的日志开关打开。libssh 通过ssh_set_log_callback或者环境变量LIBSSH_LOG输出调试日志libssh2 用libssh2_trace(session, LIBSSH2_TRACE_CONN)可以查看连接层详细记录。遇到握手失败或者通道断开先开日志看协议层面发生了什么往往比盯着业务代码猜得快很多。这两个库我都深度用过谈不上谁更好只能说不同场景下各有侧重。拿到需求先问自己三个问题是不是简单工具要不要移植到嵌入式需不需要完全掌控 socket答案清晰了选择自然就出来了。