1. 脚踏实地的Netty源码研究笔记——开篇

1.1. Netty介绍

Netty是一个老牌的高性能网络框架。在众多开源框架中都有它的身影,比如:grpc、dubbo、seata等。

里面有着非常多值得学的东西:

  • I/O模型

  • 内存管理

  • 各种网络协议的实现:http、redis、websocket等等

  • 各种各样有趣的技巧的实现:异步、时间轮、池化、内存泄露探测等等。

  • 代码风格、设计思想、设计原则等。

1.2. 源码分析方法

我一般是这样进行源码分析的:

  1. 首先是纵向,通过官方提供的demo,进行debug,并记录在一个完整的生命周期下的调用链上,会涉及到哪些组件。

  2. 然后对涉及到的组件拿出来,找出它们的顶层定义(接口、抽象类)。通过其模块/包的划分类注释定义的方法及其注释,来大致知晓每个组件是做什么的,以及它们在整个框架中的位置是怎样的。

  3. 第二步完成后,就可以对第一步的调用链流程、步骤、涉及到的组件,进行归纳、划分,从而做到心中有数,知道东南西北了。

  4. 之后就是横向,对这些归纳出来的组件体系,逐个进行分析。

  5. 在分析每个组件体系的时候,也是按照先纵向,再横向的步骤:

    1. 首先是纵向:找出该组件体系中的核心顶层接口、类,然后结合其的所有实现类,捋出继承树,然后弄清楚每个类做的是啥,它是怎么定义的,同一层级的不同实现类之间的区别大致是什么,必要的话,可以将这个继承树记下来,在心中推算几遍。

    2. 然后是横向:将各个类有选择性地拿出来分析。

当然,所谓的纵向,横向,这两个过程实际是互相交织的,也就是说整个流程不一定就分为前后两半:前面一半都是纵向,后面一半都是横向。

通过纵向的分析,我们能发现整个框架可以分成大致哪几个部分,以及有

1.3. 分析前的准备

  1. 首先在本地建一个对应的分析学习用的项目,比如:learn_netty,用maven管理依赖
  2. 然后在maven仓库,中找到我们需要的依赖,比如这里我用的是最新的:
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.77.Final</version>
</dependency>
  1. 将官方提供的demo代码,导入到项目中。
  2. 学习项目搭建好之后,就尝试编译、运行,没问题后,就命令行mvn dependency:sources命令(或者通过IDE)来下载依赖的源代码。
  3. 可选:在github上,将项目同时clone到本地,如果分析中发现问题或者自己有些优化建议,可以尝试为分析的项目贡献代码。

1.4. 分析示例的代码

以一个简单的EchoServer、EchoClient来研究。

public class EchoServer {
private final int port; public EchoServer(int port) {
this.port = port;
} public static void main(String[] args) throws Exception {
new EchoServer(8083).start();
} public void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(serverHandler);
}
}); ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));
ctx.write(in);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
public class EchoClient {
public static void main(String[] args) throws Exception {
connect("127.0.0.1", 8083);
} public static void connect(String host, int port) throws Exception {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.group(group)
.channel(NioSocketChannel.class).remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture f = bootstrap.connect();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
} @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty Sockets!", CharsetUtil.UTF_8));
} @Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println(msg.toString(CharsetUtil.UTF_8));
}
}

1.5. 开始分析

分别启动EchoServer、EchoClient,在两个ChannelFuture的位置打断点。

1.5.1. EchoServer启动调用链

进入ServerBootstrapbind方法,发现该方法定义在父类AbstractBootstrap中:

    public ChannelFuture bind() {
validate();
SocketAddress localAddress = this.localAddress;
if (localAddress == null) {
throw new IllegalStateException("localAddress not set");
}
return doBind(localAddress);
}

接着来看doBind方法,发现也在AbstractBootstrap中:

    private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
} if (regFuture.isDone()) {
// At this point we know that the registration was complete and successful.
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered(); doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}

发现doBind中主要做了两件事:

  1. initAndRegister(初始化Channel并注册到EventLoop中),这个操作是异步操作,立即返回该操作对应的句柄。

  2. 拿到initAndRegister操作的句柄后,对其进行检查。

    1. 如果initAndRegister已完成那么立即进行doBind0操作(实际的bind操作),并返回doBind0操作对应的句柄。

    2. 如果initAndRegister还没有完成,那么就将doBind0操作异步化:initAndRegister操作完成后再触发doBind0

