netty的LocalChannel源码分析

LocalChannel介绍

LcoalChannel是Netty提供的用来在同一个JVM内部实现client和server之间通信的transport。它的实现主要是通过内存里的对象作为通信介质,不会像NIO下的channel,会占用一个文件描述符;因此使用它不会影响到你系统上的打开文件数,也就不会影响到你系统所能管理的连接数了。对于在同一个JVM内部使用netty的机制进行通信的话,相对来说算轻量级的。

示例

工作中的使用还是比较负载的,我在这里写了一个简单的例子,下面的讨论都是参照这个例子的。

LocalEchoClientHandler代码示例

LocalEchoServerHandler代码示例

LocalEcho 总调用代码示例

可以看到使用方式跟使用其他的channel没有什么区别,需要注意的就是,LocalChannel也实现了自己的EventLoop,所以在使用的时候还要是配套的使用相对应的LocalEventLoopGroup。

内部实现

LocalServerChannel的bind

首先要看的当然就是server启动之后是怎么监听端口的,从示例代码中可以看出,使用LocalChannel的Server端所bind的地址只是一个字符串,并不是ip+port的形式,这也说明了LocalChannel的东西是不会占用你的端口的。

当你初始化了ServerBootStrap之后就要把它绑定到这个字符串所代表的“端口”上来,那么它是怎么做的呢?我这里抓了一下它的调用栈,我觉得它已经可以说明一切了。

"localEventLoopGroup-2-1@1385" prio=10 tid=0xd nid=NA runnable
  java.lang.Thread.State: RUNNABLE
      at io.netty.channel.local.LocalChannelRegistry.register(LocalChannelRegistry.java:32)
      at io.netty.channel.local.LocalServerChannel.doBind(LocalServerChannel.java:91)
      at io.netty.channel.AbstractChannel$AbstractUnsafe.bind(AbstractChannel.java:485)
      at io.netty.channel.DefaultChannelPipeline$HeadContext.bind(DefaultChannelPipeline.java:1081)
      at io.netty.channel.AbstractChannelHandlerContext.invokeBind(AbstractChannelHandlerContext.java:502)
      at io.netty.channel.AbstractChannelHandlerContext.bind(AbstractChannelHandlerContext.java:487)
      at io.netty.channel.DefaultChannelPipeline.bind(DefaultChannelPipeline.java:904)
      at io.netty.channel.AbstractChannel.bind(AbstractChannel.java:198)
      at io.netty.bootstrap.AbstractBootstrap$2.run(AbstractBootstrap.java:348)
      at io.netty.channel.local.LocalEventLoop.run(LocalEventLoop.java:33)
      at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:111)
      at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
      at java.lang.Thread.run(Thread.java:745)

整个很简单,最后的bind操作落到了io.netty.channel.local.LocalChannelRegistry.register。这是一个全局的注册表,它做的事情主要就是下面这个。而boundChannels就是一个ConcurrentMap

/**
 * 根据地址把LocalChannel注册到 {@link boundChannels} 中
 **/
final class LocalChannelRegistry {
    private static final ConcurrentMap<LocalAddress, Channel> boundChannels = PlatformDependent.newConcurrentHashMap();
/**
 * @param channel LocalChannel 需要绑定的通道
 * @param oldLocalAddress 原先已经注册好的通道地址
 * @param localAddress 通道当前地址
 **/
    static LocalAddress register(
            Channel channel, LocalAddress oldLocalAddress, SocketAddress localAddress) {
        if (oldLocalAddress != null) {
            throw new ChannelException("already bound");
        }
        if (!(localAddress instanceof LocalAddress)) {
            throw new ChannelException("unsupported address type: " + StringUtil.simpleClassName(localAddress));
        }

        LocalAddress addr = (LocalAddress) localAddress;
        if (LocalAddress.ANY.equals(addr)) {
            addr = new LocalAddress(channel);
        }

        Channel boundChannel = boundChannels.putIfAbsent(addr, channel);
        if (boundChannel != null) {
            throw new ChannelException("address already in use by: " + boundChannel);
        }
        return addr;
    }
....
}

通过 LocalChannelRegistry#register 首先先判断是否已经注册过该通道,若是则已存在的错误,然后判断类型,接着判断该通道的地址 localAddress 是否已经初始化了,若没有则产生一个,产生的算法是根据 Channelhashcode值 取2字节产生的正数值端口,算法不难,这里不做深究,有兴趣的可以看 io.netty.channel.local.LocalAddress#LocalAddress(io.netty.channel.Channel) ,然后把该端口值与该 Channel 进行写进 boundChannelsConcurrentMap<LocalAddress, Channel> 中,接着返回端口值绑定到 LocalChannel 中.

同样的,我把LocalClient的调用栈也抓了出来。

"Thread-1@1364" prio=5 tid=0x16 nid=NA runnable
  java.lang.Thread.State: RUNNABLE
      at io.netty.bootstrap.Bootstrap.doConnect0(Bootstrap.java:160)
      at io.netty.bootstrap.Bootstrap.doConnect(Bootstrap.java:141)
      at io.netty.bootstrap.Bootstrap.connect(Bootstrap.java:115)
      at com.nettytest.AlexLocalClient.start(AlexLocalClient.java:32)
      at com.nettytest.Demo.startLocalClient(Demo.java:22)
      at com.nettytest.Demo.access$100(Demo.java:8)
      at com.nettytest.Demo$2.run(Demo.java:40)
      at java.lang.Thread.run(Thread.java:745)
