☰
createCommunicationsException 排查指南:从SSH连接到SFTP上传的完整解决方案
2026/10/13 13:11:26 网站建设 项目流程

如果你在 Java 或 Android 项目里用 SSH 客户端库连远程服务器,对 createCommunicationsException 这个名字应该不陌生。它经常整条贴在日志里:com.jcraft.jsch.JSchException: createCommunicationsException: ...。说实话异常名本身没多少信息量,真正值钱的是冒号后面那一句,还有异常链里有没有嵌套的 IOException。它会出现在任何需要 SSH/SFTP 通信的场景里:App 里往服务器传日志、后端定时同步远端文件、CI 脚本连接代码仓库,只要网络链路或认证配置不对,它就冒出来。这篇文章我不会跟你念 API 文档,而是从实际排查经验出发,把这个问题拆成几个层面,告诉你每一步怎么验证、怎么改、怎么避免踩坑。不管你是刚接触 SSH 的小白,还是被线上告警折腾过几次的老手,都能找到可以直接抄的解决方案。

1. createCommunicationsException 不只是连接失败,它是一层包装纸

1.1 异常从哪里来:SSH 客户端库的通信层封装

先说结论:createCommunicationsException 并不是一个业务错误码,也不是某个从Exception直接继承出来的独立类名。它更多是 SSH 客户端库在建立连接、发送数据、读取服务端响应的过程中,把底层网络异常统一包装后生成的那句提示。底层的IOException可能是连接被拒绝、连接超时、连接重置、读不到数据、Socket 被关闭,等等。客户端库为了让你在业务层只和JSchException打交道,就把这些五花八门的网络问题收进同一个方法里,然后再抛出来。所以你经常会看到日志里的异常消息长成下面这样:

com.jcraft.jsch.JSchException: createCommunicationsException: Connection refused

这里Connection refused才是真正值得看的内容。再往下翻,如果打开了整个堆栈,还能看到java.net.ConnectException之类的嵌套根因。遇到这类异常,第一反不要“这库怎么这么不稳定”,而是“底层网络到底发生了什么”。我在排查线上问题时,经常在群里看到有人直接贴一行异常消息截图,这信息量是不够的。正确姿势是把getCause()打出来,或者至少把异常的完整toString()和堆栈打出来。很多团队用的日志框架会默认吞掉 cause 细节,只记录 message,导致你查了半天也看不到真实原因。

