Netty 入门(一):基本组件与线程模型

  Netty 的学习内容主要是围绕 TCP 和 Java NIO 这两个点展开的,后文中所有的内容如果没有特殊说明,那么所指的内容都是与这两点相关的。由于 Netty 是基于 Java NIO 的 API 之上构建的网络通讯框架,Java NIO 中的几个组件,都能在 Netty 中找到对应的封装。下面我们就来一一熟悉 Netty 中的基本组件。

一、基本组件

Netty 的组件主要有以下 8 个:

  1. Channel
  2. ByteBuf
  3. ChannelHandler
  4. ChannelHandlerContext
  5. Pipeline
  6. EventLoop
  7. EventLoopGroup
  8. ServerBootstrap/Bootstrap

1.1 Channel

  Netty 中的 Channel 封装了 JDK 中原生的 Channel,所有对 Netty 中 Channel 的操作,最后都会转化成对原生 Channel 的操作。那么为什么要封装呢,主要有两点:1. 原生的 Channel 与 Netty 框架的结构不够兼容,所有 Netty 进行了一层包装,使其更符合 Netty 使用逻辑;2. 避免了对 SocketChannel 的直接操作,提供更直观和友好的 API 给开发人员。

  Netty 常用的是 NioSocketChannel 和 NioServerSocketChannel,对应了 JDK 中的 SocketChannel 和 ServerSocketChannel。Netty 中的 Channel 都有与之对应的 EventLoop 和 Pipeline,后面也会介绍这两个组件。 

1.2 ByteBuf

  ByteBuf 与 JDK 中的 ByteBuffer 类似。Netty 中的 ByteBuf 有基于 ByteBuffer 构建的,也有自身设计的其他实现。从不同的层级可以有多种划分方式,使用时主要关注的可能这 3 个方面:1. 池化与非池化;2. 堆内存与直接内存;3. 是否使用了 Unsafe 类来操作内存。关于这几点暂时有个大致的了解即可。

  ByteBuffer 只使用一个指针来保存读写的索引,使用起来比较麻烦,容易出错。而 ByteBuf 则使用了两个指针分别保存当前读和写的索引,使用起来就很方便,从 ByteBuf 读取数据时,只需要关注 readerIndex 即可。将数据写入 ByteBuf 时,只需要关注 writerIndex。ByteBuff 的容量 capacity 与两个指针之间的大小关系:0 <= readerIndex <= writerIndex <= capacity。

  Netty 源码中 ByteBuf 类注释中很好的展示了它的结构:

1 /*
2  *      +-------------------+------------------+------------------+
3  *      | discardable bytes |  readable bytes  |  writable bytes  |
4  *      |                   |     (CONTENT)    |                  |
5  *      +-------------------+------------------+------------------+
6  *      |                   |                  |                  |
7  *      0      <=      readerIndex   <=   writerIndex    <=    capacity
8  */

1.3  ChannelHandler

  ChannelHandler 简言之就是一个处理器, 它的功能就是处理消息。它就像流水线上的工人,对每一个从他面前经过的部件进行加工。与工人稍有不同的地方是,它可以什么也不做,将消息直接交给下一个处理器,也可以直接将消息丢掉,不再传递。而且 ChannelHandler 是有方向的,这一点我们后面再详细学习。对于开发人员来说,就是在 ChannelHandler 中编写业务逻辑的操作代码。

1.4 ChannelHandlerContext

  从名称即可知道它是 ChannelHandler 的容器,每个 ChannelHandler 都有与之一一对应的 ChannelHandlerContext 对应。实际上,每个 ChannelHandler 并不直接交互,都是通过 ChannelHandlerContext 将彼此联系起来。ChannelHandlerContext 则是一个 Node,它有前驱和后继。对于一个 Channel 来说,它看到的是一个双向链表。

1.5 Pipeline

  Pipeline 中保存了由 ChannelHandlerContext 组成的双向链表。Netty 中的每个 Channel 都有一条自己的 Pipeline,每当该通道有需要处理的消息时,就会遍历 Pipeline 中的链表,通过每一个处理器来处理消息。Pipeline 的链表中默认就保存了一个 Head  和一个 Tail,所有用户添加的处理器都在这两个节点之间。下图就是包含一个用户处理器的 Pipeline:

1.6 EventLoop

  EventLoop 可以简单的看作是一个线程,用来处理分配给它的 Channel 上的事件,也就是说一个 EventLoop 下面可能挂了多个 Channel。

