news 2026/9/17 5:17:03

Java Connection refused 排查:TCP握手与端口监听实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Connection refused 排查:TCP握手与端口监听实战

java.net.ConnectException 这个报错,几乎每个写过 Java 的人都撞上过。报错信息往往就一行java.net.ConnectException: Connection refused: connect,简洁得像什么都没说,但它背后可能是服务没起来、端口写错了、绑定地址不对、容器网络不通、依赖仓库连不上,甚至只是启动顺序差了几秒。我第一次遇到它是在一个 Spring Boot 项目里,本地跑得好好的,一上测试环境就炸,查了大半天才发现是配置里数据库地址写的是别人的机器。从那以后我就养成了一个习惯:看到 Connection refused,先别改代码,先把"谁在连、连哪里、谁在听"这三件事弄清楚。这篇内容就是把这些年积累的排查套路、命令、配置和踩过的坑整理出来,适合刚接触网络编程的新手,也适合已经能写业务但一遇到连接问题就头大的开发者,看完你至少能有一套可复用的定位流程,而不是靠猜。

1. 先搞清楚报错语义:Connection refused 到底是谁拒绝了谁

1.1 从 TCP 三次握手的角度看"拒绝"这个动作

很多人把 Connection refused 理解成"网络不通",这个理解是偏的。网络不通通常表现为连接超时(Connection timed out),而 refused 恰恰说明网络是通的,包也送到了,只是对面明确地回了一个拒绝信号。

用 TCP 的视角来看:客户端发起连接,先发一个 SYN 包到目标 IP 和端口。如果目标主机上那个端口没有任何进程在监听,操作系统内核会直接回一个 RST(重置)包。客户端收到 RST,就立刻抛出java.net.ConnectException: Connection refused。整个过程非常快,通常几毫秒就返回了,这也解释了一个经验:秒拒大概率是端口没监听,慢拒(几十秒后超时)才是网络或防火墙的问题

还有两种情况会产生类似但不同的结果。一种是防火墙把包直接丢弃(DROP)而不是拒绝,客户端迟迟收不到回应,最后超时;另一种是目标主机不可达,路由层返回 ICMP 消息,Java 层可能报 NoRouteToHostException。这三种错误的排查方向完全不同,所以第一步永远是先分清自己遇到的到底是哪一种。

注意:Connection refused: connect这个带connect后缀的写法,在 Windows 平台上的 JDK 里更常见;同样是连接被拒,Linux 上通常只显示Connection refused。别因为后缀不一样就以为是两个不同的问题。

1.2 三类连接异常的分界线,别混在一起查

我在带新人时发现,最容易浪费时间的就是把这几类错误混为一谈。下面这张表可以当作第一道分诊台:

异常类型典型信息底层原因排查方向
ConnectExceptionConnection refused端口无进程监听,内核回 RST服务是否启动、端口是否一致、绑定地址
SocketTimeoutExceptionconnect timed out包被丢弃或路由不通防火墙、安全组、路由、目标主机状态
UnknownHostException主机名无法解析DNS 或 hosts 配置问题域名拼写、DNS 服务、hosts 文件
NoRouteToHostException无路由到主机网络层不可达网卡、子网、路由表

判断顺序建议这样走:先看异常类名,再看到底是"立刻失败"还是"等了一会儿才失败"。如果日志时间戳显示请求发出到报错不足 100ms,几乎可以锁定是端口问题;如果等了 20 秒以上,那就要往防火墙和网络策略方向查。

1.3 从堆栈里读出有效信息,别只盯着最后一行

java.net.ConnectException的堆栈里,最有价值的其实是at那几行。它们能告诉你到底是谁发起的连接。常见的发起方有几类:数据库驱动(比如 MySQL Connector 的ConnectionImpl.createNewIO)、HTTP 客户端(HttpURLConnection或 OkHttp、Apache HttpClient)、消息队列客户端(Kafka、RabbitMQ)、缓存客户端(Jedis、Lettuce),还有构建工具自己的下载器。

举个例子,如果堆栈里出现com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException,那基本就是数据库连接问题,去查 JDBC URL 的 host 和 port 就行;如果出现org.apache.maven.wagon之类的类名,那说明是构建期下载依赖被拒,跟业务代码一点关系都没有。先看调用方是谁,再去查那个调用方配置的连接串,这一步能省掉大量无头苍蝇式的搜索。

2. 排查框架:三个问题锁定九成故障

2.1 谁在连、连哪里、谁在听

我把这套方法叫"三问定位法",简单但极其好用。

