前言

前段时间,从头开始将netty源码了解了个大概,但都是原理上理解。刚好博主对dubbo框架了解过一些,这次就以dubbo框架为例,详细看看dubbo这种出色的开源框架是如何使用netty的,又是如何与框架本身逻辑进行融合的。

本文分成两大部分,一部分是dubbo服务端对netty的封装,一部分是dubbo客户端对netty的封装,而每部分都分netty初始化和调用两个阶段,下面进入正题。

一、Dubbo服务端

Dubbo服务端对netty的调用始于服务导出,服务导出的流程之前文章中有介绍,详见【https://www.cnblogs.com/zzq6032010/p/11275478.html】,在服务导出的最后,会调用DubboProtocol#openServer方法,就是在此方法中完成的netty服务端的初始化(本文均以配置了netty通信为前提),下面就以该处作为起点探寻。

1、服务端初始化

openServer方法源码如下,主体逻辑是先获取了address作为key---ip:port格式的字符串,然后做了一个双重检查,server不存在则调createServer创建一个放入serverMap中。到这里我们可以知道,dubbo服务提供者中一个ip+端口对应一个nettyServer,所有的nettyServer统一放在一个ConcurrentHashMap中维护了起来。但其实通常情况下,一个服务提供者的服务器,只会暴露一个端口给dubbo用,故虽然用Map存起来,但一般只会有一个nettyServer。此处还要注意,dubbo中是暴露一个服务提供者执行一次export方法,即一个服务提供者接口触发一次openServer方法、对应一个nettyServer,下面跟进server的创建过程。

 private void openServer(URL url) {
// find server.
String key = url.getAddress();
//client can export a service which's only for server to invoke
boolean isServer = url.getParameter(IS_SERVER_KEY, true);
if (isServer) {
ProtocolServer server = serverMap.get(key);
if (server == null) {
synchronized (this) {
server = serverMap.get(key);
if (server == null) {
serverMap.put(key, createServer(url));
}
}
} else {
// server supports reset, use together with override
server.reset(url);
}
}
}

openServer调用的方法栈如下所示:

进入NettyTransporter的bind方法,NettyTransporter一共有两个方法-bind和connect,前者初始化服务端时调用,后者初始化客户端时触发,源码如下:

 public class NettyTransporter implements Transporter {
public static final String NAME = "netty";
@Override
public RemotingServer bind(URL url, ChannelHandler listener) throws RemotingException {
return new NettyServer(url, listener);
}
@Override
public Client connect(URL url, ChannelHandler listener) throws RemotingException {
return new NettyClient(url, listener);
}
}

下面看NettyServer如何与netty关联起来的。先看下NettyServer的类图:

有经验的园友看到类图估计就能猜到,此处是源码框架中常用的分层抽象,AbstractServer作为一个模板的抽象,继承它之后可以扩展出其他类型的通信,比如MinaServer、GrizzlyServer。下面回到本文的主角NettyServer,看看其构造器:

 public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
super(ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME), ChannelHandlers.wrap(handler, url));
}

设置了一下url中的线程名参数,将handler和url进行了封装,然后调用了父类AbstractServer的构造器。

到这里,需要确定好入参的handler类型和传给父类构造器的handler类型。NettyServer构造器入参ChannelHandler是在HeaderExchanger#bind中封装的,方式如下:

 public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
}

再进一步,bind方法入参ExchangeHandler的实现类要追溯到DubboProtocol,是其成员变量requestHandler如下:

private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
// 省略若干个重写的方法逻辑
}

至此,NettyServer构造器入参ChannelHandler的类型已经确认了,其内部最终实现是DubboProtocol中的ExchangeHandlerAdapter,外部封装了一层HeaderExchangeHandler,又封装了一层DecodeHandler。简图如下:

搞清楚NettyServer构造器入参的ChannelHandler之后,下面跟进ChannelHandlers.wrap方法,最终封装方法如下:

 protected ChannelHandler wrapInternal(ChannelHandler handler, URL url) {
return new MultiMessageHandler(new HeartbeatHandler(ExtensionLoader.getExtensionLoader(Dispatcher.class)
.getAdaptiveExtension().dispatch(handler, url)));
}

而Dispatcher默认是AllDispatcher,其dispatch方法如下:

 public ChannelHandler dispatch(ChannelHandler handler, URL url) {
return new AllChannelHandler(handler, url);
}

至此,ChannelHandlers.wrap方法执行完后得到的ChannelHandler结构如下,采用的是装饰器模式,层层装饰。

了解清楚了wrap方法,下面回到主线,进入AbstractServer的构造器:

 public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
