写点什么

深入了解 Netty 原理篇

作者:邱学喆
  • 2021 年 11 月 27 日
  • 本文字数:3378 字

    阅读完需:约 11 分钟

深入了解Netty原理篇

一. 概述

在网络编程中,最牛的 java 语言的开源框架就非 netty 莫属了,其隐藏复杂网络处理的逻辑从而抽象的提供易于使用的 API 的客户端/服务器框架,便于开发人员快速使用,而不用考虑各种场景的逻辑处理只关心业务层面的开发。

在了解 netty 原理时,可以先看之前写的有关 Selector 篇《Java Selector 模型》,可以更加快速的了解 netty 是怎么运作的。

在介绍 netty 时,也会介绍其采用的设计模式。

二. 使用

2.1 服务端

通过 netty 提起一个对外服务的代码如下:

ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(new NioEventLoopGroup())//设置事件循环组  .channel(NioServerSocketChannel.class)//设置服务channel  .option(ChannelOption.SO_BACKLOG,1024)//设置server socker属性  .childOption(ChannelOption.SO_TIMEOUT, 2000)  .childHandler(new ChannelInitializer<NioSocketChannel>() {    protected void initChannel(NioSocketChannel ch) throws Exception {      //其在bind过程触发fireChannelRegistered时会被调用,将其放入管道对象中      ch.pipeline()        .addLast(....);//设置socker的数据处理器    }  });serverBootstrap.bind(8080).sync();//绑定端口,对外提供服务
复制代码

2.2 客户端

通过 netty 提供一个访问服务的代码如下:

Bootstrap bootstrap = new Bootstrap();bootstrap.group(new NioEventLoopGroup())//设置事件循环组  .channel(NioSocketChannel.class)//设置服务channel  .option(ChannelOption.TCP_NODELAY, true)//设置server socker属性  .option(ChannelOption.SO_KEEPALIVE, false)  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())  .option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize())  .option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize())  .handler(new ChannelInitializer<SocketChannel>() {    @Override    public void initChannel(SocketChannel ch) throws Exception {      //其在connect过程触发fireChannelRegistered时会被调用,将其放入管道对象中      ChannelPipeline pipeline = ch.pipeline();      pipeline.addLast(.....);//设置socker的数据处理器    }  });ChannelFuture channelFuture = bootstrap.connect(RemotingHelper.string2SocketAddress(addr));//接着就可以通过channelFuture进行向服务端发送请求
复制代码

当我们初始化后拿到 bootstrap 对象后,就可以去通过其对象的 connect 方法去创建连接,返回 ChannelFuture 对象,我们就可以通过 ChannelFuture 对象中获取其内部的 Channel 对象,调用 Channel 对象中的 write 方法向服务端发送请求;

三. 原理

3.1 启动器

启动器的类图如下:

客户端以及服务端启动器都共同继承了 AbstractBootstrap 抽象类。其属性的具体用途介绍如下:

  • group 线程模型,也就是说当一个连接请求,或者交互请求,是如何处理的一种模型;其采用了一种叫做 Reactor 设计模式来实现。在客户端,其就一个线程池,一个线程处理所有的连接请求、发送请求、接收响应;在服务端,就是一个线程池,只负责接收连接请求

  • channelFactory 创建 channel 的工厂类,在 netty 内封装了 jdk 原生中的 channel;例如 NioServerSocketChannel 以及 NioSocketChannel 等我们常见的 channel 实现类;

  • localAddress 就是 TCP 信息,即本地创建的 TCP。在服务端,就是对外服务的 IP 以及端口。而客户端,并没有使用该字段信息;

  • options 就是向目标 socket 设置相关属性的对象;

  • attrs 是向 channel 设置相关属性的对象

  • handler 是收到请求后处理器,如 IO 处理等,在客户端主要是协议解析,而在服务端主要的接受连接请求后创建 socket 对象;在服务端,我们一般不设置该属性;

3.1.1 客户端

客户端只记录两个属性,一个是服务端的目标地址;一个是配置对象 BootstrapConfig,其是一个建造类对象,最终相关的初始化信息都会落在 Bootstrap 对象中对应的属性字段;

我们将客户端连接服务端的过程梳理如下:

3.1.2 服务端

服务端的字段跟抽象类有点相似,只是父类中的是针对 ServerSocket 的,而 ServerBootstrap 类中的属性是针对 Socket 的;稍微需要特别说的是 childHandler 是针对协议解析的等功能;

我们将服务端连接服务端的过程梳理如下

其中稍微注意的初始化相关属性步骤,关键的代码如下:

3.2 线程模型

有看过聊聊Linux 五种IO模型文章的朋友都知道 IO 读写是非常慢的,在 netty 框架是采用了多路复用。另外可以学习《Scalable IO in Java》,可以对 reactor 模式有更多的认知,如果英文不行的话,可以阅读《传说中神一样的Reactor反应器模式》。