第一问,谁在连:是本地开发机上的应用,还是容器里的服务,还是远程服务器上的进程?发起方不同,网络路径完全不同,排查手段也不同。

第二问,连哪里:目标地址和端口具体是多少?这个信息一定要从实际运行时读取到的配置里拿,而不是从你记忆中的配置里拿。配置文件可能有多份(dev、test、prod),环境变量、启动参数、配置中心都可能覆盖它。我见过太多次"我明明改了啊",结果改的是没生效的那份。

第三问,谁在听:目标机器上那个端口到底有没有进程在监听?监听在哪个网卡上?只有 127.0.0.1 还是 0.0.0.0?这一步用一条命令就能验证。

这三问回答完,问题范围基本就缩小到一两个可能性了。剩下的只是验证。

2.2 五分钟快速排查清单

下面这套动作我基本是肌肉记忆了,全程不超过五分钟。

# 1. 看本机端口监听情况(Linux/macOS) ss -lntp | grep 8080 # 或者用 netstat netstat -lntp | grep 8080 # 2. Windows 下查看端口占用 netstat -ano | findstr 8080 # 3. 直接测试目标端口是否可连 nc -vz 127.0.0.1 8080 telnet 127.0.0.1 8080 # 4. 如果是 HTTP 服务,直接请求一下 curl -v http://127.0.0.1:8080/actuator/health

ss -lntp输出里的几个字段值得说清楚。-l表示只看监听状态,-n表示不做名称解析(显示数字端口),-t是 TCP,-p显示进程。看输出时重点确认三件事:端口号是否一致、监听地址是127.0.0.1还是0.0.0.0、进程名是不是你以为的那个。如果连一行输出都没有,那答案已经很明确了:没人监听,问题在服务端启动环节。

2.3 三个命令的分工与使用时机

ss用来查本机,适合排查"服务到底起没起来"。它的输出最直接,0.0.0.0:8080表示监听所有网卡,127.0.0.1:8080表示只监听回环地址,这两者的区别在跨机器访问时是致命的。

nctelnet用来做连通性验证,适合排查"从我这台机器能不能连到那台机器"。它们不关心应用协议,只做 TCP 握手,返回成功就说明端口是开的。telnet在有些精简系统上没装,nc也不一定有,那就退回到curl -v,从它的Connected to一行判断。

curl用来验证应用层,适合排查"端口通了但接口是不是正常"。这三个工具是递进关系,先用地板级的 TCP 握手确认,再往上走应用层,逐层收敛。

3. 高频场景拆解:不同上下文里的拒绝各有各的脾气

3.1 目标服务还没启动,或者启动到一半

这是最朴素也最常见的原因。微服务架构下,服务 A 启动时要调服务 B,如果 B 还在初始化,A 发出的连接自然被拒。这类问题的典型特征是:日志里第一波请求报 refused,过几十秒后自己就好了,或者重启一次就好了。

解决思路有三条。一是给调用加合理的重试和退避,第一次失败后等 1 秒、2 秒、4 秒再试,别一上来就放弃。二是配置连接超时别太长,连接超时设 2 秒左右就够,设成 30 秒反而会让问题暴露得很慢。三是梳理启动依赖,让下游先起。我在实际项目里一般用配置化的重试,示例如下:

@Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); // 连接超时 2 秒,读超时 5 秒 factory.setConnectTimeout(2000); factory.setReadTimeout(5000); return new RestTemplate(factory); }

注意:重试只能用在对幂等友好的操作上。HTTP GET 重试一般安全,涉及到写数据、扣款、下单这类操作,重试前一定要确认接口是不是幂等的,否则会把"连不上"变成"重复扣款"这种更麻烦的事。

3.2 端口对不上:配置漂移与硬编码的老毛病

端口不一致是第二大高频原因。常见形态包括:配置文件里写 8080,实际启动参数--server.port=9090覆盖了;Nginx 转发配置里指向 8081,后端服务起的却是 8080;容器映射的是宿主机 8082 到容器 8080,但客户端连的是 8081。这类问题最坑的地方在于,每一处配置单看都没问题,问题是它们没对齐。

我的经验是:连接地址只能有一个"事实来源"。要么全部走配置中心,要么全部走环境变量,别一半写在代码里、一半写在配置文件里。排查的时候,把运行时真正生效的值打印出来,比如在 Spring Boot 启动时把数据源 URL 打日志,或者通过 actuator 的/actuator/env端点查看。看到实际值,问题往往当场就明白了。

3.3 绑定地址的坑:127.0.0.1 与 0.0.0.0 的区别

