技术派项目源码地址 :
- Gitee :技术派 - https://gitee.com/itwanger/paicoding
- Github :技术派 - https://github.com/itwanger/paicoding
在开发和部署应用程序时,端口号冲突是一个常见但令人头疼的问题。多个服务或应用试图使用相同的端口号会导致冲突,进而影响系统的正常运行。为了避免这种情况,开发者需要巧妙地管理和分配端口号。常见的方法包括使用动态分配端口、配置文件中明确指定端口号范围,以及通过脚本或工具检测和避免冲突。此外,借助负载均衡器或反向代理,还可以进一步优化端口管理,确保每个服务能够稳定运行。通过这些策略,开发者可以有效避免端口冲突,保证系统的可靠性和可用性。
- 当本地的8080端口号被占用时,会重新选择一个端口号进行启动,
- 而不是直接抛端口号被占用、启动失败的异常
端口启动冲突
- 一般来说,我们会指定每个项目启动的web端口,当不指定时,
- 默认是8080,当本地有多个项目需要启动时,那么端口冲突的问题,就非常突出了
- 技术派项目启动时,表现如下 :
解决方案
思路 :
想要解决端口冲突的问题,思路好像非常明确
- 首先启动前,判断端口号是否被占用
- 若未被占用,则直接使用这个端口启动
- 若被占用,则随机选择一个端口号,重走步骤1
我们这里的关键点在于判断一个端口号是否可用,可选方案就是基于这个端口号尝试建立一个socket,
当成功建立并可用,那么表示这个端口号没有被别人占用,可以使用它
public static boolean checkIpPort(String ip, int port) {
Socket socket = new Socket();
try {
socket.connect(new InetSocketAddress(ip,port),3000);
logger.info("地址和端口号可用");
return true;
} catch (Exception e) {
logger.info("地址和端口号不可用");
return false;
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
}
}
}
}
- 但是再我们这个场景中,并不是一个写死的配置就可以支持的,
- 需要再代码中,首先找到一个可用的端口号,然后再启用它
- 改用配置覆盖,比如服务启动之时,就找出可用的端口,然后这个端口覆盖配置文件中的值
public static void main(String[] args) {
List<String> list = new ArrayList<>(args.length + 1);
Collections.addAll(list, args);
// 手动加一个指定web端口为8082的命令行参数
list.add("--server.port=8082");
args = list.toArray(new String[]{});
SpringApplication.run(Application.class, args);
}
- 上面这种其实就相当于借助命令行参数的最高优先级,覆盖配置文件中的同名配置
- 关于配置的修改, 还可以通过重写PropertySourcesPlaceholderConfigurer来修改配置
具体实现方案
- 配置端口号
server:
port: 8080
- 端口号选择 :
package com.github.paicoding.forum.core.util;
import javax.net.ServerSocketFactory;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.Random;
/**
* @author YiHui
* @date 2022/11/26
*/
public class SocketUtil {
/**
* 判断端口是否可用
*
* @param port
* @return
*/
public static boolean isPortAvailable(int port) {
try {
ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(port, 1);
serverSocket.close();
return true;
} catch (Exception var3) {
return false;
}
}
private static Random random = new Random();
private static int findRandomPort(int minPort, int maxPort) {
int portRange = maxPort - minPort;
return minPort + random.nextInt(portRange + 1);
}
/**
* 找一个可用的端口号
*
* @param minPort
* @param maxPort
* @param defaultPort
* @return
*/
public static int findAvailableTcpPort(int minPort, int maxPort, int defaultPort) {
if (isPortAvailable(defaultPort)) {
return defaultPort;
}
if (maxPort <= minPort) {
throw new IllegalArgumentException("maxPort should bigger than miPort!");
}
int portRange = maxPort - minPort;
int searchCounter = 0;
while (searchCounter <= portRange) {
int candidatePort = findRandomPort(minPort, maxPort);
++searchCounter;
if (isPortAvailable(candidatePort)) {
return candidatePort;
}
}
throw new IllegalStateException(String.format("Could not find an available %s port in the range [%d, %d] after %d attempts", SocketUtil.class.getName(), minPort, maxPort, searchCounter));
}
}
- 端口号动态配置 :
- 我们这里做了一个兼容,只有dev启动时,才需要选择可用端口(基于SpEL来实现)
- 对于部署到测试、预发、生产环境的时候,就是用默认的端口号就行了
@Value("${server.port:8080}")
private Integer webPort;
/**
* 兼容本地启动时8080端口被占用的场景; 只有dev启动方式才做这个逻辑
* @return
*/
@Bean
@ConditionalOnExpression(value = "#{'dev'.equals(environment.getProperty('env.name'))}")
public TomcatConnectorCustomizer customServerPortTomcatConnectorCustomizer() {
// 开发环境时,首先判断8080d端口是否可用;若可用则直接使用,否则选择一个可用的端口号启动
int port = SocketUtil.findAvailableTcpPort(8000, 10000, webPort);
if (port != webPort) {
log.info("默认端口号{}被占用,随机启用新端口号: {}", webPort, port);
webPort = port;
}
return connector -> connector.setPort(port);
}
测试
- 找不到占用8080的应用, 我们直接把端口改成6379, 然后本地启动redis测试
@Value("${server.port:6379}")
private Integer webPort;
server:
port: 6379
- 启动后结果如下
- 不在我们规定的6379端口启动, 而是自动随机一个新端口启动
小结
- 看完上面的内容,会发现自主选择一个可用的端口号整体来说比较简单,
- 只要我们对一个任务做好了拆解,然后逐步给出解决方案,其实落地并不会有太大的难度