1. 概述
在 Java 应用中启动一个 Socket 服务器时,java.net
API 要求我们指定一个用于监听的端口号。这个端口号是 TCP 层识别数据目标应用的关键。
✅ 显式指定端口号并不总是最优选择,因为该端口可能已被其他应用占用,这会导致我们的 Java 应用抛出 I/O 异常。
本文将介绍如何检查某个端口是否可用,以及如何自动获取一个可用端口。我们将分别探讨使用纯 Java 和 Spring 框架的方式,并介绍一些常见的嵌入式服务器(如 Tomcat 和 Jetty)是如何处理端口分配的。
2. 检查端口状态
2.1. 检查特定端口
我们可以使用 java.net.ServerSocket
类来尝试绑定一个指定端口。如果该端口已被占用,则会抛出 IOException
:
try (ServerSocket serverSocket = new ServerSocket(FREE_PORT_NUMBER)) {
assertThat(serverSocket).isNotNull();
assertThat(serverSocket.getLocalPort()).isEqualTo(FREE_PORT_NUMBER);
} catch (IOException e) {
fail("Port is not available");
}
如果尝试重复使用同一个端口(或该端口已被其他进程占用),构造函数会抛出异常:
try (ServerSocket serverSocket = new ServerSocket(FREE_PORT_NUMBER)) {
new ServerSocket(FREE_PORT_NUMBER);
fail("Same port cannot be used twice");
} catch (IOException e) {
assertThat(e).hasMessageContaining("Address already in use");
}
2.2. 检查端口范围
我们可以通过遍历一个端口范围,捕获 IOException
,来找到第一个可用的端口:
for (int port : FREE_PORT_RANGE) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
assertThat(serverSocket).isNotNull();
assertThat(serverSocket.getLocalPort()).isEqualTo(port);
return;
} catch (IOException e) {
assertThat(e).hasMessageContaining("Address already in use");
}
}
fail("No free port in the range found");
3. 自动获取可用端口
显式指定端口号不总是靠谱,特别是在测试或动态部署场景中。我们可以通过一些方式让系统自动为我们分配一个可用端口。
3.1. 使用纯 Java
在 ServerSocket
构造函数中传入端口号 0
,Java 会自动为我们分配一个可用端口:
try (ServerSocket serverSocket = new ServerSocket(0)) {
assertThat(serverSocket).isNotNull();
assertThat(serverSocket.getLocalPort()).isGreaterThan(0);
} catch (IOException e) {
fail("Port is not available");
}
3.2. 使用 Spring Framework
Spring 提供了 SocketUtils
工具类,可以方便地获取一个可用的 TCP 端口:
int port = SocketUtils.findAvailableTcpPort();
try (ServerSocket serverSocket = new ServerSocket(port)) {
assertThat(serverSocket).isNotNull();
assertThat(serverSocket.getLocalPort()).isEqualTo(port);
} catch (IOException e) {
fail("Port is not available");
}
⚠️ 注意:Spring 的
SocketUtils
内部其实也是通过ServerSocket(0)
实现的。
4. 其他服务器实现
4.1. Jetty
Jetty 是一个非常流行的嵌入式 Java Web 服务器。如果不显式调用 setPort()
方法,它会自动分配一个可用端口:
Server jettyServer = new Server();
ServerConnector serverConnector = new ServerConnector(jettyServer);
jettyServer.addConnector(serverConnector);
try {
jettyServer.start();
assertThat(serverConnector.getLocalPort()).isGreaterThan(0);
} catch (Exception e) {
fail("Failed to start Jetty server");
} finally {
jettyServer.stop();
jettyServer.destroy();
}
4.2. Tomcat
Tomcat 是另一个常见的嵌入式服务器。与 Jetty 类似,可以通过 setPort(0)
来自动分配端口:
Tomcat tomcatServer = new Tomcat();
tomcatServer.setPort(0);
try {
tomcatServer.start();
assertThat(tomcatServer.getConnector().getLocalPort()).isGreaterThan(0);
} catch (LifecycleException e) {
fail("Failed to start Tomcat server");
} finally {
tomcatServer.stop();
tomcatServer.destroy();
}
⚠️ 注意:如果不调用
setPort()
,Tomcat 会默认使用 8080 端口,而这个端口很容易被占用。
5. 总结
在本文中我们介绍了以下几种方式来获取可用端口:
✅ 检查某个端口是否被占用
✅ 遍历端口范围寻找可用端口
✅ 使用 ServerSocket(0)
自动分配端口
✅ 利用 Spring 的 SocketUtils
工具类
✅ 嵌入式服务器(Jetty、Tomcat)的端口自动分配机制
这些方法在编写自动化测试、微服务动态部署、或本地调试时非常实用。
完整代码示例可从 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-networking-3