这个坑我在容器化项目里见过太多次。服务在自己容器里监听127.0.0.1:8080,同一个容器里访问没问题,但一旦别的容器或宿主机来连,就会立刻被拒。原因是127.0.0.1只绑在回环网卡上,外部流量根本不进入这个网卡。

正确的做法是让服务监听0.0.0.0,也就是所有网卡。Spring Boot 里配置server.address=0.0.0.0,或者干脆不配这个属性,让默认值生效。Tomcat 的Connector默认绑的就是所有地址,但如果有人手动改过address属性,就要留个心眼。

同理,MySQL 的bind-address、Redis 的bind配置项都有这个问题。默认情况下它们往往只监听本机,一旦从另一台机器连过去,看到的症状就是 Connection refused,而不是用户名密码错误。排查时用ss -lntp看一眼监听地址,比翻配置文件快得多。

3.4 容器与虚拟网络:localhost 的语义差异

容器场景下,localhost的含义和在宿主机上完全不同。在容器内,localhost指容器自己,不是宿主机。如果应用配置里写的是localhost:3306,而这个 MySQL 跑在宿主机或另一个容器里,那必然会拒绝连接。

Docker Compose 场景下,正确写法是用服务名当主机名,比如jdbc:mysql://mysql-db:3306/demo,其中mysql-db是 compose 文件里定义的服务名,Docker 的内嵌 DNS 会把它解析到对应容器的 IP。如果用的是宿主机上的服务,Linux 下可以用host.docker.internal这个特殊域名(需要额外配置或较新版本支持),或者直接写宿主机的局域网 IP。

这里还有一个经常被忽略的点:容器启动顺序。Compose 的depends_on只保证启动顺序,不保证依赖服务已经就绪。MySQL 容器启动了但还没初始化完,此时应用去连,一样会被拒。稳妥的做法是加健康检查,让应用在依赖真正健康后再启动。

3.5 依赖仓库与插件下载被拒:构建期的拒绝

有一种 Connection refused 跟业务代码毫无关系,出现在构建阶段。典型场景是 Maven 拉依赖、Gradle 同步、IDE 初始化项目时,报错里带着一堆 URL,还提示"请检查 url、网络和代理设置"或者初始化失败。这种时候不要去看业务代码,问题在下载链路。

常见原因有三类。一是仓库地址不可达,或者配置的私服挂了;二是公司内网需要走 HTTP 代理才能出网,但构建工具或 IDE 没配代理;三是配置了错误的代理,导致请求全被发到一个不存在的地方。我遇到过最隐蔽的一次,是开发机上设置了系统级代理,但那个代理服务已经卸载了,所有请求都打到本地的代理端口上,结果自然是满屏 refused。

Maven 的settings.xml是排查重点,配置镜像时建议使用稳定可用的公共镜像:

<mirror> <id>aliyun-public</id> <mirrorOf>central</mirrorOf> <name>Aliyun Public Repository</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>

注意:如果你在公司内网,镜像地址和代理配置一定要找运维确认,别照搬网上博客。改完settings.xml后建议执行mvn -U clean package强制更新快照,同时用mvn help:effective-settings确认最终生效的配置,避免"改了没生效"的经典问题。

Gradle 的话,检查gradle.properties里的代理相关属性和init.gradle中的仓库配置,IDE 里还有一层独立的代理设置,这三个地方要保证一致,否则很容易出现命令行能拉、IDE 拉不了的情况。

3.6 中间件连接:MySQL、Redis、Kafka 的典型拒绝

数据库场景下,Connection refused 和 Access denied 是两码事。前者是连不上,后者是账号密码不对。看到 refused,说明网络层就没通,别去折腾密码了。

MySQL 的常见原因包括:服务没启动(systemctl status mysqld看一眼)、端口不是 3306、bind-address只绑了本机、防火墙拦了 3306。Redis 类似,默认bind 127.0.0.1且开启保护模式,从别的机器连就是拒绝。Kafka 的坑更绕,它涉及listenersadvertised.listeners两个配置:客户端先连 bootstrap server 拿到元数据,元数据里返回的是advertised.listeners里配置的地址,如果那里写的是内网主机名而客户端解析不了或者连不上,就会报 refused。这种时候看客户端日志会发现,第一次连接是成功的,后面才失败。

排查中间件我一般这样走:先在目标机上ss -lntp确认端口在听,再从客户端机器nc -vz测连通,最后才用客户端工具连一次。三步都过,问题多半在配置的地址上,而不是网络。

4. 一次完整的实战定位记录

4.1 现场:服务启动正常,调用下游接口就报 refused