1.7 EventLoopGroup

   从它的名称就可以知道它维护了一组 EventLoop,可以看作是一个 Netty 实现的线程池,负责给每一个新建里的 Channel 分配线程。

学习 Netty 的过程:对基本组件先有一个大致的了解,然后逐步熟悉各个组件的细节。

1.8 ServerBootstrap/Bootstrap

  这两个组件分别是用来启动服务端和客户端 ,在启动之前,可以通过这两个组件设定各种参数,添加 Handler,指定通道类型等。(Bootstrap 是鞋带的意思,为啥跟启动挂上勾了,可以参考知乎上的解答:Boot一词是为什么被用作计算机并作为引导解释的?或者说他的由来? - 知乎

二、Netty 线程模型

2.1 图解

  Netty 采用的是 Reactor 线程模型,先从一个最简单的 HelloWorldServer 级别的线程模型来入手,如下图:

  先了解一个 Channel 的建立过程:

  1. 服务端启动时,会启用一个线程并创建一个 NioServerSocketChannel 来监听指定的端口。这个线程上有一个 Selector,它关注的是 Accpet 事件;
  2. 当有客户端连接过来时,上图中的 EventLoop-0会创建一个 NioSocketChannel ,将该通道注册到 EventLoop-1 的 Selector 上,然后 EventLoop-1 就负责此后该 Channel 生命周期上所有的读写事件的处理;
  3. EventLoop-1 对其所属通道数据读写及其他处理,就通过 Pipeline 中的处理器链来实现。

  Netty 还包含了普通任务和定时任务的执行,暂且不管。

2.2 演示代码

  下面就贴一下简单的服务端演示代码: 

 1 package netty;
 2 
 3 import io.netty.bootstrap.ServerBootstrap;
 4 import io.netty.channel.ChannelFuture;
 5 import io.netty.channel.ChannelInitializer;
 6 import io.netty.channel.ChannelOption;
 7 import io.netty.channel.EventLoopGroup;
 8 import io.netty.channel.nio.NioEventLoopGroup;
 9 import io.netty.channel.socket.SocketChannel;
10 import io.netty.channel.socket.nio.NioServerSocketChannel;
11 
12 import java.net.InetSocketAddress;
13 
14 /**
15  * Protocol:
16  * Desc:
17  *
18  * @author xi
19  * @date 2018/7/24 20:25
20  */
21 public class HelloWorldServer {
22 
23     private int port;
24 
25     public HelloWorldServer(int port) {
26         this.port = port;
27     }
28 
29     public void start() {
30         EventLoopGroup bossGroup = new NioEventLoopGroup(1);
31         EventLoopGroup workerGroup = new NioEventLoopGroup();
32         try {
33             ServerBootstrap sbs = new ServerBootstrap().group(bossGroup, workerGroup)//设置两个 Group
34                     .channel(NioServerSocketChannel.class)//指定使用的通道类型
35                     .localAddress(new InetSocketAddress(port))
36                     .handler(new HelloWorldServerHandler())//添加 NioServerSocket 的处理器
37                     .childHandler(new ChannelInitializer<NioSocketChannel>() {//添加 NioSocketChannel 的处理器
38                         @Override
39                         protected void initChannel(NioSocketChannel ch) throws Exception {
40                             ch.pipeline().addLast(new InBoundHandlerA());                          
41                         }
42                     }).option(ChannelOption.SO_BACKLOG, 128)
43                     .childOption(ChannelOption.SO_KEEPALIVE, true);
44 
45             ChannelFuture future = sbs.bind(port).sync();//监听指定端口,同步操作,阻塞到建立监听成功
46             System.out.println("Server start listen at " + port);
47             future.channel().closeFuture().sync();//等待上一步建立监听的通道关闭
48         } catch (Exception e) {
49             bossGroup.shutdownGracefully();
50             workerGroup.shutdownGracefully();
51         }
52     }
53 
54     public static void main(String[] args) throws Exception {
55         int port = 8080;
56         new HelloWorldServer(port).start();
57     }
58 }

  演示代码很简单,唯二没有贴出来的地方就是两个处理器的代码,暂且不管,对于下一篇熟悉服务端启动的源码没有影响。

  参考资料:

  1. 『Netty 实战』- 中文版
  2. 『Netty 权威指南』- 第二版,这本书是基于 Netty 5 写的,虽然 Netty 5 项目已经关闭了,但是本书还是值得参考的
  3. Java读源码之Netty深入剖析-慕课网实战 - 基本上对源码的分析和了解,都是基于这个视频课程的内容,讲的挺好的

猜你喜欢

转载自www.cnblogs.com/magexi/p/10192784.html