精华内容
下载资源
问答
  • 本篇文章简单介绍了在业务逻辑中处理断线重连的一种方法 之前一直对如何在业务逻辑中处理断线重连没有一个清晰的认识,后来做了一些思考,这里简单记录一下~ 假设存在一段业务逻辑 AAA ,整体实现上分为两部分: ...

    本篇文章简单介绍了在业务逻辑中处理断线重连的一种方法

    之前一直对如何在业务逻辑中处理断线重连没有一个清晰的认识,后来做了一些思考,这里简单记录一下~

    假设存在一段业务逻辑 AA ,整体实现上分为两部分:

    • 服务器逻辑部分 ASA_S
    • 客户端逻辑部分 ACA_C

    一般来讲都是 ASA_S 负责维护逻辑状态与事件分发,ACA_C 则主要负责显示,输入等表现层的处理.

    假设 ACA_C 不存在状态存储,仅作为纯终端显示的话,那么我们就不用处理断线重连的问题了,因为 ACA_C 的显示(由 ASA_S 驱动)总是与 ASA_S 同步的.

    不过在现实的开发中并没有这么理想化, ACA_C 或多或少总会在本地存储一些状态,于是 ACA_CASA_S 便产生了状态同步问题,如果网络条件良好,逻辑上也没有纰漏的话, ACA_CASA_S 间的状态同步其实也不会存在什么问题.

    只是一旦引入断线重连,状态同步问题就出现了,因为在 ACA_C 断线然后进行重连的这段时间中, ASA_S 发生的状态变化将无法同步至 ACA_C, 甚至 ACA_C 重连成功之后, ASA_S 本身都可能因为处理完毕而结束了自己的逻辑过程.

    那么如何正确的处理这种情况下的断线重连呢?

    以下是我的一点思考:

    • ASA_SACA_C 都监听并处理 on_relay_successon\_relay\_success 事件
    • ACA_Con_relay_successon\_relay\_success 事件中将本地所有相关的逻辑状态清空
    • ASA_Son_relay_successon\_relay\_success 事件中将 ACA_C 所需要的逻辑状态做一次全量同步(需要保证 ASA_Son_relay_successon\_relay\_success 事件发生在 ACA_Con_relay_successon\_relay\_success 事件之后)

    除了逻辑状态以外, ASA_SACA_C 之间可能还会进行事件通知,推荐规避这些事件通知,都改以状态(的变化)实现.

    采用上述方案之后, ACA_C 就能在重连成功之后,获得最新的 ASA_S 状态,于是便能与 ASA_S 再次形成同步;即便此时 ASA_S 逻辑已经退出,不再能推送当前状态信息,也因为 ACA_Con_relay_successon\_relay\_success 之后主动做了一次状态清除操作,所以状态上也是同步的(ASA_S 退出便意味着 ACA_C 状态需要清除).

    展开全文
  • Netty断线重连

    2019-07-18 17:24:44
    连接的操作是客户端这边执行的,重连逻辑也得加在客户端,首先我们来看启动时要是连接不上怎么去重试 增加一个负责重试逻辑的监听器,代码如下: import java.util.concurrent.TimeUnit; import ...

    转载自易吉欢的博客

    在Netty中实现重连的操作比较简单,Netty已经封装好了,我们只需要稍微扩展一下即可。

    启动时连接失败

    连接的操作是客户端这边执行的,重连的逻辑也得加在客户端,首先我们来看启动时要是连接不上怎么去重试

    增加一个负责重试逻辑的监听器,代码如下:

    import java.util.concurrent.TimeUnit;
    
    import com.netty.im.client.ImClientApp;
    
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelFutureListener;
    import io.netty.channel.EventLoop;
    /**
     * 负责监听启动时连接失败,重新连接功能
     * @author yinjihuan
     *
     */
    public class ConnectionListener implements ChannelFutureListener {
    	
    	private ImConnection imConnection = new ImConnection();
    	
    	@Override
    	public void operationComplete(ChannelFuture channelFuture) throws Exception {
    		if (!channelFuture.isSuccess()) {
    			final EventLoop loop = channelFuture.channel().eventLoop();
    			loop.schedule(new Runnable() {
    				@Override
    				public void run() {
    					System.err.println("服务端链接不上,开始重连操作...");
    					imConnection.connect(ImClientApp.HOST, ImClientApp.PORT);
    				}
    			}, 1L, TimeUnit.SECONDS);
    		} else {
    			System.err.println("服务端链接成功...");
    		}
    	}
    }
    

    通过channelFuture.isSuccess()可以知道在连接的时候是成功了还是失败了,如果失败了我们就启动一个单独的线程来执行重新连接的操作。

    只需要在ConnectionListener添加到ChannelFuture中去即可使用

    public class ImConnection {
    
    	private Channel channel;
    	
    	public Channel connect(String host, int port) {
    		doConnect(host, port);
    		return this.channel;
    	}
    
    	private void doConnect(String host, int port) {
    		EventLoopGroup workerGroup = new NioEventLoopGroup();
    		try {
    			Bootstrap b = new Bootstrap();
    			b.group(workerGroup);
    			b.channel(NioSocketChannel.class);
    			b.option(ChannelOption.SO_KEEPALIVE, true);
    			b.handler(new ChannelInitializer<SocketChannel>() {
    				@Override
    				public void initChannel(SocketChannel ch) throws Exception {
    					
    					// 实体类传输数据,protobuf序列化
                    	ch.pipeline().addLast("decoder",  
                                new ProtobufDecoder(MessageProto.Message.getDefaultInstance()));  
                    	ch.pipeline().addLast("encoder",  
                                new ProtobufEncoder());  
                    	ch.pipeline().addLast(new ClientPoHandlerProto());
    				
    				}
    			});
    
    			ChannelFuture f = b.connect(host, port);
    			f.addListener(new ConnectionListener());
    			channel = f.channel();
    		} catch(Exception e) {
    			e.printStackTrace();
    		}
    	}
    	
    }
    

    运行中连接断开时重试

    使用的过程中服务端突然挂了,就得用另一种方式来重连了,可以在处理数据的Handler中进行处理。

    public class ClientPoHandlerProto extends ChannelInboundHandlerAdapter {
    	private ImConnection imConnection = new ImConnection();
    
    	@Override
    	public void channelRead(ChannelHandlerContext ctx, Object msg) {
    		MessageProto.Message message = (MessageProto.Message) msg;
    		System.out.println("client:" + message.getContent());
    	}
    
    	@Override
    	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    		cause.printStackTrace();
    		ctx.close();
    	}
    	
    	@Override
    	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    		System.err.println("掉线了...");
    		//使用过程中断线重连
    		final EventLoop eventLoop = ctx.channel().eventLoop();
    		eventLoop.schedule(new Runnable() {
    			@Override
    			public void run() {
    				imConnection.connect(ImClientApp.HOST, ImClientApp.PORT);
    			}
    		}, 1L, TimeUnit.SECONDS);
    		super.channelInactive(ctx);
    	}
    
    }
    
    展开全文
  • Netty的断线重连

    千次阅读 2019-05-31 17:24:20
    因为工作中经常使用到TCP,所以会频繁使用到诸如Mina或Netty之类的通信框架,为了... * 提供重连功能,需传入bootstrap,并实现handlers */ @ChannelHandler.Sharable public abstract class FunctionsChanne...

    因为工作中经常使用到TCP,所以会频繁使用到诸如Mina或Netty之类的通信框架,为了方便项目的逻辑调用,经常会在框架的基础上再一次进行封装,这样做其实有画蛇添足的嫌疑,但也是无奈之举。

    这里主要记载使用Mina和Netty,构建适合项目的一个完整的重连逻辑。
    当然,都是作为客户端,毕竟一般只有客户端才会做重连。

    在这之前,需要考虑几个问题:

    • 连接行为的结果可以较为方便地获得,成功或失败,最好直接有接口回调,可以在回调中进行后续逻辑处理
    • 当前通信连接的活跃状态需要准确实时而方便地获得,这样有利于重连时对连接的判断
    • 能够较为灵活的配置Listener或Handler或Filter
    • 支持计数,无论是首次连接失败多次后不再尝试连接,还是中途断开后断线重连多次后不再尝试连接,一般不作无休止地重连

    从代码层面看,框架中最好有一个类似Connector的类,能够暴露合适的接口或方法,提供各种状态与回调,使通信连接的动向能够实时把握,然而事情并不是那么美好。

    连接结果

    由于框架设计的一些原则,一个connector根本不足以暴露这些接口。
    对于Mina而言,作为客户端一般用于连接的连接器是NioSocketConnector
    对于Netty而言,则是Bootstrap

    下表是一些常见的定义在两个框架中的对比,不一定准确,但意义相近;

    定义 Mina Netty
    连接器 SocketConnector Bootstrap
    会话 IoSession Channel
    连接结果 ConnectFuture ChannelFuture
    逻辑处理 IoHandler ChannelHandler
    过滤器 IoFilter ChannelHandler

    对于Mina而言,连接操作是这样的:

                ConnectFuture future = mConnector.connect();
                future.awaitUninterruptibly();
    			if (future.isConnected()) {
    				//得到会话
                    mSession = future.getSession();
    			}
    

    对于Netty来说,连接可以写成与Mina几乎相同的形式:

                ChannelFuture future = bootstrap.connect();
                future.awaitUninterruptibly();
    			if(future.isSuccess()){
                    mChannel = future.channel();
    			}
    

    也可以不阻塞等待,两种future都可以自行添加Listener监听异步任务是否完成:

    //Mina
                future.addListener(new IoFutureListener<IoFuture>() {
    				@Override
    				public void operationComplete(IoFuture ioFuture) {
    
    				}
    			});
    //Netty
                future.addListener(new GenericFutureListener<ChannelFuture>() {
    				@Override
    				public void operationComplete(ChannelFuture f) throws Exception {
    
    				}
    			});
    

    毕竟是出自一人之手,部分API真是惊人的相似。
    到这里,第一个连接返回结果问题算是有所结论,两种框架都可以正常返回连接的结果。

    会话状态

    而上述代码中,返回的mSession与mChannel就是得到的会话,这两种类各自提供了一些接口,可以用于获得通信连接的实时状态。
    Mina的IoSession这里只取部分方法:

    public interface IoSession {
        IoHandler getHandler();
    
        IoSessionConfig getConfig();
    
        IoFilterChain getFilterChain();
    
        ReadFuture read();
    
        WriteFuture write(Object var1);
    
        WriteFuture write(Object var1, SocketAddress var2);
    
        CloseFuture closeNow();
    
        boolean isConnected();
    
        boolean isActive();
    
        boolean isClosing();
    
        boolean isSecured();
    
        CloseFuture getCloseFuture();
    
        SocketAddress getRemoteAddress();
    
        SocketAddress getLocalAddress();
    
        SocketAddress getServiceAddress();
    
        boolean isIdle(IdleStatus var1);
    
        boolean isReaderIdle();
    
        boolean isWriterIdle();
    
        boolean isBothIdle();
    }
    

    再对比看下Netty提供的Channel,这里也只取部分方法展示:

    public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable<Channel> {
        EventLoop eventLoop();
    
        Channel parent();
    
        ChannelConfig config();
    
        boolean isOpen();
    
        boolean isRegistered();
    
        boolean isActive();
    
        SocketAddress localAddress();
    
        SocketAddress remoteAddress();
    
        boolean isWritable();
    
        Channel.Unsafe unsafe();
    
        ChannelPipeline pipeline();
    
        public interface Unsafe {
            SocketAddress localAddress();
    
            SocketAddress remoteAddress();
    
            void register(EventLoop var1, ChannelPromise var2);
    
            void bind(SocketAddress var1, ChannelPromise var2);
    
            void connect(SocketAddress var1, SocketAddress var2, ChannelPromise var3);
    
            void disconnect(ChannelPromise var1);
    
            void close(ChannelPromise var1);
    
            void write(Object var1, ChannelPromise var2);
    
            void flush();
        }
    }
    

    可以看出,无论是IoSession还是Channel,都有相关的API可以知晓通信是否活跃,所以第二个问题在可以获得IoSession或Channel的情况下,是没有问题的。

    配置Handler

    那么再看配置Listener或Handler的相差操作是否灵活。
    二者在这方面的差别较为明显。

    对于Mina而言,添加Handler可以直接利用Connector,真正的逻辑Handler只能由setHandler方法添加,且只能为一个,而相关的Filter则要通过getFilterChain()拿到的过滤器集合去添加;对于Mina来说,Handler和Filter是没有交集的,他们分属不同的接口IoHandler和IoFilter

    mConnector.setHandler(handler);
    mConnector.getFilterChain().addLast(CODEC, protocolCodecFilter);
    

    Netty有所不同,netty中所有的handler、filter都是ChannelHandller,这些handler都要在连接行为发生后才能生效,也就是挂载到Channel上的,而不是Bootstrap,一般添加是这样的:

    bootstrap.handler(handler);
    channel.pipeline().addLast(someHandler);
    channel.pipeline().addLast(someFilter);
    

    但handler依旧只能添加一个,如果要添加多个handler或filter,就必须获取到channel,然后进行添加,netty本身提供了一个ChannelInitializer可以用于添加多个channelHandler,一般会这么写:

                bootstrap.handler(new ChannelInitializer<Channel>() {
    				@Override
    				protected void initChannel(Channel channel) throws Exception {
                        channel.pipeline().addLast(handler);
                        channel.pipeline().addLast(someHandler);
                        channel.pipeline().addLast(someFilter);
    				}
    			});
    

    对于Netty来说,Handler和Filter是同一个东西,都是ChannelHandler

    两者在这方面的区别比较明显:
    一是netty将handler和filter都统一为handler了,
    二是netty不能像mina一样,在未连接之前就可以配置所有的Handler或Filter,netty必须获得channel也就是连接成功后才能配置多个Filter。

    这就造成了一个问题,Mina可以提前就配置监听器监听连接的状态,可以正常监听中途断开,也就是在创建Connector后就可以挂载上监听:

            mConnector.getFilterChain().addFirst("reconnect", new IoFilterAdapter() {
    			@Override
    			public void sessionClosed(NextFilter nextFilter, IoSession session) throws Exception {				
    				//监听到断开,可接入回调接口,做进一步的重连逻辑
                    mConnector.connect();
    			}
    		});
    

    而Netty不能,创建Connector也就是Bootstrap并不能实现类似的挂载,Bootstrap只能挂载一个Handler,而相关的过滤器或监听只能在Channel出现后再进行挂载,那么就会写成这样:

                bootstrap.handler(new ChannelInitializer<Channel>() {
    				@Override
    				protected void initChannel(Channel channel) throws Exception {
    					//添加其他Filter或Handler
    				}
    
    				@Override
    				public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    					super.channelInactive(ctx);
    					//监听到断开,重连
                        bootstrap.connect();
    				}
    			});
    

    这里initChannel方法永远是最先被调用的,因为在源码中是这样的:

    //ChannelInitializer.java
        protected abstract void initChannel(C var1) throws Exception;
    
        public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            if (this.initChannel(ctx)) {
                ctx.pipeline().fireChannelRegistered();
                this.removeState(ctx);
            } else {
                ctx.fireChannelRegistered();
            }
        }
    

    在这种逻辑下,Mina可以在sessionClosed回调中使用SocketConnetor进行重连,Netty可以在channelInactive回调中使用Bootstrap进行重连。
    看起来没什么毛病。

    但需要注意一点,就是Handler的复用问题,也就是对Handler或Filter的检查,Mina和Netty都有对Handler的重复添加进行过检查,不过检查逻辑有细微的差别。
    Mina中是这样检查的:

    //DefaultIoFilterChain.java
        public synchronized void addLast(String name, IoFilter filter) {
            this.checkAddable(name);
            this.register(this.tail.prevEntry, name, filter);
        }
    
        private final Map<String, Entry> name2entry = new ConcurrentHashMap();
    
        private void checkAddable(String name) {
            if (this.name2entry.containsKey(name)) {
                throw new IllegalArgumentException("Other filter is using the same name '" + name + "'");
            }
        }
    

    可以看到,Mina只会检查Filter在Map中对应的key是否被使用过,当然理论上Filter挂载在SocketConnector的FilterChain中,只要配置过一次,就无需再进行配置。

    那么Netty呢?
    Netty的Handler不是能随意复用的,要复用必须标明注解@Sharable,否则就会出现异常:

    警告: Failed to initialize a channel. Closing: [id: 0x1caafa97]
    io.netty.channel.ChannelPipelineException: io.netty.handler.timeout.IdleStateHandler is not a @Sharable handler, so can't be added or removed multiple times.
    

    这是因为在源码进行检查时,是对Handler本身进行检查的,handler会有一个added的属性,一旦被添加使用过,就会置为true,而判断逻辑会阻止为added=true的handler添加进来 。这样一来,如果强行添加已经添加过的handler就会抛出异常:

    //DefaultChannelPipeline.java
     public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
            AbstractChannelHandlerContext newCtx;
            synchronized(this) {
                checkMultiplicity(handler);
                newCtx = this.newContext(group, this.filterName(name, handler), handler);
                //省略部分代码
            }
    
            this.callHandlerAdded0(newCtx);
            return this;
        }
        
       private static void checkMultiplicity(ChannelHandler handler) {
            if (handler instanceof ChannelHandlerAdapter) {
                ChannelHandlerAdapter h = (ChannelHandlerAdapter)handler;
                if (!h.isSharable() && h.added) {
                    throw new ChannelPipelineException(h.getClass().getName() + " is not a @Sharable handler, so can't be added or removed multiple times.");
                }
                h.added = true;
            }
        }
    

    这也就说明使用channel.pipeline().addLast(handler)这种方法添加handler时,如果想不同的Channel添加同一个Handler实例,每种handler都必须注解了@Sharable,如果正好要使用IdleStateHandler这种源码内部的Handler,而IdleStateHandler是没有注解过@Sharable,那么就会出现上面的异常。
    而实际应用中,为了实现心跳,IdleStateHandler是一般都会使用到的。

    那么问题来了,Mina每次重新连接,创建新的session,但只要SocketConnector没有变,所有Handler和Filter自然就没有变,仍然可用,因为所有Handler和Filter是挂载到SocketConnector的FilterChain中,算是只和Connector相关的;
    而Netty,如果重新连接的话,会创建新的Channel,然后会重新调用initChannel,然后利用channel.pipeline().addLast添加Handler,算是挂载到Channel上的,而不是Bootstrap上。

    这样显示出两者最大的区别就是,Mina中配置一次即可,而Netty则需要每次产生新的Channel时对其进行重新配置。

    所以Netty中的handler想复用的话,就必须加注解,否则就会报异常。如果一定要用到无法注解@Sharable的Handler,比如上面的IdleStateHandler,那就要想办法每次initChannel时,也新建一个新的IdleStateHandler…
    或者,继承IdleStateHandler,然后加上注解也行,虽然也很丑就是了。

    So Bad…

    这样的情况下,可以想办法,每次都新建,类似这种:

                FunctionsChannelHandler functionsChannelHandler = new FunctionsChannelHandler(bootstrap){
    
    				@Override
    				public ChannelHandler[] handlers() {
    					return new ChannelHandler[]{
    							new NormalClientEncoder(),
    							new IdleStateHandler(20, 10, 20),
    							this,
    							new NormalClientHandler()};
    				}
    			};
    
                bootstrap.handler(new ChannelInitializer<Channel>() {
    				@Override
    				protected void initChannel(Channel channel) throws Exception {
    					//添加各种handler
                        channel.pipeline().addLast(functionsChannelHandler.handlers());
    				}
    
    				@Override
    				public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    					super.channelInactive(ctx);
    					//监听到断开
    				}
    			});
    

    因为netty把所有监听器过滤器逻辑处理都归为ChannelHandler的原因,把一个handler扩展成一个功能较为丰富的handler是一种不错的方法 。或者沿用这种思路,使其每次新加Handler时,都是new过的Handler。
    应对框架自带的一些未注解@Sharable的类,也可以继承之,自行加注解:

    @ChannelHandler.Sharable
    public class HeartHandler extends IdleStateHandler {
    	public HeartHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
    		super(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds);
    	}
    
    	public HeartHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
    		super(readerIdleTime, writerIdleTime, allIdleTime, unit);
    	}
    
    	public HeartHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
    		super(observeOutput, readerIdleTime, writerIdleTime, allIdleTime, unit);
    	}
    }
    

    这样一来,配置Handler也勉强可算灵活了。

    连接计数

    对连接计数一般都是开发者编写的逻辑,主要是应对无休止地连接。
    主要应用在两种场景:
    一是首次连接,如果多次连接不成功,那么停止连接,或者另有逻辑;
    二是断线重连,如果多次重连不成功,那么停止连接并销毁,或者另有逻辑。

    因为Mina和Netty都是多线程模型的缘故,计数为了求稳可以直接使用Atom类,当然觉得大材小用也可以直接使用普通int值,毕竟理论上两次连接中间应该会有一定延时才对。

    应用示例

    所以最后,都可以对各自的连接器进行二次封装,然后编写对自己有利的逻辑。
    对于Mina,大概可以写成这样:

    public class TCPConnector {
    
    	private static final int BUFFER_SIZE = 10 * 1024;
    	private static final long CONNECT_TIMEOUT_MILLIS = 10 * 1000;
    	private static final int KEEPALIVE_REQUEST_INTERVAL = 10;
    	private static final int KEEPALIVE_REQUEST_TIMEOUT = 40;
    
    	private static final String RECONNECT = "reconnect";
    	private static final String CODEC = "codec";
    	private static final String HEARTBEAT = "heartbeat";
    	private static final String EXECUTOR = "executor";
    
    
    	/**
    	 * 连接器
    	 */
    	private SocketConnector mConnector;
    	/**
    	 * 会话
    	 */
    	private IoSession mSession;
    
    	/**
    	 * 外用接口
    	 */
    	private IConnectorListener connectorListener;
    
    	/**
    	 * 连接所在线程
    	 */
    	private ExecutorService mExecutor;
    
    	/**
    	 * 重连次数
    	 */
    	private AtomicInteger recconnectCounter;
    	/**
    	 * 首次连接次数
    	 */
    	private AtomicInteger connectCounter;
    
    	private String host;
    	private int port;
    
    	public interface IConnectorListener {
    		/**
    		 * 连接建立成功
    		 */
    		void connectSuccess(IoSession session);
    
    		/**
    		 * 连接建立失败
    		 */
    		void connectFailed();
    
    		/**
    		 * 连接中途断掉时
    		 */
    		void sessionClosed(IoSession session);
    	}
    
    	public TCPConnector() {
    		mConnector = new NioSocketConnector();
    		recconnectCounter = new AtomicInteger(0);
    		connectCounter = new AtomicInteger(0);
    	}
    
    	/**
    	 * 设置目标地址与端口
    	 *
    	 * @param host 目标地址
    	 * @param port 目标端口
    	 */
    	public void setHostPort(String host, int port) {
            L.d("设置地址与端口-" + host + ":" + port);
    		this.host = host;
    		this.port = port;
    	}
    
    	public String getHost() {
    		return this.host;
    	}
    
    	/**
    	 * 在子线程中启用连接
    	 */
    	public void connectInThread() {
            mExecutor.execute(new Runnable() {
    			@Override
    			public void run() {
    				connect();
    			}
    		});
    	}
    
    	/**
    	 * 根据设置的参数连接
    	 */
    	private void connect() {
    		//如果已经连接,则直接【连接成功】
    		if (mSession == null || !mSession.isConnected()) {
    			//连接
                mConnector.setDefaultRemoteAddress(new InetSocketAddress(host, port));
                L.i("连接-->" + host + ":" + port);
                ConnectFuture future = mConnector.connect();
    			//阻塞,等待连接建立响应
                future.awaitUninterruptibly(CONNECT_TIMEOUT_MILLIS);
    			//响应连接成功或失败
    			if (future.isConnected()) {
    				//得到会话
                    mSession = future.getSession();
    				if (connectorListener != null) {
                        connectCounter.set(0);
                        connectorListener.connectSuccess(mSession);
    				}
    			} else {
    				if (connectorListener != null) {
                        connectCounter.incrementAndGet();
                        connectorListener.connectFailed();
    				}
    			}
    		} else {
    			if (connectorListener != null) {
                    connectCounter.incrementAndGet();
                    connectorListener.connectSuccess(mSession);
    			}
    		}
    	}
    
    	/**
    	 * 重连
    	 */
    	private void reconnect() {
    		if (mConnector == null)
    			throw new IllegalArgumentException("IoConnector cannot be null");
    		//如果已经连接,则直接【连接成功】
    		if (mSession == null || !mSession.isConnected()) {
    			//连接
                ConnectFuture future = mConnector.connect();
    			//阻塞,等待连接建立响应
                future.awaitUninterruptibly();
    			//响应连接成功或失败
    			if (future.isConnected()) {
    				//得到会话
                    mSession = future.getSession();
    			}
    		}
    	}
    
    	/**
    	 * 重连
    	 *
    	 * @param reconnectTimeoutMills 连接的超时时间
    	 * @param reconnectTimes        重连次数
    	 */
    	public void reconnect(final long reconnectTimeoutMills, final int reconnectTimes) {
    		try {
                recconnectCounter.set(0);
    			while (mConnector != null && !(mSession != null && mSession.isConnected()) && recconnectCounter.incrementAndGet() < reconnectTimes) {
    				reconnect();
    				if (mSession != null && mSession.isConnected()) {
    					break;
    				}else{
                        TimeUnit.MILLISECONDS.sleep(reconnectTimeoutMills);
    				}
    				L.w(Thread.currentThread().getName() + "," + "重连" + host + ":" + port + "(" + recconnectCounter.get() + ")次...");
    			}
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			if (mSession != null && mSession.isConnected()) {
    				if (connectorListener != null) {
                        connectorListener.connectSuccess(mSession);
    				}
    			} else {
    				if (connectorListener != null) {
                        connectorListener.connectFailed();
    				}
    			}
    		}
    	}
    
    	public IoSession getSession() {
    		return mSession;
    	}
    
    	public IoConnector getConnector() {
    		return mConnector;
    	}
    
    	public boolean isActive() {
    		return mConnector != null && mConnector.isActive() && mSession != null;
    	}
    
    	public boolean isConnected() {
    		return mSession != null && mSession.isConnected();
    	}
    
    	public IConnectorListener getConnectorListener() {
    		return connectorListener;
    	}
    
    	public int getRecconnectCounter() {
    		return recconnectCounter.get();
    	}
    
    	public int getConnectCounter() {
    		return connectCounter.get();
    	}
    
    	public void resetConnectCounter() {
            connectCounter.set(0);
    	}
    
    	/**
    	 * 断开连接,释放资源
    	 */
    	public void disconnect() {
    		if (mConnector != null) {
                connectorListener = null;
                mConnector.getFilterChain().clear();
                mConnector.dispose();
                mConnector = null;
    		}
    		if (mSession != null) {
                mSession.closeNow();
                mSession = null;
    		}
    		if (mExecutor != null) {
                mExecutor.shutdown();
                mExecutor = null;
    		}
            L.w("断开");
    	}
    
    	public static class Builder {
    		private TCPConnector newInstance = new TCPConnector();
    		private ProtocolCodecFilter protocolCodecFilter;
    		private KeepAliveFilter keepAliveFilter;
    
    		public Builder setExecutor(ExecutorService executor) {
                newInstance.mExecutor = executor;
    			return this;
    		}
    
    		public Builder setConnectListener(IConnectorListener listener) {
                newInstance.connectorListener = listener;
    			return this;
    		}
    
    		public Builder setHost(String host) {
                newInstance.host = host;
    			return this;
    		}
    
    		public Builder setPort(int port) {
                newInstance.port = port;
    			return this;
    		}
    
    		public Builder setProtocolCodecFilter(ProtocolCodecFactory protocolCodecFactory) {
                protocolCodecFilter = new ProtocolCodecFilter(protocolCodecFactory);
    			return this;
    		}
    
    		public Builder setConnectTimeoutMillis(long connectTimeoutMillis) {
                newInstance.mConnector.setConnectTimeoutMillis(connectTimeoutMillis);
    			return this;
    		}
    
    		public Builder setKeepAliveFilter(KeepAliveMessageFactory keepAliveMessageFactory, int keepAliveRequestInterval) {
                keepAliveFilter = new KeepAliveFilter(keepAliveMessageFactory, IdleStatus.BOTH_IDLE, KeepAliveRequestTimeoutHandler.LOG, keepAliveRequestInterval, KEEPALIVE_REQUEST_TIMEOUT);
    			return this;
    		}
    
    		public Builder setKeepAliveFilter(KeepAliveMessageFactory keepAliveMessageFactory, KeepAliveRequestTimeoutHandler keepAliveRequestTimeoutHandler, int keepAliveRequestInterval, int keepAliveRequestTimeOut) {
                keepAliveFilter = new KeepAliveFilter(keepAliveMessageFactory, IdleStatus.BOTH_IDLE, keepAliveRequestTimeoutHandler, keepAliveRequestInterval, keepAliveRequestTimeOut);
    			return this;
    		}
    
    
    		public Builder setHandlerAdapter(IoHandlerAdapter handler) {
                newInstance.mConnector.setHandler(handler);
    			return this;
    		}
    
    		public Builder setReadBuffer(int size) {
                newInstance.mConnector.getSessionConfig().setReadBufferSize(size);
    			return this;
    		}
    
    		public Builder setReceiveBuffer(int size) {
                newInstance.mConnector.getSessionConfig().setReceiveBufferSize(size);
    			return this;
    		}
    
    		public Builder setSendBuffer(int size) {
                newInstance.mConnector.getSessionConfig().setSendBufferSize(size);
    			return this;
    		}
    
    		public TCPConnector build() {
    			//添加重连监听
    			if (newInstance.connectorListener != null) {
                    newInstance.mConnector.getFilterChain().addFirst(RECONNECT, new IoFilterAdapter() {
    					@Override
    					public void sessionClosed(NextFilter nextFilter, IoSession session) throws Exception {
    						if (newInstance != null && newInstance.connectorListener != null)
                                newInstance.connectorListener.sessionClosed(session);
    					}
    				});
    			}
    
    			//设置编码解码
    			if (protocolCodecFilter != null)
                    newInstance.mConnector.getFilterChain().addLast(CODEC, protocolCodecFilter);
    
    			//设置心跳
    			if (keepAliveFilter != null)
                    newInstance.mConnector.getFilterChain().addLast(HEARTBEAT, keepAliveFilter);
    
    			//connector不允许使用OrderedThreadPoolExecutor
                  newInstance.mConnector.getFilterChain().addLast(EXECUTOR, new ExecutorFilter(Executors.newSingleThreadExecutor()));
    			return newInstance;
    		}
    	}
    
    	@Override
    	public String toString() {
    		return "MinaHelper{" +
    				"mSession=" + mSession +
    				", mConnector=" + mConnector +
    				", connectorListener=" + connectorListener +
    				", mExecutor=" + mExecutor +
    				'}';
    	}
    
    	@Override
    	public boolean equals(Object o) {
    		if (this == o) return true;
    		if (o == null || getClass() != o.getClass()) return false;
    
            TCPConnector connector = (TCPConnector) o;
    
    		return port == connector.port && (host != null ? host.equals(connector.host) : connector.host == null);
    	}
    
    	@Override
    	public int hashCode() {
    		int result = host != null ? host.hashCode() : 0;
            result = 31 * result + port;
    		return result;
    	}
    

    使用起来是这样的:

            HigherGateWayHandler higherGateWayHandler = new HigherGateWayHandler();
            TCPConnector higherGateWayClient = new TCPConnector.Builder()
    				.setExecutor(ThreadPool.singleThread("higher_gateway_client"))
    				.setHost(NC.GATEWAT_HIGHER_HOST)
    				.setPort(NC.LOWER_PORT)
    				.setConnectTimeoutMillis(10 * 1000)
    				.setReadBuffer(10 * 1024)
    				.setHandlerAdapter(higherGateWayHandler)
    				.setProtocolCodecFilter(new HigherGateWayCodecFactory())
    				.setKeepAliveFilter(new KeepAliveHigherGateWay(), higherGateWayHandler, 10, 20)
    				.setConnectListener(new TCPConnector.IConnectorListener() {
    					@Override
    					public void connectSuccess(IoSession session) {
                            //连接成功后
    					}
    
    					@Override
    					public void connectFailed() {
    						//重连失败
    						if (higherGateWayClient.getRecconnectCounter() == 3) {
    							//重连失败后
    						}
    						//非重连失败,优先级连接情况下
    						if (higherGateWayClient.getRecconnectCounter() == 0 && higherGateWayClient.getConnectCounter() > 2) {
                                higherGateWayClient.resetConnectCounter();
    						} else {
                                higherGateWayClient.connectInThread();
    						}
    					}
    
    					@Override
    					public void sessionClosed(IoSession session) {
                            executors.execute(new Runnable() {
    							@Override
    							public void run() {
    							   //重连逻辑
                                   higherGateWayClient.reconnect(10 * 1000, 3);
    							}
    						});
    					}
    				})
    				.build();
    

    而Netty,封装起来会有一点花里胡哨,目前遇到的问题是当重连以后复用IdleStateHandler这种Handler时,就会使得其中的计时机制失效,也就是说,心跳没用了,暂时不明原因,大概率是其中的线程被销毁无法再起的原因。那么当前就只能想办法每次调用initChannel时,创建新的Handler才行:

    public class NettyConnector {
    
    	/**
    	 * 连接器
    	 */
    	private Bootstrap bootstrap;
    
    	/**
    	 * 地址
    	 */
    	private String host;
    	private int port;
    
    	/**
    	 * 会话
    	 */
    	private Channel channel;
    
    	private static final long TIME_OUT = 10;
    
    
    	private long connectTimeoutMills;
    
    	/**
    	 * 重连次数
    	 */
    	private AtomicInteger recconnectCounter;
    
    	/**
    	 * 首次连接次数
    	 */
    	private AtomicInteger connectCounter;
    
    	/**
    	 * 以接口引出通信状态
    	 */
    	public interface IChannelStateListener {
    		void onConnectSuccess(Channel channel);
    
    		void onConnectFailed();
    
    		void onDisconnect();
    	}
    
    	private IChannelStateListener channelStateListener;
    
    	private NettyConnector(final Builder builder) {
            recconnectCounter = new AtomicInteger(0);
            connectCounter = new AtomicInteger(0);
            connectTimeoutMills = builder.timeoutMills;
            bootstrap = builder.bootstrap;
            bootstrap.handler(new ChannelInitializer() {
    			@Override
    			protected void initChannel(Channel channel) throws Exception {
                    channel.pipeline().addLast(new ChannelDisconnectHandler());
                    channel.pipeline().addLast(builder.handlerSet.handlers());
    			}
    		});
    	}
    
    	public void setRemoteAddress(String host, int port) {
            L.d("设置地址与端口-" + host + ":" + port);
    		this.host = host;
    		this.port = port;
    	}
    
    
    	public void setChannelStateListener(IChannelStateListener listener) {
            channelStateListener = listener;
    	}
    
    	public void connect() {
    		if (channel == null || !channel.isActive()) {
                bootstrap.remoteAddress(this.host, this.port);
                L.d("第" + (connectCounter.get() + 1) + "次连接" + host + ":" + port + "中......");
    
    			final long startMills = System.currentTimeMillis();
                ChannelFuture channelFuture = bootstrap.connect();
    
                channelFuture.addListener(new GenericFutureListener<ChannelFuture>() {
    				@Override
    				public void operationComplete(ChannelFuture f) throws Exception {
    					if (f.isSuccess()) {
                            L.d("连接(" + bootstrap.config().remoteAddress() + ")成功");
                            channel = f.channel();
    						if (channelStateListener != null) {
                                connectCounter.set(0);
                                channelStateListener.onConnectSuccess(channel);
    						}
    					} else {
    						long delay = System.currentTimeMillis() - startMills;
    						if (delay > 0) {
                                TimeUnit.MILLISECONDS.sleep(connectTimeoutMills - delay);
    						}
                            L.d("连接(" + bootstrap.config().remoteAddress() + ")失败");
    						if (channelStateListener != null) {
                                connectCounter.incrementAndGet();
                                channelStateListener.onConnectFailed();
    						}
    					}
    				}
    			});
    		}
    	}
    
    	private void reconnect() {
    		if (bootstrap == null)
    			throw new IllegalArgumentException("bootstrap cannot be null");
    		//如果已经连接,则直接【连接成功】
    		if (channel == null || !channel.isActive()) {
    			//连接
                channel = bootstrap.connect().awaitUninterruptibly().channel();
    		}
    	}
    
    	/**
    	 * 重连
    	 * @param reconnectTimeoutMills 重连超时时间
    	 * @param reconnectTimes 重连次数
    	 */
    	public void reconnect(final long reconnectTimeoutMills, final int reconnectTimes) {
    		try {
                recconnectCounter.set(0);
    			while (channel != null && !channel.isActive() && recconnectCounter.getAndIncrement() < reconnectTimes) {
                    L.d(Thread.currentThread().getName() + "," + "重连" + bootstrap.config().remoteAddress() + "(" + recconnectCounter.get() + ")次...");
    				reconnect();
    				if (channel.isActive()) {
    					break;
    				} else {
                        TimeUnit.MILLISECONDS.sleep(reconnectTimeoutMills);
    				}
                    L.d(channel.isActive() + "");
    			}
    		} catch (InterruptedException e) {
                e.printStackTrace();
    		} finally {
    			if (channel != null && channel.isActive()) {
    				if (channelStateListener != null) {
                        channelStateListener.onConnectSuccess(channel);
    				}
    			} else {
    				if (channelStateListener != null) {
                        channelStateListener.onConnectFailed();
    				}
    			}
    		}
    	}
    
    	public Channel getChannel() {
    		return channel;
    	}
    
    	public boolean isConnected() {
    		return channel != null && channel.isActive();
    	}
    
    	public String getAddress() {
    		return host + ":" + port;
    	}
    
    	public int getConnectFailedTimes() {
    		return connectCounter.get();
    	}
    
    	public int getReconnectFailedTimes() {
    		return recconnectCounter.get();
    	}
    
    	public static class Builder {
    
    		private Bootstrap bootstrap = new Bootstrap();
    		private HandlerSet handlerSet;
    		private long timeoutMills = 10 * 1000;
    
    		public Builder group(EventLoopGroup loopGroup) {
                bootstrap.group(loopGroup);
    			return this;
    		}
    
    		@Deprecated
    		public Builder remoteAddress(String inetHost, int inetPort) {
                bootstrap.remoteAddress(inetHost, inetPort);
    			return this;
    		}
    
    		public Builder setConnectTimeoutMills(long timeout) {
                timeoutMills = timeout;
    			return this;
    		}
    
    		public Builder handler(HandlerSet handlers) {
                handlerSet = handlers;
    			return this;
    		}
    
    		public NettyConnector build() {
                bootstrap.channel(NioSocketChannel.class);
    			return new NettyConnector(this);
    		}
    	}
    
    	/**
    	 * 主要用于监听断开
    	 */
    	class ChannelDisconnectHandler extends ChannelInboundHandlerAdapter {
    
    		@Override
    		public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                ctx.fireChannelInactive();
    			if (channelStateListener != null) {
                    channelStateListener.onDisconnect();
    			}
    		}
    	}
    
    	/**
    	 * 主要用于创建新的handler,避免复用带来的一些问题
    	 */
    	@ChannelHandler.Sharable
    	public static abstract class HandlerSet extends ChannelInboundHandlerAdapter {
    		public abstract ChannelHandler[] handlers();
    	}
    
    }
    

    因为Netty不能像Mina直接在Connector上挂载监听sessionClosed,只能用一个ChannelDisconnectHandler这样的东西去监听是否已经断开,并通过接口引出结果;
    并且因为只能在Channel.Pipeline中才能添加多个Handler的原因,这里用一个HandlerSet强行将所有需要的Handler集合,然后在创建Bootstrap的时候一次性添加进去,想要保证每次都新建,这里就使用抽象方法,让使用的时候可以自行创建。
    注意,由于这里的抽象类HandlerSet每次其实并不是新建的,所有是需要复用的,所以需要加注解@Sharable,但也只需要加它一个就行了,其他都是新建出来的,无需理会。
    写出来就是这样:

            NettyConnector connector = new NettyConnector.Builder()
    				.group(new NioEventLoopGroup())
    				.handler(new NettyConnector.HandlerSet() {
    
    					@Override
    					public ChannelHandler[] handlers() {
    						return new ChannelHandler[]{new HeartHandler(10, 10, 10),
    								new NormalClientEncoder(),
    								new NormalClientHeartBeatHandler(),
    								new NormalClientHandler()};
    					}
    				})
    				.setConnectTimeoutMills(5 * 1000)
    				.build();
    
            connector.setRemoteAddress("192.168.0.102", 8000);
            connector.setChannelStateListener(new NettyConnector.IChannelStateListener() {
    			@Override
    			public void onConnectSuccess(Channel channel) {
                    L.d("连接" + channel.remoteAddress().toString() + "成功");
    			}
    
    			@Override
    			public void onConnectFailed() {
                    L.d("连接" + connector.getAddress() + "失败");
    				if (connector.getReconnectFailedTimes() == 0 && connector.getConnectFailedTimes() < 3) {
                        connector.connect();
    				}
    			}
    
    			@Override
    			public void onDisconnect() {
                    L.d(connector.getChannel().remoteAddress().toString() + "已断开");
                    connector.reconnect(5000, 5);
    			}
    		});
    

    其中的HeartHandler是继承自IdleStateHandler的。

    整个封装显得花里胡哨…却又很丑,不过勉强能用,水平有限。

    就这样吧。

    展开全文
  • netty4 断线重连

    2019-09-28 11:09:12
    监听到连接服务器失败时,会在3秒后重新连接(执行doConnect...在我们自己定义逻辑处理的Handler中 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { Log.d(Config....

    监听到连接服务器失败时,会在3秒后重新连接(执行doConnect方法)

    这还不够,当客户端掉线时要进行重新连接

    在我们自己定义逻辑处理的Handler中

     

    @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            Log.d(Config.TAG, "与服务器断开连接服务器");
            super.channelInactive(ctx);
            MsgHandle.getInstance().channel = null;
    
            //重新连接服务器
            ctx.channel().eventLoop().schedule(new Runnable() {
                @Override
                public void run() {
                    MyClient.doConnect();
                }
            }, 2, TimeUnit.SECONDS);
            ctx.close();
        }

    转载于:https://www.cnblogs.com/zhengtu2015/p/7447255.html

    展开全文
  • <div><p>...CTP在中午休市时断线重连之后获取到当日所有trade,重复触发position计算逻辑。</p><p>该提问来源于开源项目:ricequant/rqalpha-mod-vnpy</p></div>
  • obs-studio 断线重连

    万次阅读 2017-06-09 20:03:06
    obs发送逻辑在rtmp-stream.c文件中 暴露出的连接口  /**  * Sets the reconnect settings. Set retry_count to 0 to disable reconnecting.  */ EXPORT void obs_output_set_reconnect_settings(obs_...
  • jedis:subscribe(订阅)断线重连(reconnect)

    千次阅读 2019-05-08 23:49:49
    使用jedis 实现redis消息...在实际应用场景下,Redis服务暂时中断是可预见一种异常,必须处理,这时就必须实现重连(reconnect)。 下面是我的应用中实现subscribe reconnect的逻辑。 /** * 创建消息线程,订阅指定...
  • 写好ChatPlayer,MJPlayer等类。 同时用注解标记好每个模块断线重连相关的处理函数,这样玩家断线重连时,每个模块进行自己的断线重连逻辑处理。
  • 开放平台通信(OPC)和OPC UA(统一架构)是促进可编程逻辑控制器(PLCs)、人机接口(HMIs)、服务器、客户端和其他机器之间的数据交换的标准,以实现互联互通和信息流。 当然,这种互性和通信性对于制造工厂来说是必不可...
  • 1.首先进入房间有四种途径 a.进入大厅界面的时候查看是个有oldRoomId,如果有直接进入对应房间 ...用户断线重连的时候,如果房间号存在,则直接进入房间 2.接下来就是进入房间实际操作 enterRoom:f...
  • 下载 ... 逻辑 5分钟运行一次 win.tcp.est.changed.py ...因为当前服务器是作为client连接远程,由于自己写的协议不健壮性,导致我们作为client会被远端异常Abort,虽然现在我们的程序可以断线重连,但是
  • python MySQLdb 封装接口

    2012-05-18 12:09:39
    鸟人封装的MySQLdb操作接口,包含读写分离、断线重连逻辑,对于具体应用可以继承和扩展
  • websocket js辅助代码

    2019-05-23 10:02:00
    封装了页面段js代码,页面上就... * 配置了断线重连机制 * * 页面调用形式 * var webSocketCal=new tbwebsocket(); * //设定断线重连连接次数 * webSocketCal.setLimitConnectNum(100); * //https的连接 w...
  •  分布式(网络透明的分布式运算 组件编程 自动连接自动断线 共享变量/对象/类/和过程),  网络编程,无状态数据,  安全,动态类型,异常处理,内存管理,交互的IDE  多用途语言(交互的应用程序,GUI窗口程序,移动客户...
  • egret项目bug总结

    2020-12-16 16:52:26
    怀疑是断线重连回来,退出战斗的逻辑异常 重现步骤:pc端断网,然后重连,大概率弹出重连游戏提示框,手机易复现但无法代码调试 结论:只能通过分析代码,查看网络重连那块代码作了哪些处理,发现只作了挂机战斗重连...
  • 保证逻辑帧与渲染帧分离,逻辑帧保证同步,版本控制,确保战斗流畅,网络流量优化,处理丢包,处理断线重连,防作弊
  • RTKRegister.zip

    2020-12-31 09:17:17
    Android App通过Ntrip协议获取RTK差分数据。只需配置NtripCaster的IP地址,端口号,挂载点即可。然后上传GGA数据获取RTK差分数据。完整的网络处理逻辑断线重连等机制。
  • 专项-弱网络测试

    千次阅读 2020-08-25 15:35:03
    客户端的核心场景必须有断线重连机制,并在有网络抖动、延时、丢包的网络场景下,客户端需达到以下要求: 一. 不能出现以下现象: 1、游戏中不能出现收支不等、客户端卡死/崩溃等异常情况; 2、游戏核心功能(如登录...
  • MMORPG游戏服务器技术选型参考 游戏服务器一般追求稳定和效率,所以偏向于保守,...多个网关:维持与玩家间的SOCKET连接,可处理广播、断线重连逻辑。 一个或多个账号登陆验证服务器:处理登陆、排队等逻辑。 多...
  • 1、典型按场景分服设计开发语言: c++数据库:mysql架构:多个网关:维持与玩家间的SOCKET连接,可处理广播、断线重连逻辑。一个或多个账号登陆验证服务器:处理登陆、排队等逻辑。多个场景服务器:处理在本地图上...
  • 1、典型按场景分服设计开发语言: c++数据库:mysql架构:多个网关:维持与玩家间的SOCKET连接,可处理广播、断线重连逻辑。一个或多个账号登陆验证服务器:处理登陆、排队等逻辑。多个场景服务器:处理在本地图上...
  • 2、ZK初始化时unlock逻辑调整,优化断线重连特性; 3、Client端ZK初始化逻辑调整,取消对ZK状态的强依赖,连接失败也允许启动,此时使用镜像配置文件; 4、修复配置监听首次无效的问题,监听前先get一次该配置; 5、...
  • WebSocket样例

    2018-12-01 15:26:17
    此Demo是使用maven实现的一个小的样例,意在帮助同学们理解,同时实现了断线自动重连,做IOM通讯的同学们,更好的关注逻辑即可,无需考虑断线,断网带来的问题
  • C# 多线程断点续传

    2012-05-31 00:11:41
    这个还是比较简单的,当然如果功能比较复杂,质量要求高的话,...断线重连, 下载完成 较检   难的地方在于线程网络请求断开重连,重新下载的处理。 以及如何判断服务器支持的最大可连接数,以及相关的逻辑处理。
  • 包含客户端服务器端,运行即用 ip端口全部写好了 也没多少代码, 游戏逻辑完全没有实现(只写了一个简易的测试用的第三人称控制器,完全不适合帧同步) ,还有很多逻辑需要完善,就像断线重连和单位生成逻辑,...
  • 1、内置注册中心选择ZK时逻辑优化,ZK初始化时unlock逻辑调整,优化断线重连特性; 2、除了springboot类型示例;新增无框架示例项目 "xxl-rpc-executor-sample-frameless"。不依赖第三方框架,只需main方法即可启动...

空空如也

空空如也

1 2 3 4
收藏数 76
精华内容 30
关键字:

断线重连逻辑