然后我们先看initAndRegister,它同样在AbstractBootstrap中:

    final ChannelFuture initAndRegister() {
Channel channel = null;
try {
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
if (channel != null) {
// channel can be null if newChannel crashed (eg SocketException("too many open files"))
channel.unsafe().closeForcibly();
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
} ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
} // If we are here and the promise is not failed, it's one of the following cases:
// 1) If we attempted registration from the event loop, the registration has been completed at this point.
// i.e. It's safe to attempt bind() or connect() now because the channel has been registered.
// 2) If we attempted registration from the other thread, the registration request has been successfully
// added to the event loop's task queue for later execution.
// i.e. It's safe to attempt bind() or connect() now:
// because bind() or connect() will be executed *after* the scheduled registration task is executed
// because register(), bind(), and connect() are all bound to the same thread. return regFuture;
}

忽略对异常的处理,看到有三个步骤:

  1. 使用工厂创建一个channel

  2. 对这个channel进行init:由子类实现。

  3. 将创建的channel注册(register)到EventLoopGroup中,异步操作,将该操作对应的句柄返回。

看完了initAndRegister后,在回来看doBind0

    private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) { // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (regFuture.isSuccess()) {
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}

发现在doBind0中,最终是通过调用channelbind方法来完成的。而这个动作是包裹成了一个任务,提交给了channel所注册到的eventloop,由它来执行。

1.5.2. EchoClient启动调用链

首先进入Bootstrapconnect方法中:

    public ChannelFuture connect() {
validate();
SocketAddress remoteAddress = this.remoteAddress;
if (remoteAddress == null) {
throw new IllegalStateException("remoteAddress not set");
} return doResolveAndConnect(remoteAddress, config.localAddress());
}

同样忽略validate,直接看doResolveAndConnect

    private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel(); if (regFuture.isDone()) {
if (!regFuture.isSuccess()) {
return regFuture;
}
return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
// Directly obtain the cause and do a null check so we only need one volatile read in case of a
// failure.
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
}
}
});
return promise;
}
}

我们发现Bootstrap::doResolveAndConnectAbstractBootstrap::doBind类似。意思也是说,在initAndRegister完成channel的创建、初始化、绑定到EventLoop之后再进行实际的操作doResolveAndConnect0

于是我们来看doResolveAndConnect0:


private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
final SocketAddress localAddress, final ChannelPromise promise) {
try {
final EventLoop eventLoop = channel.eventLoop();
AddressResolver<SocketAddress> resolver;
try {
resolver = this.resolver.getResolver(eventLoop);
} catch (Throwable cause) {
channel.close();
return promise.setFailure(cause);
} if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
// Resolver has no idea about what to do with the specified remote address or it's resolved already.
doConnect(remoteAddress, localAddress, promise);
return promise;
} final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress); if (resolveFuture.isDone()) {
final Throwable resolveFailureCause = resolveFuture.cause(); if (resolveFailureCause != null) {
// Failed to resolve immediately
channel.close();
promise.setFailure(resolveFailureCause);
} else {
// Succeeded to resolve immediately; cached? (or did a blocking lookup)
doConnect(resolveFuture.getNow(), localAddress, promise);
}
return promise;
} // Wait until the name resolution is finished.
resolveFuture.addListener(new FutureListener<SocketAddress>() {
@Override
public void operationComplete(Future<SocketAddress> future) throws Exception {
if (future.cause() != null) {
channel.close();
promise.setFailure(future.cause());
} else {
doConnect(future.getNow(), localAddress, promise);
}
}
});
} catch (Throwable cause) {
promise.tryFailure(cause);
}
return promise;
}

我们可以看出,doResolveAndConnect0正如其名:

  1. 首先获取channel所绑定的eventloop所对应的AddressResolver(从AddressResolverGroup)中拿。
  2. 拿到AddressResolver之后,如果它不知道该怎么处理给定的需要连接的地址,或者说这个地址已经被其解析过,那么就直接doConnect。否则使用AddressResolver来解析需要连接的地址(异步操作),并将doConnect操作异步化。

先暂时忽略AddressResolver,我们来看doConnect

    private static void doConnect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) { // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
final Channel channel = connectPromise.channel();
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (localAddress == null) {
channel.connect(remoteAddress, connectPromise);
} else {
channel.connect(remoteAddress, localAddress, connectPromise);
}
connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
});
}

我们看到doConnect和之前的doBind0一样,最终也是调用channel的方法,并且将实际的执行交给channel绑定的eventloop来执行。

1.6. 总结

就目前debug的调用链上,我们发现涉及到的组件有:

  • Bootstrap系列:脚手架,提供给开发人员使用,类似Spring的ApplicationContext
  • Channel系列:连接通道
  • EventLoopGroup、EventLoop系列:执行器与事件驱动循环,IO模型。
  • AddressResolverGroup、AddressResolver系列:地址解析器
  • netty自定义的Future、Promise相关:异步化的基础

我们发现netty的操作全程是异步化的,并且最终要解开其原理的庐山真面目,关键还在于提及的eventloop、channel。