// 1、调用父类构造器将这两个变量存起来,最终是存在了AbstractPeer中
super(url, handler);
// 2、设置两个address
localAddress = getUrl().toInetSocketAddress();
String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
if (url.getParameter(ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
bindIp = ANYHOST_VALUE;
}
bindAddress = new InetSocketAddress(bindIp, bindPort);
this.accepts = url.getParameter(ACCEPTS_KEY, DEFAULT_ACCEPTS);
this.idleTimeout = url.getParameter(IDLE_TIMEOUT_KEY, DEFAULT_IDLE_TIMEOUT);
try {
// 3、完成netty源码的调用-开启netty服务端
doOpen();
} catch (Throwable t) {
// 省略异常处理
}
// 4、获取/创建线程池
executor = executorRepository.createExecutorIfAbsent(url);
}

1/2的逻辑较简单,3和4才是重点,下面进入3处的doOpen方法,doOpen方法在AbstractServer中是抽象方法,所以要到其子类NettyServer中看:

 protected void doOpen() throws Throwable {
// 这里可以看到熟悉的netty代码了
bootstrap = new ServerBootstrap();
// bossGroup一个线程
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("NettyServerBoss", true));
// workerGroup线程数取的CPU核数+1与32的小值
workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
new DefaultThreadFactory("NettyServerWorker", true));
// ***1、此处将NettyServer封装进NettyServerHandler中,实现了netty和dubbo的连接
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
channels = nettyServerHandler.getChannels();
// netty封装
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
ch.pipeline().addLast("negotiation",
SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
}
ch.pipeline()
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
.addLast("handler", nettyServerHandler);
}
});
// 绑定IP和端口,此处用到的就是AbstractServer中的bindAddress变量
ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
}

标星号***的1处就是关键点,看下面的pipeline.addLast可知,存放着dubbo逻辑的NettyServer被封装进了NettyServerHandler中,进而放入了pipeline里面,当有客户端连接的时候就会触发这个nettyServerHandler中的对应方法,进入dubbo的接口调用逻辑。从dubbo功能到netty框架之间的连接者就是这个NettyServerHandler类。NettyServer中封装了一个线程池,即一个客户端连接过来之后,服务端用一个线程池来接收处理这个客户端的一系列请求,即在netty原有线程模型基础上又加了一层线程池。

4中的executorRepository.createExecutorIfAbsent(url)用于生成线程池,此处为服务端,点进去源码可以发现在dubbo的服务端,一个port端口对应一个线程池,而且此处未设置特殊的参数,故走ThreadPool的默认类型fixed,即FixedThreadPool的getExecutor方法:

 public Executor getExecutor(URL url) {
String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);
int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,
queues == 0 ? new SynchronousQueue<Runnable>() :
(queues < 0 ? new LinkedBlockingQueue<Runnable>()
: new LinkedBlockingQueue<Runnable>(queues)),
new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
}

该方法可关注两点:线程数默认为200个,阻塞队列由于queues==0采用的是SynchronousQueue。这个线程池初始化之后干啥用?搜遍了调用关系,发现只有在NettyServer进行重置或者关闭时才会操作这个线程池。但理论上讲不通啊,总不能创建了一个线程池之后只是为了关闭它。且往下看。

2、服务端调用

其实服务端的线程池这里给博主看源码一个启发,注意,此处是去仓库获取一个线程池的引用(即executorRepository.createExecutorIfAbsent(url)),而仓库创建了线程池是将其缓存了起来,而缓存之后的线程池引用还可以暴露给其他地方,在其他地方执行线程池的execute方法。具体在这里,最终是在AllChannelHandler中调用的线程池,比如connected方法,如下所示,getExecutorService方法就是去仓库中获取了服务端的这个线程池,封装出一个ChannelEventRunnable丢给线程池执行。而服务端接收到请求时的received方法也是同样的处理流程。

 public void connected(Channel channel) throws RemotingException {
ExecutorService executor = getExecutorService();
try {
executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
} catch (Throwable t) {
throw new ExecutionException("connect event", channel, getClass() + " error when process connected event .", t);
}
}

下面结合netty的调用流程对服务调用时的处理流程做一个梳理:在之前讲解netty的run方法一文【https://www.cnblogs.com/zzq6032010/p/13122483.html】中有过介绍,netty的ServerBootstrap启动后,会开启bossGroup中的那个线程(即Reactor线程),一直执行run方法。而当有客户端要连接时,select方法会从操作系统获取到一个连接事件,Reactor线程会为该连接方创建一个NioSocketChannel,并从workerGroup中挑选一个线程,运行run方法,该线程用于处理服务端与这个客户端的后续通讯。而此处添加进pipeline中的nettyServerHandler会在客户端传来读写请求时触发对应的方法。最终调用到上述AllChannelHandler中的对应方法,用线程池执行后续业务逻辑。

