别让端口打架!巧妙化解端口号冲突

技术派项目源码地址 :


在开发和部署应用程序时,端口号冲突是一个常见但令人头疼的问题。多个服务或应用试图使用相同的端口号会导致冲突,进而影响系统的正常运行。为了避免这种情况,开发者需要巧妙地管理和分配端口号。常见的方法包括使用动态分配端口、配置文件中明确指定端口号范围,以及通过脚本或工具检测和避免冲突。此外,借助负载均衡器或反向代理,还可以进一步优化端口管理,确保每个服务能够稳定运行。通过这些策略,开发者可以有效避免端口冲突,保证系统的可靠性和可用性。


  • 当本地的8080端口号被占用时,会重新选择一个端口号进行启动,
  • 而不是直接抛端口号被占用、启动失败的异常

端口启动冲突

  • 一般来说,我们会指定每个项目启动的web端口,当不指定时,
  • 默认是8080,当本地有多个项目需要启动时,那么端口冲突的问题,就非常突出了

image.png

  • 技术派项目启动时,表现如下 :

image.png
image.png


解决方案

思路 :

想要解决端口冲突的问题,思路好像非常明确

  • 首先启动前,判断端口号是否被占用
  • 若未被占用,则直接使用这个端口启动
  • 若被占用,则随机选择一个端口号,重走步骤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测试

image.png


@Value("${server.port:6379}")
private Integer webPort;
server:
  port: 6379
  • 启动后结果如下
  • 不在我们规定的6379端口启动, 而是自动随机一个新端口启动

image.png


小结

  • 看完上面的内容,会发现自主选择一个可用的端口号整体来说比较简单,
  • 只要我们对一个任务做好了拆解,然后逐步给出解决方案,其实落地并不会有太大的难度
  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值