前阵子碰到一个案例,记录一下完整过程,比较有代表性。情况是这样:一个 Spring Boot 服务,本地开发一切正常,部署到测试环境后,调用下游的用户中心接口一直失败,日志里是java.net.ConnectException: Connection refused。后端同学第一反应是下游挂了,但下游服务的负责人说他们跑得好好的,访问日志里根本没有收到这个请求。

这就是典型的"还没到应用层就被拒了",说明连接在 TCP 层就没建立起来。判断依据很直接:如果是应用层报错,下游的访问日志里会有记录,哪怕返回 500;现在下游日志干干净净,说明包根本没被应用接收。

4.2 逐层验证:从本机到目标一步步收紧

第一步,在报错的应用机器上确认配置的目标地址。通过环境变量和配置中心查了一遍,实际生效的是user-center.internal:8080。注意这里是个内部域名,不是 IP。

第二步,测域名解析。用nslookup或者getent hosts user-center.internal查一下,解析出来是一个内网 IP,这一步正常。

第三步,测端口连通性。nc -vz user-center.internal 8080,结果是Connection refused。到这一步确认了,域名没问题,就是目标端口拒绝连接。

第四步,去目标机器上查监听。ss -lntp | grep 8080显示,服务确实在跑,但监听地址是127.0.0.1:8080,而不是0.0.0.0:8080。问题找到了:下游服务在一个容器里,配置文件里显式绑定了回环地址,导致只有容器内部能访问,跨容器就是拒绝。

4.3 修复与回归验证

修复方案很简单,把下游服务的监听地址改成0.0.0.0,重新发布。发布后再从调用方机器执行nc -vz user-center.internal 8080,这次返回succeeded。然后重新触发一次业务调用,接口正常返回,问题解决。

整个过程从开始定位到修复大概二十分钟,其中真正花时间的是"确认下游服务到底监听在哪"这一步,因为一开始大家都盯着调用方看,没人去目标机器上确认。这也是我想强调的点:Connection refused 的答案大概率在目标端,不在发起端。发起端只能告诉你"我被拒了",目标端才能告诉你"为什么拒"。

4.4 把这次经验固化下来

这个案例之后,我在团队里推了两个小改动。一是在所有服务的启动脚本里加了一行日志,把实际监听地址和端口打印出来,方便一眼确认。二是在部署流水线里加了个健康检查步骤,发布后自动从另一个容器去连目标端口,连不通就直接判定发布失败,别等到业务报错才发现。

健康检查这段可以用很朴素的脚本实现:

#!/bin/bash # 发布后连通性自检 HOST=$1 PORT=$2 RETRY=10 for i in $(seq 1 $RETRY); do if nc -z "$HOST" "$PORT"; then echo "port $PORT is reachable" exit 0 fi echo "waiting for $HOST:$PORT ... ($i/$RETRY)" sleep 2 done echo "port $PORT is NOT reachable" exit 1

这个脚本看着简单,但在滚动发布、容器重启的场景里能拦掉不少低级问题,尤其是那种"服务还没起来就被调用"的时序问题。

5. 常见问题速查与避坑经验

5.1 一张表覆盖八成场景

现象高概率原因验证命令处理方式
本地调用本地服务被拒服务未启动或端口写错ss -lntp | grep 端口启动服务、核对端口
跨机器访问被拒监听在 127.0.0.1ss -lntp看地址改绑 0.0.0.0
容器内访问宿主机被拒localhost 语义错误ip addr查网关用服务名或宿主机 IP
构建时下载依赖被拒仓库地址或代理配置错误mvn -X看请求地址修 settings.xml 与代理
连接数据库被拒端口不通或 bind-address 限制nc -vz db 3306改配置、开防火墙
Kafka 首次能连后续被拒advertised.listeners 配错查 broker 配置改成客户端可达地址
偶发被拒,重试就好下游启动慢或滚动发布看报错时间分布加重试 + 健康检查

5.2 我踩过的几个坑

第一个坑是只改配置不重启。有些配置是启动时读取的,运行期改了文件不生效。我一度以为是网络问题,折腾半天,最后重启一下就好了。判断方法很简单,看修改时间和你上次重启时间,如果配置比进程新,那就该重启了。

第二个坑是把超时当成拒绝。有一回日志里写的是 refused,但实际排查发现是防火墙做的静默丢弃,只是客户端框架把超时异常包装成了 ConnectException 的子类,误导了方向。后来我养成习惯,一定要看日志的时间戳,从请求发起到报错隔了多久,这个信息比异常类名还可靠。