二、Dubbo的客户端

1、客户端初始化

dubbo客户端初始化时会调用RegistryProtocol的refer方法,几经周折,最后到了DubboProtocol的protocolBindingRefer方法,如下,其中第5行调用的getClients方法是与netty整合的重点,即生成连接服务端的客户端。注意此处是在客户端中每一个引入的服务接口对应一个DubboInvoker。

 public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url); // create rpc invoker.
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers); // 为每个invoker生成对应的nettyClient
invokers.add(invoker); return invoker;
}

继续跟进,会进入NettyTransporter的connect方法,到这里应该会很熟悉,因为服务端初始化时调用的是该类下面的bind方法。bind方法初始化的是NettyServer对象,而connect初始化的是NettyClient对象。

public class NettyTransporter implements Transporter {

    public static final String NAME = "netty";

    @Override
public RemotingServer bind(URL url, ChannelHandler listener) throws RemotingException {
return new NettyServer(url, listener);
} @Override
public Client connect(URL url, ChannelHandler listener) throws RemotingException {
return new NettyClient(url, listener);
} }

NettyClient的类图结构与NettyServer类似:

下面看NettyClient的构造器:

 public NettyClient(final URL url, final ChannelHandler handler) throws RemotingException {
// you can customize name and type of client thread pool by THREAD_NAME_KEY and THREADPOOL_KEY in CommonConstants.
// the handler will be warped: MultiMessageHandler->HeartbeatHandler->handler
super(url, wrapChannelHandler(url, handler));
}

直接调用了父类构造器,其中wrapChannelHandler方法与NettyServer中的一样,不再赘述。下面看父类构造器:

 public AbstractClient(URL url, ChannelHandler handler) throws RemotingException {
super(url, handler); needReconnect = url.getParameter(Constants.SEND_RECONNECT_KEY, false);
// 1、初始化客户端线程池
initExecutor(url); try {
doOpen(); // 2、创建客户端的Bootstrap
} catch (Throwable t) {
// 省略异常处理
}
try {
// 3、连接Netty服务端
connect();
} catch (RemotingException t) {
// 省略异常处理
} catch (Throwable t) {
close();
// 抛异常
}
}

主要有三步,已经在上面标出,下面分别跟进这三个方法。

1)、initExecutor方法直接先将线程池类型添加进url中,客户端默认是Cached类型,所以在调用executorRepository.createExecutorIfAbsent(url)时会进入CachedThreadPool中。

 private void initExecutor(URL url) {
url = ExecutorUtil.setThreadName(url, CLIENT_THREAD_POOL_NAME);
url = url.addParameterIfAbsent(THREADPOOL_KEY, DEFAULT_CLIENT_THREADPOOL);
executor = executorRepository.createExecutorIfAbsent(url);
}

CachedThreadPool代码如下,可见是创建的核心线程数为0最大线程数无上限的线程池,阻塞队列默认SynchronousQueue。

 public class CachedThreadPool implements ThreadPool {

     @Override
public Executor getExecutor(URL url) {
String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);
int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
int alive = url.getParameter(ALIVE_KEY, DEFAULT_ALIVE);
return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,
queues == 0 ? new SynchronousQueue<Runnable>() :
(queues < 0 ? new LinkedBlockingQueue<Runnable>()
: new LinkedBlockingQueue<Runnable>(queues)),
new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
}
}

2)、doOpen方法

实现逻辑在NettyClient中,都是正常的封装,变化的地方是将NettyClientHandler放入pipeline中。注意此处只是将Bootstrap初始化,但并未触发与服务端的连接。

3)、connect方法

该方法最终在NettyClient的doConnect方法中调用了bootstrap的connect方法,完成与服务端的连接。

2、客户端调用

在消费端调用服务端接口或者接收到服务端的返回结果时,均会触发NettyClientHandler中对应的方法,而此处跟NettyServerHandler类似,最终都是在AllChannelHandler中获取之前创建的客户端线程池(Cached类型的),用该线程池进行后续操作。

最后,来一张示意图做个调用的总结:

