在代码层面解决Springboot+Netty+Tomcat热部署端口占用问题

问题描述:在Tomcat中热部署Springboot应用时,报如下错误:

java.net.BindException: Address already in use  
    at sun.nio.ch.Net.bind0(Native Method)  
    at sun.nio.ch.Net.bind(Net.java:444)  
    at sun.nio.ch.Net.bind(Net.java:436)  
    at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:214)  
    at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:74)  

问题原因:应用销毁时,未关闭netty server和释放端口。

现有解决办法:

在网上百度来的全是通过kill -9 干掉进程的方式,这个方法过于简单粗暴,且大大增加了运维成本。

做为一个优秀的码农+运维(自加鸡腿一根:)怎么能允许这种事情发生呢?

让我们来优雅的解决这个问题。↓

解决思路:监控springboot容器销毁事件,在此事件中先关闭netty。然后再加载时就不会有端口冲突了。

首先,将ServerChannel和EventLoopGroup定义为全局静态变量,在接收到关闭事件时,关闭channel,EventLoopGroup即可。

话不多说,直接上代码:

一,NettyServer:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NettyServerMain {
	private static EventLoopGroup bossGroup = null;
	private static EventLoopGroup workGroup = null;
	private static Channel serverChannel = null;

	public static void main(String[] args) {
		// write your code here
	}

	public void bind(String host, int port) throws Exception {
		log.info("Netty local host = " + host + " and port = " + port);
		bossGroup = new NioEventLoopGroup();
		workGroup = new NioEventLoopGroup();

		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workGroup);
			b.channel(NioServerSocketChannel.class);
			b.option(ChannelOption.SO_BACKLOG, 1024);
			b.option(ChannelOption.SO_REUSEADDR, true);
//			b.option(ChannelOption.SO_KEEPALIVE, true);
			b.childHandler(new ChildChannelHandler());

			// 绑定端口
			ChannelFuture channelFuture = b.bind(host, port).sync();
			// 将ServerChannel保存下来
			serverChannel = channelFuture.channel();
			// 等待服务端监听端口关闭
			channelFuture.channel().closeFuture().sync();

		} catch (Exception e) {
			log.error("Netty bind error");
			e.printStackTrace();
		} finally {
			// 优雅的退出
			bossGroup.shutdownGracefully();
			workGroup.shutdownGracefully();
		}

	}

	/**
	 * 关闭netty server
	 */
	public void close() {
		if (serverChannel != null) {
			try {
				serverChannel.close();
				serverChannel = null;
				bossGroup.shutdownGracefully();
				workGroup.shutdownGracefully();
			} catch (Exception e) {
				log.error("Close netty server failed.");
				e.printStackTrace();
			}
		}
	}
}

二,SpringBoot监听:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class MessageReceiver implements ApplicationListener<ContextRefreshedEvent> {

	private ExecutorService webSocketSinglePool;

	private volatile AtomicBoolean isInit = new AtomicBoolean(false);

	private NettyServerMain nettyServerMain = null;

	@PostConstruct
	public void setup() {
		ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("webSocketSinglePool-%d").build();
		webSocketSinglePool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), namedThreadFactory,
				new ThreadPoolExecutor.AbortPolicy());
		log.info("webSocketSinglePool init.");
	}

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		log.info("=================onApplicationEvent: ContextRefreshedEvent 进入");
		// 防止重复触发
		if (!isInit.compareAndSet(false, true)) {
			log.info("=================onApplicationEvent: ContextRefreshedEvent 是再次触发,忽略");
			return;
		}
		if (event.getApplicationContext().getParent() == null) {
			runNettyServer();
		}
	}

	private void runNettyServer() {
		log.info("=================runNettyServer ");
		webSocketSinglePool.execute(() -> {
			try {
				runNetty();
			} catch (Exception e) {
				log.error("NettyServer listen and serve error.", e);
			}
		});
	}

	/**
	 * 关闭Netty Server
	 */
	private void destoryNettyServer() {
		if (nettyServerMain != null) {
			nettyServerMain.close();
			nettyServerMain = null;
			log.info("netty destroyed.");
		}
	}

	@PreDestroy
	public void cleanup() {
		destoryNettyServer();
		webSocketSinglePool.shutdown();
		log.info("webSocketSinglePool destroyed.");
	}

	private void runNetty() {
		try {
			log.info("=================start to get Netty local host  and port!");
			nettyServerMain = new NettyServerMain();
			nettyServerMain.bind("127.0.0.1", 8765);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

补充:

遗留问题,在热部署完成之后,虽然不再报端口占用的问题,但在Tomcat log中会看到有几个EventLoopGroup线程未关闭,会导致内存泄漏的问题。有没有这方面的大神,来一起解决一下 :)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值