最近周末也没啥事就学学Netty,同时打算写一些博客记录一下(写的过程理解更加深刻了)

本文主要从三个方法来呈现:Netty核心组件简介、Netty服务端创建、Netty启动过程源码分析

如果你对Netty有一定的了解, 那阅读起来应该会比较愉快

Netty核心组件简介

ByteBuf

缓冲区ByteBuf是对JDK NIO类库中ByteBuffer的增强

缓冲区直接连接通道两端( 通过通道发送数据时需要先转换为ByteBuf对象, 从通道直接获取的也是ByteBuf对象)

Channel和Unsafe

Channel聚合一组网络I/O操作 --- 读、写、客户端发起连接、关闭连接、链路关闭等

UnSafe接口辅助Channel实现I/O操作(不应有用户代码直接调用)

ChannelPipeline和ChannelHandler

ChannelHandler:负责处理I/O事件,每个ChannelHanlder对需要关注的I/O事件实现自己的处理逻辑,一般职责较单一,如解码Handler只做解码操作。

ChannelPipeline:一个ChannelPipeline由多个按一定顺序排列的ChannelHandler组成, I/O事件在pipeline中流动(入站事件从头到尾、出站事件从尾到头),每个handler会对事件进行处理。

NioEventLoop和NioEventLoopGroup

NioEventLoop: 事件循环(Reactor线程),负责监听多个通道的就绪状态,当通道就绪时产生相应的入站事件

NioEventLoopGroup:事件循环池(Reactor线程池),当新的通道被创建时,NioEventLoopGroup会为其分配一个事件循环,后续该通道的所有I/O操作都在该事件循环进行。

Future和Promise

这两个类是Netty对异步的支持,Promise用于设置异步操作结果(写),Future用于获取异步操作结果(读)。

Netty服务端创建

我们从搭建一个简单的服务端程序开始

下面是一个获取当前日期和时间的服务端程序:当客户端输入行为"today"时返回当天日期 "2020-12-11",输入行为"time"时返回当前时间 "03:11:11"。

   public static void main(String[] args) {
//1.线程池配置
ServerBootstrap bootstrap = new ServerBootstrap();
NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
NioEventLoopGroup childGroup = new NioEventLoopGroup(4);
bootstrap.group(parentGroup, childGroup);
//2.服务端Channel配置
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.option(ChannelOption.SO_BACKLOG, 1024); //3.子Channel配置
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Override
protected void initChannel(SocketChannel channel) throws Exception {
//解码器
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
channel.pipeline().addLast(new StringDecoder(StandardCharsets.UTF_8));
//业务handler
channel.pipeline().addLast(new BusinessHandler());
//编码器
channel.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8));
}
}); try {
ChannelFuture future = bootstrap.bind(7001).syncUninterruptibly();
future.channel().closeFuture().sync();
} catch (Exception e) {
//todo
}finally {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
} static class BusinessHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String s = (String) msg;
String ret = "";
if ("today".equals(s)) {
ret = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
} else if ("time".equals(s)) {
ret = new SimpleDateFormat("hh:mm:ss").format(new Date());
}
ret += "\r\n";
ctx.channel().writeAndFlush(ret);
}
}

整个应用搭建的过程很简单,归纳起来有四步

1.Reactor线程池配置

2.服务端Channel配置

3.子Channel配置(通过服务端通道创建的子通道)

4.绑定本地端口并启动服务

Reactor线程池配置

我们新建两个Reactor线程池parentGroup和childGroup

parentGroup是服务端通道使用,用于接受新的客户端连接(accept)

childGroup用于处理所有服务端通道创建子通道的网络I/O请求

ServerBootstrap bootstrap = new ServerBootstrap();
NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
NioEventLoopGroup childGroup = new NioEventLoopGroup(4);
bootstrap.group(parentGroup, childGroup);

服务端Channel配置

服务端Channel配置主要涉及:Channel类型、ChanelOption、AttributeKey(handler一般不用配置)

  1. Channel类型配置

    Channel的类型我们选用 NioServerSocketChannel -- 底层使用的是JDK NIO的 ServerSocketChannel.

bootstrap.channel(NioServerSocketChannel.class);
  1. 设置ChannelOption 和 AttributeKey

ChildOption:TCP选项, 如接受缓冲区大小(SO_RCVBUF)、发送缓冲区大小(SO_SNDBUF)、内核TCP连接队列大小(SO_BACKLOG)等

AttributeKey:附在Channel上的对象, 可以在多个ChannelHandler之间进行数据共享

bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
bootstrap.attr(AttributeKey.newInstance("TEST"), new Object());

备注:ChannelHandler 用于处理I/O事件,是通道所必须的。因为Netty提供了初始化客户端连接的handler(ServerBootstrapAcceptor),所以对于服务端Channel我们可以不用设置

Channel配置