第三个坑是代理设置残留。开发机上配过代理,工具卸载了但环境变量还在,导致所有请求都往一个不存在的地址发。排查方式就是检查http_proxyhttps_proxyHTTP_PROXY这些环境变量,以及 IDE、构建工具各自独立的代理配置。这类问题最隐蔽,因为命令行和 IDE 的表现可能不一样。

第四个坑是IPv6 优先。有些环境里localhost优先解析成::1,而服务只监听了 IPv4 的127.0.0.1,于是连接被拒。解决办法要么把服务改成同时监听,要么在客户端直接用127.0.0.1而不是localhost。这种问题在 macOS 上尤其常见,因为它的解析顺序和 Linux 有差异。

5.3 几条预防性的配置建议

与其每次出问题临时查,不如提前把该做的做了。连接超时统一设成 3 到 5 秒,别用默认的无限等待;关键的下游调用加上带退避的重试;服务启动日志里打印实际监听地址和关键连接串;健康检查走真实的 TCP 探测而不是简单的进程存活判断;环境变量和配置文件之间确定一个优先级规则并写进文档。

还有一点值得单独提:日志里把目标地址打出来。很多框架的默认日志只写"连接失败",不写连的是谁。在客户端初始化的时候加一行 info 日志,把 host 和 port 打出来,故障时能省下大量时间。这个改动成本极低,收益极高。

我个人在这些年处理这类问题的体会是,Connection refused 本身其实是个很"诚实"的错误,它几乎从不骗人,说端口不通就是端口不通。真正难的是找到那个生效的配置在哪儿,以及确认目标端到底在听什么。把"三问定位法"和那几条命令变成习惯,大部分同类问题都能在十分钟内定位到方向。最后再分享一个小技巧,如果机器上没装 nc 也没装 telnet,可以用 bash 自带的能力测端口,省得临时装工具:

# 不依赖 nc/telnet 的端口探测 timeout 2 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/8080' && echo "open" || echo "closed"

这个写法在大多数 Linux 和 macOS 的 bash 环境里都能用,出问题时手边有台机器就能立刻验证,比翻工具箱方便得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 5:17:02

CentOS 7 Failed to mount /sysroot 排查修复

1. 一行报错背后的启动链条&#xff0c;先搞清楚 /sysroot 到底是谁很多人第一次看到Failed to mount /sysroot的时候&#xff0c;第一反应是"我的硬盘挂了"或者"根分区被我删了"。说实话&#xff0c;我第一次遇到是在一台跑了两年的老服务器上&#xff0c…

作者头像 李华
网站建设 2026/9/17 5:13:20

Android账本APP开发:从Room数据库到APK构建全流程

简介&#xff1a;这是一份面向高校计算机类专业学生与Android初学者的移动开发实战项目资源&#xff0c;适用于课程设计、期末大作业、毕设选题及技能进阶训练。项目实现了一个功能完整的个人账本APP&#xff0c;包含收支记录、分类统计、数据持久化等核心模块&#xff0c;配套…

作者头像 李华
网站建设 2026/9/17 5:13:15

vLLM-Omni 架构全景:面向全模态模型的分阶段推理与服务体系

vLLM-Omni 架构全景&#xff1a;面向全模态模型的分阶段推理与服务体系 【免费下载链接】vllm-omni A framework for efficient model inference with omni-modality models 项目地址: https://gitcode.com/GitHub_Trending/vl/vllm-omni 本文以 vLLM-Omni 的架构总览文…

作者头像 李华
网站建设 2026/9/17 5:13:14

MySQL基本查询全解析:SELECT、JOIN与WHERE的实战避坑指南

数据库这行干久了&#xff0c;你会发现在业务代码里写 SQL 的时间&#xff0c;往往比写 Java、Python 还要多。尤其是“基本查询”这四个字&#xff0c;看着简单&#xff0c;真到面试、上线、排查线上问题的时候&#xff0c;多少人栽在它上面。我见过写了好几年代码的开发&…

作者头像 李华
网站建设 2026/9/17 5:12:49

戴尔准系统爆改低功耗NAS:90元老机器的工业级重生

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 5:10:01

UniApp集成Towxml实现Markdown渲染与分包优化

1. 项目背景与需求分析在小程序开发中&#xff0c;Markdown内容的展示一直是个痛点。传统的文本展示方式无法完美呈现代码块、数学公式、表格等结构化内容。Towxml作为一款专为微信小程序设计的渲染引擎&#xff0c;能够将Markdown/HTML转换为小程序原生组件&#xff0c;支持丰…

作者头像 李华