"localEventLoopGroup-3-1@1398" prio=10 tid=0x18 nid=NA runnable
  java.lang.Thread.State: RUNNABLE
      at io.netty.channel.local.LocalChannel$LocalUnsafe.connect(LocalChannel.java:337)
      at io.netty.channel.DefaultChannelPipeline$HeadContext.connect(DefaultChannelPipeline.java:1089)
      at io.netty.channel.AbstractChannelHandlerContext.invokeConnect(AbstractChannelHandlerContext.java:543)
      at io.netty.channel.AbstractChannelHandlerContext.connect(AbstractChannelHandlerContext.java:528)
      at io.netty.channel.AbstractChannelHandlerContext.connect(AbstractChannelHandlerContext.java:510)
      at io.netty.channel.DefaultChannelPipeline.connect(DefaultChannelPipeline.java:909)
      at io.netty.channel.AbstractChannel.connect(AbstractChannel.java:203)
      at io.netty.bootstrap.Bootstrap$2.run(Bootstrap.java:165)
      at io.netty.channel.local.LocalEventLoop.run(LocalEventLoop.java:33)
      at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:111)
      at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
      at java.lang.Thread.run(Thread.java:745)

从堆栈中我们可以知道从Client的Bootstrap 中通过一层层的调用,到最后的LocalChannel$LocalUnsafe.connect的方法。下面是当前执行方法。

private class LocalUnsafe extends AbstractUnsafe {

        public void connect(final SocketAddress remoteAddress,
                SocketAddress localAddress, final ChannelPromise promise) {
            if (!promise.setUncancellable() || !ensureOpen(promise)) {
                return;
            }

            if (state == State.CONNECTED) {
                Exception cause = new AlreadyConnectedException();
                safeSetFailure(promise, cause);
                pipeline().fireExceptionCaught(cause);
                return;
            }
            //ChannelPromise promise的意义是表示该通道时能够writable ,其报错的意思是该通道是由阻塞的{@link SocketChannel} 创建的
            if (connectPromise != null) {
                throw new ConnectionPendingException();
            }

            connectPromise = promise;

            if (state != State.BOUND) {
                // Not bound yet and no localAddress specified - get one.
                if (localAddress == null) {
                    localAddress = new LocalAddress(LocalChannel.this);
                }
            }

            if (localAddress != null) {
                try {
                    doBind(localAddress);
                } catch (Throwable t) {
                    safeSetFailure(promise, t);
                    close(voidPromise());
                    return;
                }
            }

            Channel boundChannel = LocalChannelRegistry.get(remoteAddress);
            if (!(boundChannel instanceof LocalServerChannel)) {
                Exception cause = new ConnectException("connection refused: " + remoteAddress);
                safeSetFailure(promise, cause);
                close(voidPromise());
                return;
            }

            LocalServerChannel serverChannel = (LocalServerChannel) boundChannel;
            peer = serverChannel.serve(LocalChannel.this);
        }
    }

代码先是判断当前LocalClient的状态,确定会不会重连,会不会是堵塞状态。当状态时State.BOUND时,然后new的时候会根据hashcode 产生PORT,原理类似上面的 LocalAddress 构造函数一样。然后根据远程端口获取到 LocalServerChannel ,最后对该Channel 进行监听。具体下面讲解这个server.serve()。

public class LocalServerChannel extends AbstractServerChannel {
    private final Queue<Object> inboundBuffer = new ArrayDeque<Object>();
......
    LocalChannel serve(final LocalChannel peer) {
        final LocalChannel child = new LocalChannel(this, peer);
        //当开始运行循环时 的inEventLoop()的注释意义是
        /**
         * Return {@code true} if the given {@link Thread} is executed  
         * in the event loop, {@code false} otherwise.
         */
        if (eventLoop().inEventLoop()) {
            serve0(child);
        } else {
        //初始绑定
            eventLoop().execute(new Runnable() {
              @Override
              public void run() {
                serve0(child);
              }
            });
        }
        return child;
    }

/**
 *开始 serverChannel 开始读取数据
 */
 private void serve0(final LocalChannel child) {
     //把clientChannel 添加到ServerChannel 里面去
        inboundBuffer.add(child);
        if (acceptInProgress) {
            acceptInProgress = false;

            readInbound();
        }
    }



/**
* 不断读取队列里的通道进行监听读取数据
**/
private void readInbound() {
        RecvByteBufAllocator.Handle handle = unsafe().recvBufAllocHandle();
        handle.reset(config());
        ChannelPipeline pipeline = pipeline();
        do {
            //对存储在inboundBuffer 的LocalChannel 进行获取 
            Object m = inboundBuffer.poll();
            if (m == null) {
                break;
            }
            //不断循环到Channel 读取数据
            pipeline.fireChannelRead(m);
        } while (handle.continueReading());
        //读取完成
        pipeline.fireChannelReadComplete();
    }

}

LocalServerChannelserve函数先判断是否已经在运行,若不是则开始对它进行监听.监听首先把它放进 LocalServerChannel 的监听进入队列 inboundBuffer ,然后开始对队列里的通道进行监听读取数据.

需要注意的地方

从上面的分析来看,LocalChannel是依赖于这个中央的注册表的,而这个注册表又是以channel的id作为key的,那么这个channel id的唯一性就非常重要,如果local client的数目非常多,那么就有可能发生channel id冲突的情况,导致你的channel 在注册表中注册失败,继而导致connect失败或者是bind失败。

其实netty的作者也注意到了这个问题,给出了解决方案。注意到这个PR的target版本号是5.0.0.Alpha1,而作者另外的一个backporttarget是4.1.0.Beta1。

猜你喜欢

转载自blog.csdn.net/lzcaqde/article/details/81775638