Channel配置主要涉及:ChanelOption和AttributeKey、ChannelHandler

  1. 设置ChannelOption 和 AttributeKey

    针对每个Channel可以配置ChannelOption和AttributeKey,同服务端通道配置一样。

  2. ChannelHandler配置

    对于服务端Channel,Netty框架提供了用于接受连接的Handler,我们可以不用设置;但是对于服务端Channel创建的每个子Channel我们需要为其配置Handler,以处理I/O事件。

    首先:解码器是必须的。我们业务逻辑中流转的一般是对象,通过配置解码器将字节转换成Java对象(解码器同时需要处理TCP拆包、粘包)

    然后:自定义业务处理器用于处理具体的业务逻辑,如上面的BusinessHandler。

    最后:需要对结果进行返回时需要配置编码器,用于将输出对象编码成可用于通道传输的ByteBuf对象

    对于这个例子:

    LineBasedFrameDecoder和StringDecoder是解码器:将一行数据解码成Java中的String对象

    BusinessHandler是业务处理器:处理具体的业务逻辑(获取当前日期或者时间)

    StringEncoder是编码器:将String对象编码成ByteBuf对象,用于通道传输。

绑定本地端口并启动服务

配置就绪后直接绑定本地端口启动服务

ChannelFuture future = bootstrap.bind(7001).syncUninterruptibly();

到这里通过Netty创建一个服务端应用程序就完成了,下面我们从源码成面看看Netty的启动过程

Netty服务端启动过程源码分析

源码基于4.1分支:做了部分简化,只保留了核心逻辑

从bind方法开始

 public ChannelFuture bind(int inetPort) {
return bind(new InetSocketAddress(inetPort));
} public ChannelFuture bind(SocketAddress localAddress) {
this.validate();
return this.doBind(localAddress);
} private ChannelFuture doBind(final SocketAddress localAddress) {
//1. 初始化NioServerSocketChannel,并且注册到EventLoopGroup
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
//2.1. 注册失败,直接返回
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();
//2.2. 注册成功,直接bind本地端口
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);
//3.如果注册还未知(因为是异步操作),添加listener到regFuture对象上用于注册完成后进行回调处理
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
promise.setFailure(cause);
} else {
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}

整个bind方法比较简单, 核心逻辑都在doBind方法里面,doBind里的逻辑主要有三步

  1. initAndRegister:实例化ServerSocketChannel(这里是NioServerSocketChannel)并注册到事件循环(EventLoopGroup)

  2. 如果第一步失败,直接返回;如果注册成功,调用doBind方法绑定本地端口启动服务器

  3. 如果注册结果还未知(reg是异步操作),添加ChannelFutureListener到regFuture对象上用于注册完成后的回调处理

第二和第三个步都比较简单,我们主要需要看下第一步--initAndRegister

initAndRegister(初始化NioServerSocketChannel并注册到EventLoopGroup)

initAndRegister其实是个模版方法,也可以分成三步来分析

  1. 实例化,这里其实是通过基于反射的工厂方法实例化
  2. 初始化(由子类实现)
  3. 注册到EventLoopGroup
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
//1. 实例化,基于反射的工厂方法
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
//
}
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
//
}
return regFuture;
}

第一步和第三步这里我们不做展开,主要看下第二步init做了什么事

init(初始化通道)

下面是ServerBootstrap中的init方法的源码

void init(Channel channel) {
setChannelOptions(channel, newOptionsArray(), logger);
setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY)); ChannelPipeline p = channel.pipeline(); final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
}
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY); p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

归纳起来其实是把我们通过ServerBootstarp引导类配置的一些参填充到NioServerSocketChannel实例中去了,没有问题。

需要注意这里在socketChannel的pipeline中添加了一个ServerBootstrapAcceptor类型的handler(ServerBootstrapAcceptor用于初始化服务端接受的子通道,感兴趣的可以自己展开)

总结

通过对bind、doBind、initAndRegister、init的几个方法的分析,我们可以Netty的整个启动过程有个大致的认识

1.实例化并初始化NioServerSocketChannel

2.把初始化后的nioServerSocketChannel注册到EventLoopGroup(parentEventLoopGroup)

3.注册成功之后调用绑定本地端口完成整个启动过程

当然,只有对pipeline、handler、eventLoop等有一定的了解才能理解Netty的工作机制

写在最后

TO ME: 2021年第一篇博客,加油! 自己一个字一个字码出来的感觉很好!!

TO YOU: 如果觉得有帮助记得点赞或者推荐哦!