try { session.connect(15_000); } catch (JSchException e) { // 重点:把 cause 一起打出来 log.error("SSH session connect failed: {}", e.getMessage(), e); if (e.getCause() != null) { log.error("Root cause: {} - {}", e.getCause().getClass().getName(), e.getCause().getMessage()); } }

日志里也有可能出现连续两个createCommunicationsException: createCommunicationsException: ...,这是因为它内部某个环节又调用了同样的包装逻辑,不用被这种重复吓到,继续往里追 cause 就好。

1.2 真正要看的是伴随信息,异常名只是入口

我把实际遇到过的伴随信息整理成了几个典型的“信号词”,每个信号词背后对应的排查方向完全不同。如果你只盯着 createCommunicationsException 这一个名字,很容易把问题统一归结为“网络故障”,然后在服务器上重启、换端口、改乱七八糟的配置,浪费时间。

日志里的伴随信息大概率原因优先排查方向
Connection refused服务端端口未监听、防火墙拒绝连接端口连通性、服务状态、防火墙/安全策略
Connection timed out网络不通、安全策略丢包、服务器负载太高路由、链路质量、服务端负载
Connection reset连接被服务端强制断开服务端 sshd 日志、登录限制、网络中间设备
Algorithm negotiation fail客户端和服务端支持的加密算法交集为空SSH 算法协商配置、客户端库版本
UnknownHostExceptionDNS 解析失败域名解析、hosts 配置
Auth fail认证失败,但还没到网络层用户名密码、密钥、服务端认证方式
Host key verification failed主机密钥校验失败known_hosts、主机指纹变化

这个表格你可以直接截图保存。真正排查的时候,先看异常消息属于哪一类,再跳到后面的对应小节,效率会高很多。后面我也会按这个思路把每类问题展开讲。

2. 动手前先把环境复现起来,别靠猜

2.1 最小复现样例:一个简单的 SFTP 上传程序

很多人一看到连接异常,第一反应是去翻服务器配置,或者检查网络。但我的习惯是先在本地把问题稳定复现出来,因为只有能稳定复现的问题,才有办法验证修复方案。下面这个例子我用的是 Java 里最常见的 SSH 客户端库。你可以建一个最简单的 Maven 工程,只跑一段main方法,连目标服务器的 SFTP 端口,尝试上传一个小文件。代码尽量精简,减少变量干扰。

<dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.2.20</version> </dependency>
import com.jcraft.jsch.*; import java.io.FileInputStream; import java.util.Properties; public class SftpUploadDemo { public static void main(String[] args) throws Exception { String host = "192.0.2.10"; int port = 22; String user = "someuser"; String password = "somepassword"; JSch jsch = new JSch(); // 如果使用公钥登录,取消下一行注释,并换成你自己的私钥路径 // jsch.addIdentity("/path/to/id_rsa"); Session session = jsch.getSession(user, host, port); session.setPassword(password); session.setTimeout(10_000); Properties config = new Properties(); // 推荐使用 yes:第一次连接时人工确认指纹,并把指纹写入 known_hosts config.put("StrictHostKeyChecking", "yes"); config.put("PreferredAuthentications", "publickey,password,keyboard-interactive"); session.setConfig(config); try { session.connect(15_000); } catch (JSchException e) { e.printStackTrace(); if (e.getCause() != null) { e.getCause().printStackTrace(); } return; } ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp"); sftp.connect(10_000); try (FileInputStream in = new FileInputStream("/local/file.txt")) { sftp.put(in, "/remote/file.txt"); } sftp.disconnect(); session.disconnect(); } }

这个代码里用了文档保留地址192.0.2.10,实际测试时换成你自己的服务器地址。跑起来之后,如果异常复现了,你就有了一个可以反复调整的最小环境。接下来做任何配置变更都先在这里验证,不要在线上直接盲改。

2.2 打开 SSH 客户端日志,让异常自己开口

复现出异常之后,很多人会立刻去服务端翻日志,但如果客户端日志里能看到 SSH 握手走到了哪一步,很多问题当场就能定案。JSch 自带了一个Logger接口,默认情况下是不输出的。我们需要在程序初始化时挂一个简单实现,把所有日志打到控制台。

JSch.setLogger(new JSch.Logger() { @Override public boolean isEnabled(int level) { return true; } @Override public void log(int level, String message) { System.out.println("JSCH[" + level + "] " + message); } });

加上这段之后,再跑一次程序,你会看到类似下面的输出:

JSCH[1] Connecting to 192.0.2.10 port 22 JSCH[1] Connection established JSCH[1] Remote version string: SSH-2.0-OpenSSH_8.9 JSCH[1] SSH_MSG_KEXINIT sent JSCH[1] SSH_MSG_KEXINIT received JSCH[1] SSH_MSG_KEXINIT failed

注意看两行关键日志:Connection established出现,说明 TCP 层已经通了;再往后如果 KEXINIT 后失败,问题基本就在算法协商;如果连Connection established都没出现,说明是网络层问题。这比看业务层异常要精准得多。客户端日志配合服务端日志,基本能把问题定位到某一个具体环节。

2.3 四个影响连接成败的 session 配置项

这里我特别提醒四个配置,每一个都有可能导致 createCommunicationsException,而且踩坑的概率非常高。

第一个是StrictHostKeyChecking。很多老教程图省事,直接设成no,然后线上就裸奔。正确做法是保持yes或ask,并提前把服务器公钥指纹写入 known_hosts。后面我会专门讲这个校验机制。

第二个是PreferredAuthentications。如果服务端只支持公钥认证,而你默认走密码,登录就会失败。一般我会把publickey,password,keyboard-interactive都列上,让客户端按顺序尝试。

第三个是Session.setTimeout()。这个超时是 Socket 读写超时,不是连接超时。如果你只设置了session.connect()的超时参数,但服务端一直不返回数据,客户端可能会一直等下去。合理的做法是两个都设:setTimeout(10_000)加上connect(15_000)。

第四个是Channel的连接超时。很多人只设置了session.connect(15_000),却忘了channel.connect()也要给超时时间。SFTP 通道建立失败时,同样会包装成连接类异常,但不一定有createCommunicationsException字样。为了统一排查,给通道也设上超时。

private static final int SESSION_TIMEOUT_MS = 15_000; private static final int CHANNEL_TIMEOUT_MS = 10_000; private static final int READ_TIMEOUT_MS = 10_000; Session session = jsch.getSession(user, host, port); session.setPassword(password); session.setTimeout(READ_TIMEOUT_MS); session.connect(SESSION_TIMEOUT_MS); ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp"); sftp.connect(CHANNEL_TIMEOUT_MS);

3. 排查 createCommunicationsException 的四个层面

3.1 网络层:连接是被拒绝、被中断还是没响应

拿到 createCommunicationsException 之后,我第一件事永远是判断 TCP 层是否真的通。很多看起来像应用层的问题,其实是安全策略把端口挡了,或者服务器只允许特定来源 IP 访问。

最简单的验证方式是用nc命令探测端口。Linux 和 macOS 下一般都有这个工具。

nc -vz 192.0.2.10 22

如果返回succeeded,说明 22 端口从你当前所在网络是可达的。如果报Connection refused,可能是服务没起来,也可能是防火墙直接拒绝了。如果长时间卡住不动,最后timed out,那大概率是安全策略丢包或者链路不可达。

在 Java 程序里,也可以写一个非常轻量的 Socket 测试,排除 SSH 客户端库的干扰:

import java.net.InetSocketAddress; import java.net.Socket; public class TcpProbe { public static void main(String[] args) throws Exception { String host = "192.0.2.10"; int port = 22; try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(host, port), 5_000); System.out.println("TCP connect ok"); } catch (Exception e) { e.printStackTrace(); } } }

如果这个 Socket 测试都连不上,那就别折腾 SSH 客户端了,先解决网络层面。常见原因包括:服务端防火墙没放行 22 端口、服务端 sshd 没启动、来源 IP 被限制、路由器或云平台安全策略拦截。还有一种情况是服务器端fail2ban之类的保护机制把来源 IP 临时封禁了,表现也是Connection refused或超时。遇到这类问题,去服务端查一查系统日志,比如/var/log/auth.log或/var/log/secure。

3.2 认证层:账号密码与公钥登录的坑

TCP 通了,但 SSH 客户端日志停在版本号交换之后,然后抛Auth fail,这就是认证层的问题。认证层并不复杂,但变种很多。

第一,检查用户名和密码是否真的正确。密码里如果有特殊字符,比如@、#、$,在配置文件和代码里要注意转义。我见过有人把密码写在 YAML 里,结果#被当成注释截断了。

第二,确认服务端支持的认证方式。OpenSSH 服务端的/etc/ssh/sshd_config里可以这样配置允许的认证方法:

PasswordAuthentication yes PubkeyAuthentication yes KbdInteractiveAuthentication no

如果你的服务端关闭了密码认证,只允许公钥登录,而客户端默认在尝试密码,你就会看到一遍遍的Permission denied。此时需要把客户端PreferredAuthentications改成publickey,并正确加载私钥。

第三,私钥文件本身有问题。OpenSSH 新版生成的私钥默认是 OpenSSH 格式,某些老版本的 SSH 客户端库并不能直接读取这种格式,会报invalid privatekey或者解析失败。解决办法是把私钥转成 PEM 格式:

ssh-keygen -p -m PEM -f id_rsa

转换过程会要求你输入私钥密码,不想设密码就直接回车。转换之后再用原来的客户端去加载。

第四,私钥文件权限太宽。Linux 下 OpenSSH 对私钥权限要求很严格,如果id_rsa是 644 或者 777,服务端会直接拒绝使用。用下面命令修正:

chmod 600 id_rsa

认证失败时,服务端日志会给出更多线索。如果有系统日志权限,直接看/var/log/auth.log或/var/log/secure,里面会记录类似Failed password for invalid user、Connection closed by authenticating user、User someuser from ... not allowed because not listed in AllowUsers的信息。这个信息比客户端日志准得多。

3.3 算法协商:Algorithm negotiation fail 的正确解法

算法协商失败是我遇到最多的一类,也是最容易让人一头雾水的。日志会显示:

createCommunicationsException: Algorithm negotiation fail

字符串里没有 IP,没有用户名,没有具体算法。只有打开客户端完整日志,才可能看到SSH_MSG_KEXINIT failed。服务端日志里通常会有这样一行:

Unable to negotiate with 192.0.2.20 port 50231: no matching key exchange method found. Their offer: diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521

这说明客户端发出去的密钥交换算法列表,和服务端能接受的列表没有交集。

为什么会出现这个情况?最典型的原因是服务器升级了 OpenSSH 版本,新版本默认禁用了一些旧的、不够安全的算法,而你的客户端库版本太老,还是只支持旧算法。比如某些老版本客户端还在使用diffie-hellman-group1-sha1或ssh-rsa签名,当服务器端把这些算法关掉后,两边就谈不拢了。

正确的解法是升级客户端库,让客户端支持新算法。如果你用的是老的 SSH 客户端库,建议升级到较新版本,新版本通常会支持curve25519-sha256、rsa-sha2-512等更安全的算法。升级之后,重新运行之前的最小复现程序,绝大多数算法协商失败都会消失。

另一种是临时兼容方案:在服务端sshd_config里把旧算法加回来。这个方案我不建议在生产环境长期使用,因为旧的加密和签名算法存在已知弱点,等于是主动把安全水位拉低。如果一定要做,可以考虑在调试阶段临时验证,确认客户端的真实问题后,尽快升级客户端并移除这些兼容配置。

此外,主机密钥算法也会出现协商失败,报错类似no matching host key type found. Their offer: ssh-ed25519, ssh-rsa。这种情况同样优先升级客户端,或者临时在服务端追加兼容的主机密钥算法。

3.4 主机密钥校验:StrictHostKeyChecking 不是开关,是安全机制

主机密钥校验失败时报的通常是Host key verification failed,但它同样会被包装成 createCommunicationsException 系列异常。你可能会看到下面的完整消息:

com.jcraft.jsch.JSchException: createCommunicationsException: Host key verification failed

这背后的机制是:SSH 客户端第一次连接服务器时,会拿到服务器的公钥指纹。为了防中间人攻击,客户端会把这个指纹记进 known_hosts 文件。之后每次连接都会比对,如果服务器公钥变了,就会拒绝连接。

最常见的场景是服务器重装系统或者重新生成了主机密钥,导致客户端 known_hosts 里保存的还是旧指纹。这时候不要简单粗暴地设置StrictHostKeyChecking=no,而是应该用带外渠道重新获取服务器指纹,更新 known_hosts。比如先用你信任的安全通道登录服务器,执行:

ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

拿到类似下面的指纹:

256 SHA256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234 hostname (ED25519)

然后再在客户端机器上,用ssh-keyscan把当前服务器的公钥保存到 known_hosts,并人工确认得到的指纹和服务器上读到的一致:

ssh-keyscan -t ed25519 192.0.2.10 >> ~/.ssh/known_hosts

在 Java 代码里,可以通过下面方式显式指定 known_hosts 文件:

jsch.setKnownHosts("/home/app/.ssh/known_hosts");

如果你在代码里看到了任何把StrictHostKeyChecking设成no的老代码,建议抓紧改成yes,并把服务器指纹管理起来。这个校验不是用来恶心人的,它保证你连的服务器就是你以为的那台。

4. 一个线上连接失败的完整排查实录

4.1 现象:定时任务导数据全部失败

某公司的内部数据同步项目,每天的定时任务会从一台远程 Linux 服务器拉取 CSV 文件到本地处理。某天早上开始,任务大面积失败,日志里清一色是:

com.jcraft.jsch.JSchException: createCommunicationsException: Algorithm negotiation fail

负责排查的同学一开始怀疑网络,测了ping和nc,TCP 端口是通的。又怀疑账号被锁,但换了几组账号都一样。后来把客户端完整日志打开,发现 SSH 握手在SSH_MSG_KEXINIT阶段就断了,并且服务端日志明确提示算法不匹配。

4.2 服务端 sshd 日志里的关键信息

登录服务器看/var/log/auth.log,找到了几行关键日志:

Unable to negotiate with 192.0.2.20 port 50231: no matching key exchange method found. Their offer: diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,rsa-sha2-256,rsa-sha2-512

这里的Their offer是客户端提出来的算法列表,但服务端已经没有交集了。再看服务端sshd_config,里面已经显式配置了只接受更强的算法。也就是说,服务器最近做了安全加固,把旧算法都禁掉了,而客户端还停留在非常老的版本,只能提出已经很弱甚至禁用的算法。

4.3 客户端低版本算法库不匹配的修复过程

这次修复没有去动服务端配置,而是从客户端入手。第一件事是升级 Java 项目里的 SSH 客户端依赖库到较新版本。升级前先用一个临时脚本打印客户端支持的算法列表,确认新版本里已经支持curve25519-sha256和rsa-sha2-512这些服务端接受的算法。升级后,同样的最小复现程序一次就连接成功。

这里我特意补充一点:升级依赖库之后不要只看日志“连接成功”,还要重新验证 SFTP 的上传、下载、删除、重命名这些核心操作,因为新版本库对某些 API 的默认行为可能有调整。比如有的版本对FilenameEncoding的默认值不同,可能导致中文文件名处理结果不一样。

4.4 事后加固:熔断、重试与告警

这次问题解决之后,还做了一些加固,避免下次再被类似问题折腾半天。连接工具类里增加了统一的异常分类方法:把JSchException的 message、cause、以及是否属于认证失败、算法协商失败、网络超时区分开,分别输出到不同日志级别,并记录到监控系统。

在重试策略上,我加了一个简单的指数退避重试:第一次失败等 2 秒,第二次失败等 4 秒,第三次失败等 8 秒,最多重试 3 次。但是对Auth fail这类认证失败,不做重试,因为账号密码错误重试多少次都一样,只会增加服务器封禁风险。对Host key verification failed同样不做重试,避免误报刷屏。

如果你也要写重试逻辑,建议参考下面这段伪代码:

int maxRetry = 3; int attempt = 0; long backoffMs = 2_000; Exception lastException = null; while (attempt < maxRetry) { try { SftpResult result = executeSftpTask(); return result; } catch (SftpAuthException e) { throw e; // 认证失败直接抛出 } catch (SftpNetworkException e) { attempt++; Thread.sleep(backoffMs * attempt); } }

另外,告警不能只监控“抛出异常”这个级别。我一般还会监控 SSH 连接耗时,如果连接耗时突然从几百毫秒涨到十几秒,即使最终成功,也说明网络或者服务端已经不正常了,可以提前介入。

5. 常见问题速查与三个易漏点

5.1 createCommunicationsException 常见场景速查表

我把实际工作中遇到过的问题整理成了一张速查表,遇到问题时可以直接对着查。

场景典型日志特征解决方案
服务器 22 端口不通Connection refused/Connection timed out检查服务、端口、防火墙、安全策略
云平台安全策略未放行TCP 探测超时调整入站规则,只放行需要访问的 IP
服务端禁用密码登录Auth fail/Permission denied改用公钥登录,或临时开启密码认证做验证
客户端私钥格式过老invalid privatekey将私钥转为 PEM 格式
算法版本不匹配Algorithm negotiation fail升级 SSH 客户端库,服务端做临时兼容需谨慎
主机密钥变更Host key verification failed重新获取服务器指纹并更新 known_hosts
连接空闲被服务端断开Connection reset/session down开启ServerAliveInterval保活
DNS 解析失败UnknownHostException检查域名解析配置和 hosts 文件
服务端来源 IP 限制TCP 通但 SSH 握手被断开检查AllowUsers、DenyUsers、防火墙规则

这张表只能帮你快速定位方向,具体到某个线上环境,还是要结合服务端系统日志来看。

5.2 三个容易忽略的细节

第一个细节是Session不能被多个线程共享。很多人写一个连接管理类,整个应用共用一个Session,然后并发去开多个 Channel,结果偶尔报channel is not opened、session is down,或者莫名其妙的连接中断。SSHSession是有状态的,推荐每个工作线程一个Session,或者使用连接池按需创建。并发量太高时,必须限制并发连接数,否则服务端会因为MaxStartups限制而拒绝新连接。

第二个细节是服务端会话超时设置。OpenSSH 默认的LoginGraceTime是 120 秒,如果客户端在建立连接后迟迟没有完成认证,服务端会主动断开。反过来如果网络链路比较慢,客户端可以适当调大connect()超时时间。不过如果连接过程耗时太久,更应该关注的是网络质量,而不是盲调超时。

第三个细节是 SSH 客户端库的日志默认是关的。线上环境第一次出现 createCommunicationsException 时,很容易因为缺少客户端日志而只能靠猜。建议在测试环境把日志级别打开,验证通过后再关闭,或者使用动态开关,只对特定用户、特定请求开启。这样既能保留排障能力,又不会因为日志刷屏影响性能。

还有一个我认为非常重要的习惯:看到任何连接类异常,先把getCause()的异常类型记下来。java.net.ConnectException、java.net.SocketTimeoutException、java.net.NoRouteToHostException三条路径对应的处理方式完全不同。排查时顺着根因走,比在 SSH 配置里反复试错要靠谱得多。

我在实际排查中最大的体会是:createCommunicationsException 本身不是答案,它只是一个路标。真正要看的,是客户端日志和服务端日志交汇在一起后,那个断掉的 SSH 握手阶段。连接被拒、认证失败、算法不匹配、主机密钥变化,这四类问题闭上眼睛都能数出来,但每次定位还是要把根因链打开,不能省这一步。安全配置更别顺手关掉主机密钥校验,宁可第一次多花五分钟同步指纹,也不要留一个能被中间人利用的口子。下次线上再报这个异常,你就知道它不是在说“网络不通”一句废话,而是整个 SSH 连接流程里某一个环节没走通,需要你一层层去把它找出来。

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

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

立即咨询