LocalChannel介绍
LcoalChannel是Netty提供的用来在同一个JVM内部实现client和server之间通信的transport。它的实现主要是通过内存里的对象作为通信介质,不会像NIO下的channel,会占用一个文件描述符;因此使用它不会影响到你系统上的打开文件数,也就不会影响到你系统所能管理的连接数了。对于在同一个JVM内部使用netty的机制进行通信的话,相对来说算轻量级的。
示例
工作中的使用还是比较负载的,我在这里写了一个简单的例子,下面的讨论都是参照这个例子的。
可以看到使用方式跟使用其他的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
是否已经初始化了,若没有则产生一个,产生的算法是根据 Channel
的 hashcode
值 取2字节产生的正数值端口,算法不难,这里不做深究,有兴趣的可以看 io.netty.channel.local.LocalAddress#LocalAddress(io.netty.channel.Channel)
,然后把该端口值与该 Channel
进行写进 boundChannels
的 ConcurrentMap<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();
}
}
LocalServerChannel
的 serve
函数先判断是否已经在运行,若不是则开始对它进行监听.监听首先把它放进 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。