Dubbo源码学习之-通过源码看看dubbo对netty的使用的更多相关文章

  1. 通过源码了解ASP.NET MVC 几种Filter的执行过程

    一.前言 之前也阅读过MVC的源码,并了解过各个模块的运行原理和执行过程,但都没有形成文章(所以也忘得特别快),总感觉分析源码是大神的工作,而且很多人觉得平时根本不需要知道这些,会用就行了.其实阅读源 ...

  2. Linux下通过源码编译安装程序

    本文简单的记录了下,在linux下如何通过源码安装程序,以及相关的知识.(大神勿喷^_^) 一.程序的组成部分 Linux下程序大都是由以下几部分组成: 二进制文件:也就是可以运行的程序文件 库文件: ...

  3. 通过源码了解ASP.NET MVC 几种Filter的执行过程 在Winform中菜单动态添加“最近使用文件”

    通过源码了解ASP.NET MVC 几种Filter的执行过程   一.前言 之前也阅读过MVC的源码,并了解过各个模块的运行原理和执行过程,但都没有形成文章(所以也忘得特别快),总感觉分析源码是大神 ...

  4. Kafka详解六:Kafka如何通过源码实现监控

    问题导读: 1.kafka的消费者组的消费偏移存储,kafka支持两个版本?        2.ConsumerOffsetChecker类的作用是什么?        3.Kafka如何通过源码实现 ...

  5. 通过源码编译安装VIM

    开发中使用的是Ubuntu 12.04 LTS,通过sudo apt-get install vim安装的版本较低,不支持YCM,所以,用源码编译并安装最新的Vim. 卸载旧版本的Vim: sudo ...

  6. 通过源码安装PostgresSQL

    通过源码安装PostgresSQL 1.1 下载源码包环境: Centos6.8 64位 yum -y install bison flex readline-devel zlib-devel yum ...

  7. 如何通过源码包的方式在linux安装python36

    背景: python34的安装非常简单,直接用yum就可以安装,但是安装最新版的python36通过yum方式是不行的,需要通过源码包进行安装 具体步骤如下: 1.安装openssl静态库[pip3安 ...

  8. 通过源码分析Java开源任务调度框架Quartz的主要流程

    通过源码分析Java开源任务调度框架Quartz的主要流程 从使用效果.调用链路跟踪.E-R图.循环调度逻辑几个方面分析Quartz. github项目地址: https://github.com/t ...

  9. 在centos6.7通过源码安装python3.6.7报错“zipimport.ZipImportError: can't decompress data; zlib not available”

    在centos6.7通过源码安装python3.6.7报错: zipimport.ZipImportError: can't decompress data; zlib not available 从 ...

随机推荐

  1. IO—》Properties类&序列化流与反序列化流

    Properties类 介绍:Properties 类表示了一个持久的属性集.Properties 可保存在流中或从流中加载.属性列表中每个键及其对应值都是一个字符串. Properties类特点: ...

  2. 面试(JS篇)

    1.js基本类型 Boolean,Number,String,Null,Undefined,Symbol. 2.null非对象,但是typeof null 返回的是Object,原因遗留下来的一个bu ...

  3. PHP timezone_offset_get() 函数

    ------------恢复内容开始------------ 实例 返回相对于 GMT 的时区偏移: <?php$tz=timezone_open("Asia/Taipei" ...

  4. Linux 如何以管理员身份运行终端

    如何以管理员身份在终端执行指令: 目录 如何以管理员身份在终端执行指令: 1. 以sudo 指令在其他指令前加上sudo 2. 以su 进入root权限,以管理员方式执行命令 设置root初始密码: ...

  5. HA模式下的java api访问要点

    在非HA架构的HDFS中,客户端要通过java接口调用HDFS时一般是在JobRunner的类中按照下面的方式: 因为nodename只有一个节点所以会在代码中显式的指明要连接哪一个节点:但是在HA模 ...

  6. SpringCloud启动异常Stopping service [Tomcat]

    问题场景: 领导让我搭建一套Jenkins实现自动化部署,项目是SpringCloud项目,配置的过程很顺利,给我提供了一台服务器做部署测试(服务器以前是做dev环境,很长时间没人用了) 我把所有项目 ...

  7. [转]Nginx介绍-反向代理、负载均衡

    原文:https://www.cnblogs.com/wcwnina/p/8728391.html 作者:失恋的蔷薇 1. Nginx的产生 没有听过Nginx?那么一定听过它的"同行&qu ...

  8. 在不同网段使用 VLAN 通信 - SVI,单臂路由

    在 VLAN 这篇文章中知道,设置 VLAN 目的是隔离大型的广播域,将其分成很小的广播域,从而更好的管理.但也就带来了一些问题:如流量不能在不同的 VLAN 间通信. 而为了解决这个问题,可以采用如 ...

  9. Linux常用命令之用户权限管理chmod、chown、chgrp、umask命令讲解

    这节课我们重点来学习权限管理命令,说到权限大家可能第一时间能想到的就是读.写.执行 rwx 三种权限,在正式讲解权限命令之前,先简单的介绍一下rwx权限对于文件和目录的不同含义. 权限字符 权限 对文 ...

  10. SqlServer 多表连接、聚合函数、模糊查询、分组查询应用总结(回归基础)

    --exists 结合 if else 以及 where 条件来使用判断是否有数据满足条件 select * from Class where Name like '%[1-3]班' if (not ...