此阶段的纵向分析,目前只解开一隅,待我们看看eventloop、channel后,再来解开更大的谜题。

脚踏实地的Netty源码研究笔记——开篇的更多相关文章

  1. Netty源码研究笔记(4)——EventLoop系列

    1. Netty源码研究笔记(4)--EventLoop系列 EventLoop,即事件驱动,它是Netty的I/O模型的抽象,负责处理I/O事件.任务. 不同的EventLoop代表着不同的I/O模 ...

  2. Netty源码阅读(一) ServerBootstrap启动

    Netty源码阅读(一) ServerBootstrap启动 转自我的Github Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...

  3. 【Netty源码学习】DefaultChannelPipeline(三)

    上一篇博客中[Netty源码学习]ChannelPipeline(二)我们介绍了接口ChannelPipeline的提供的方法,接下来我们分析一下其实现类DefaultChannelPipeline具 ...

  4. 【Netty源码分析】发送数据过程

    前面两篇博客[Netty源码分析]Netty服务端bind端口过程和[Netty源码分析]客户端connect服务端过程中我们分别介绍了服务端绑定端口和客户端连接到服务端的过程,接下来我们分析一下数据 ...

  5. 【Netty源码分析】客户端connect服务端过程

    上一篇博客[Netty源码分析]Netty服务端bind端口过程 我们介绍了服务端绑定端口的过程,这一篇博客我们介绍一下客户端连接服务端的过程. ChannelFuture future = boos ...

  6. 【Netty源码分析】ChannelPipeline(二)

    在上一篇博客[Netty源码学习]ChannelPipeline(一)中我们只是大体介绍了ChannelPipeline相关的知识,其实介绍的并不详细,接下来我们详细介绍一下ChannelPipeli ...

  7. 【Netty源码学习】ChannelPipeline(一)

    ChannelPipeline类似于一个管道,管道中存放的是一系列对读取数据进行业务操作的ChannelHandler. 1.ChannelPipeline的结构图: 在之前的博客[Netty源码学习 ...

  8. 【Netty源码学习】ServerBootStrap

    上一篇博客[Netty源码学习]BootStrap中我们介绍了客户端使用的启动服务,接下来我们介绍一下服务端使用的启动服务. 总体来说ServerBootStrap有两个主要功能: (1)调用父类Ab ...

  9. 【Netty源码解析】NioEventLoop

    上一篇博客[Netty源码学习]EventLoopGroup中我们介绍了EventLoopGroup,实际说来EventLoopGroup是EventLoop的一个集合,EventLoop是一个单线程 ...

随机推荐

  1. 顺利通过EMC实验(19)

  2. 在VisualStudio调试器中使用内存窗口和查看内存分布

    调试模式下内存窗口的使用 在调试期间,"内存"窗口显示应用使用的内存空间.调试器窗口(如"监视"."自动"."局部变量" ...

  3. .map() vs .forEach() vs for() 如何选择?

    访问原文地址 .map() vs .forEach() vs for() 笔者说,自己基本没怎么用过for()来遍历,主要是用.forEach(). 但是总是会被很多朋友说,这些人认为for()的速度 ...

  4. 利用Charles做代理测试电脑上写的H5页面

    做H5页面的同学可能经常会遇到一个场景,就是电脑上调试好的页面怎么在手机上访问测试呢? 下面就介绍一种自己经常使用的方式,利用Charles代理软件来实现! 安装Charles 直接去官网下载对应的系 ...

  5. python-逆序输出

    输入一行字符串,然后对其进行如下处理. 输入格式: 字符串中的元素以空格或者多个空格分隔. 输出格式: 逆序输出字符串中的所有元素.然后输出原列表.然后逆序输出原列表每个元素,中间以1个空格分隔.注意 ...

  6. 【uniapp 开发】智能温控开关 (环状图)

    index.vue <template> <view> <view class="qiun-columns"> <uCharts id=& ...

  7. 用SimpleDateFormat求出哪天是星期几,如2008-11-11

    题目5: 巧妙利用SimpleDateFormat求出: 2008-11-11是星期几?import java.text.ParseException;import java.text.SimpleD ...

  8. 关于mysql使用utf8编码在cmd窗口无法添加中文数据的问题以及解决 方法二

    如果非要用cmd窗口的话,那么可以加这句话,set names gbk:

  9. android的布局xml文件如何添加注释?

    xml布局文件如图添加注释后报错,错误内容如下: 上网查阅xml添加注释的语法规则: XML 中的注释 在 XML 中编写注释的语法与 HTML 的语法很相似: <!--This is a co ...

  10. ThingsBoard安装编译搭建环境踩坑记录

    1.首先从github拉下来项目,我们采用源码编译的方式部署 git clone https://github.com/thingsboard/thingsboard.git 2.切换分支 git c ...