这里我简单整理出来几大关键步骤:

  • 连接请求(Connection request)

  • 接收请求(Read request)

  • 解析请求(Decode request)

  • 处理服务(Process service)

  • 序列化(Encode reply)

  • 发送响应结果(Send reply)

其服务端的交互图如下:

对应 netty 的接口 EventLoopGroup,类图如下:

分别的流程如下:

客户端接收响应跟上图是差不多的;

这里值得注意的是,netty 都是采用异步操作,其机制跟 jdk 中的 Future 原理差不多。当我查看 netty 的源码时候,都会看到 ChannelFuture,它们的关系如下图:

线程中负责读写操作,而 ChannelFuture 充当桥梁;业务代码可以往 ChannelFuture 添加对应的监听器,监听一旦已处理完则会触发监听器所对应的动作。

3.3 处理器以及管道

在 netty 框架中,处理器是非常重要的地位;上面介绍的线程模型是负责监听 socket 的相关事件,具体的事件的如何处理,还得交给处理器来处理;类图如下:

这里理清楚下面的几个接口的用途:

  • ChannelHandler 处理 IO 事件或者拦截 IO 操作,以及转发到下一个处理器操作。

  • ChannelOutboundHandler 接收 IO 操作的通知

  • ChannelInboundHandler 监听有关状态变更,可以触发对应的回调函数,如注册(Register)、读(Read)等状态的变更;

  • ChannelInboundInvoker、ChannelOutboundInvoker 是有关抽象出来的方法

  • ChannelPipeline 是管道接口

状图如下:

我们从几个动作所处的流程进行了解:


比如说服务器绑定端口所触发的逻辑,客户端连接所触发的流程,跟上面相似;可以去查阅;

3.3.1 处理器

处理是对 IO 传过来的信息进行处理,跟 tomcat 的过滤器有点相似。下面我所列的是常见的处理器;

  • IdleStateHandler 心跳机制,其通过 fireUserEventTriggered 来触发,所以业务端需要实现 ChannelInboundHandler 类来收集这些事件来做对应的处理。

  • FlowControlHandler 流量管控,但其比较粗糙。一般不会用;但是可以借鉴。

  • CorsHandler 跨域处理器

  • ProxyHandler 代理服务处理器

  • MessageToMessageCodec 类型转换处理器

  • ByteToMessageCodec 将 ByteBuf 转换成目标对象的类型处理器

  • ByteToMessageDecoder 将 ByteBuf 转换成目标对象的类型处理器

  • MessageToMessageDecoder 将匹配好的源对象转换成目标对象的类型转换处理器

  • AbstractTrafficShapingHandler 限流器

  • ServerBootstrapAcceptor 客户端连接处理器

  • ChannelInitializer 处理器追加处理,当启动器启动时,会将 ChannelInitializer 中的一些处理器追加到管道中;上面例子就可以看出来

我们常说的协议解析,主要介绍的 ByteToMessageDecoder。该类是抽象类,我们常用的定长报文解析 FixedLengthFrameDecoder,当然还有根据一个规则,如前面几个字节来确定一个完整的报文的长度是多少,如 LengthFieldBasedFrameDecoder,在 RocketMQ 就是使用该类来解析报文的;http 协议对应的 HttpObjectDecoder 下面的子类。redis 协议对应的 RedisDecoder。具体使用时具体找到 ByteToMessageDecoder 下面的实现类具体逻辑,现在对其有一个初步认知,后面继续加深认识。


除了协议解析之外,还有增加额外的功能,如限流 AbstractTrafficShapingHandler ,跨域处理 CorsHandler ,日志打印 LoggingHandler,压缩等。


在服务端,netty 默认会管道中添加 ServerBootstrapAcceptor 处理器,当客户端连接请求过来,会将该 socket 注册到 Selector 中。

四. 字节操作

字节操作也就是 netty 将 jdk 提供的 ByteBuffer 封装成了 ByteBuf 对象。便于操作;具体会在另外一篇详细介绍。


五.总结

通过上面的介绍,我们知道了启动器 AbstractBootstrap,启动器包含客户端、服务端。以及线程模型 EventLoopGroup 以及 EventLoop。以及管道 ChannelPipeline,以及 ChannelHandler,以及 ByteBuf。下面列出它们之间的关系;


发布于: 2021 年 11 月 27 日阅读数: 13
用户头像

邱学喆

关注

计算机原理的深度解读,源码分析。 2018.08.26 加入

在IT领域keep Learning。要知其然,也要知其所以然。原理的爱好,源码的阅读。输出我对原理以及源码解读的理解。个人的仓库:https://gitee.com/Michael_Chan

评论

发布
暂无评论
深入了解Netty原理篇