Netty入门一:服务端应用搭建 & 启动过程源码分析的更多相关文章

  1. Activity启动过程源码分析(Android 8.0)

    Activity启动过程源码分析 本文来Activity的启动流程,一般我们都是通过startActivity或startActivityForResult来启动目标activity,那么我们就由此出 ...

  2. Android系统默认Home应用程序(Launcher)的启动过程源码分析

    在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还须要有一个Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home应 ...

  3. Android应用程序绑定服务(bindService)的过程源码分析

    Android应用程序组件Service与Activity一样,既能够在新的进程中启动,也能够在应用程序进程内部启动:前面我们已经分析了在新的进程中启动Service的过程,本文将要介绍在应用程序内部 ...

  4. 10.4 android输入系统_框架、编写一个万能模拟输入驱动程序、reader/dispatcher线程启动过程源码分析

    1. 输入系统框架 android输入系统官方文档 // 需FQhttp://source.android.com/devices/input/index.html <深入理解Android 卷 ...

  5. Spark(五十一):Spark On YARN(Yarn-Cluster模式)启动流程源码分析(二)

    上篇<Spark(四十九):Spark On YARN启动流程源码分析(一)>我们讲到启动SparkContext初始化,ApplicationMaster启动资源中,讲解的内容明显不完整 ...

  6. Android Content Provider的启动过程源码分析

    本文參考Android应用程序组件Content Provider的启动过程源码分析http://blog.csdn.net/luoshengyang/article/details/6963418和 ...

  7. Flume-NG启动过程源码分析(二)(原创)

    在上一节中讲解了——Flume-NG启动过程源码分析(一)(原创)  本节分析配置文件的解析,即PollingPropertiesFileConfigurationProvider.FileWatch ...

  8. Spark(四十九):Spark On YARN启动流程源码分析(一)

    引导: 该篇章主要讲解执行spark-submit.sh提交到将任务提交给Yarn阶段代码分析. spark-submit的入口函数 一般提交一个spark作业的方式采用spark-submit来提交 ...

  9. Spring启动过程源码分析基本概念

    Spring启动过程源码分析基本概念 本文是通过AnnotationConfigApplicationContext读取配置类来一步一步去了解Spring的启动过程. 在看源码之前,我们要知道某些类的 ...

随机推荐

  1. shell--数据库备份脚本

    #!/bin/bash #数据库的完全备份 #把日期显示为170605(这个是当前的时间)的格式 date=$(date +%y%m%d) #计算下这个备份的数据库文件的大小 size=$(du -s ...

  2. js实现弹幕

    弹幕是一个很常见的功能,下面是本人封装的一个小小的实现方案,存在不足之处可以提出来或自由改进. 直接上代码:复制可运行 <!DOCTYPE html> <html> <h ...

  3. 关于C++的异常抛出

    在接触 throw 之前,我们只知道可以通过函数的返回值来获取和定位错误,比如通过 return 来层层返回是一种方法,但如果牵扯到多层函数调用,那么通过 return 来返回错误显得过于拖沓,这时就 ...

  4. STL——容器(List)List 的构造函数

    list<T> lstT -- list 对象的默认构造 list 与 vector 一样,同样采用模板类实现,对象的默认构造形式:list<T> lstT  如: 1 #in ...

  5. Object not found! The requested URL was not found on this server.... 报错解决方案

    服务器(centos6.5) lnmp 报错如下 Object not found! The requested URL was not found on this server. The link ...

  6. Golang之应用测试

    Go 应用测试 测试的覆盖率 命令: go test ./ -v -cover 在<Go Web 编程>一书中,有以下结论: 这并不是绝对的,测试文件可以在不同的包,进行测试也是不会出现问 ...

  7. 阿里云视频点播之URL批量拉取上传(调整为多个视频上传)

    项目引入阿里云视频点播PHP-SDK 背景:2021年乐视云的点播将停止提供服务,项目决定选择选用阿里云的视频的点播.在上线前,需要将之前的视频提前导入资源库,URLS方式拉取是比较方便的,对编辑同事 ...

  8. 出现VMware Workstation 无法连接到虚拟机。请确保您有权运行该程序、访问该程序使用的所有目录以及访问所有临时文件目录。 未能将管道连接到虚拟机: 所有的管道范例都在使用中。

    今天在学习Linux 的时候 启动VM时出现了这个问题, 搞了很久终于弄好了, 就写篇博客来记录一下,帮助一下大家,如果对大家有帮助,还请大哥大姐点个关注,你的支持就是我坚持下去的动力 ! VMwar ...

  9. 6 个例子教你重构 Python 代码

    1. 合并嵌套的 if 条件 太多的嵌套会使代码难以理解,这在 Python 中尤为如此,因为 Python 没有括号来帮助区隔不同的嵌套级别. 阅读深度嵌套的代码容易让人烦躁,因为你必须理清哪些条件 ...

  10. PHP代码样例

    1 <?php 2 3 /** 4 * 时间:2015-8-6 5 * 作者:River 6 * 超级有用.必须收藏的PHP代码样例 7 */ 8 class Helper { 9 10 /** ...