netty第一篇--初识netty

本文是建立在你有一点的java Nio基础,才展开的netty学习,不然你会比较吃力。

netty的起源和历史

2004年6月Netty2发布

    2004年6月Netty2的1.0版本发布,这是在java社区中第一个基于事件驱动的应用网络框架。

    Maven中最后版本:1.9.2 :net.gleamynode:netty2:1.9.2

2005年5月Mina发布

    2005年5月官方发布了第一个版本mina 0.7.1,并在ApacheDS 项目中使用。

    2006年10月Mina发布1.0.0版本。

    2010年9月Mina发布2.0.0版本。

2008年Netty3发布

    2008年10月jboss发布Netty3.0.0版本。

2013年Netty4发布

    2013年7月Netty(netty.io)发布4.0.0版本。

Netty和Mina是Java世界非常知名的通讯框架。它们都出自同一个作者,Mina属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户netty.io。

netty的现状及欢迎程度

 
  Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持。作为当前最流行的NIO底层通信框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。 Apache Flink,Apache Spark , Elastic Search, gRPC, Apache dubbo,  Nacos, Redisson等诸多明星项目都在使用。
 
  Netty 利用 Java 高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 构建一个客户端/服务端,其具有高并发、传输快、封装好等特点。
  • 高并发   Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高 。
  • 传输快   Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。
  • 封装好   Netty封装了NIO操作的很多细节,提供易于使用的API,还有心跳、重连机制、拆包粘包方案等特性,使开发者能能够快速高效的构建一个稳健的高并发应用。
 

  为什么Netty使用NIO而不是AIO?

  1. Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化
  2. Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱
  3. Linux上AIO不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)

netty的整体架构--reactor模型

  Reactor具体分为三种线程模型

  1. 单Reactor单线程
  2. 单Reactor多线程
  3. 主从Reactor多线程

单Reactor单线程

  在单线程模型中Reactor和Acceptor,以及执行任务的线程都在一个线程,当线程在执行耗时的业务处理时,这时的链=连接请求或者其他的业务不能及时的处理,当请求并发量比较低的时候还是可以抗住的,一旦高并发,将不堪重负,而且单线程也没有充分利用多核cpu的特点,浪费了资源

 

单Reactor多线程

  在单Reactor多线程模型下,连接请求及acceptor仍然在一个线程,io的read和send操作也是在Reactor线程,但是将业务逻辑处理交给线程池(多线程),此时不管业务操作多耗时,线程里有多个线程来处理任务,任务之间不会有大的延迟处理的影响,但如果并发太高,Reactor线程也来不及连接请求和读写数据,于是主从Reactor多线程模型产生

主从Reactor多线程模型

  在这个模型中,主要有一个mainReactor,多个subReactor,以及每个subReactor都附带一个线程池,这个模型大大的分解了各项处理,mainReactor主要负责监听来自客户端的请求(即selector.select()),当有连接请求到达,将会调用accept函数(位于mainReactor线程中),并将产生的socketChannel注册到一个subReactor的selector中,而且绑定该socketChannel感兴趣的事件,当有该socketChannel感兴趣的事件发生时,该subReactor就会调用selector.select()方法,并将需要完成的任务扔到绑定该subReactor的线程池来执行任务,任务结果返回subReactor,然后由subReactor回复客户端.

 

netty快速上手:

maven引入
<dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.53.Final</version>
</dependency>

 建立netty服务端:

 1 import io.netty.bootstrap.ServerBootstrap;
 2 import io.netty.channel.*;
 3 import io.netty.channel.nio.NioEventLoopGroup;
 4 import io.netty.channel.socket.nio.NioServerSocketChannel;
 5 import io.netty.channel.socket.nio.NioSocketChannel;
 6 import io.netty.handler.codec.string.StringDecoder;
 7 
 8 public class NettyServerDemo {
 9 
10     public static void main(String[] args) {
11         try {
12             new NettyServerDemo().run();
13         } catch (Exception e) {
14             e.printStackTrace();
15         }
16     }
17 
18     public void run() throws Exception{
19         // ServerBootstrap负责建立服务端,可以直接使用Channel去建立服务端但是一般使用ServerBootstrap
20         // ServerBootstrap 是服务端启动引导类。
21         ServerBootstrap serverBootstrap = new ServerBootstrap();
22 
23         // boos接收传入连接,因为boos仅接收客户端连接,不做复杂的逻辑处理,为了尽可能减少资源的占用,取值越小越好
24         EventLoopGroup boss = new NioEventLoopGroup(1);
25         // 用于真正读写事件的group socketChannel
26         EventLoopGroup worker = new NioEventLoopGroup();
27 
28         serverBootstrap.group(boss, worker)
29                 // 指定使用NioServerSocketChannel产生一个Channel用来接收连接
30                 // 设置通道实现方式
31                 .channel(NioServerSocketChannel.class)
32                 // ChannelInitializer用于配置一个新的Channel
33                 // 用于向你的Channel当中添加ChannelInboundHandler的实现
34                 // 该方法用来设置业务处理类(自定义handler)
35 
36                 //一个channel 只能分配给一个EventLoop,但是一个EventLoop可以同时维护多个channel 🌟🌟🌟
37                 .childHandler(new ChannelInitializer<NioSocketChannel>() {
38 
39                     protected void initChannel(NioSocketChannel ch) {
40 
41                         ChannelPipeline pipeline = ch.pipeline();
42                         //解码器
43                         pipeline.addLast(new StringDecoder());
44 
45                         //因为你的 Echo 服务器会响应传入的消息,所以它需要实现 ChannelInboundHandler 接口,用 来定义响应入站事件的方法。
46                         // 这个简单的应用程序只需要用到少量的这些方法,所以继承 Channel- InboundHandlerAdapter 类也就足够了,它提供了 ChannelInboundHandler 的默认实现。
47                         ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
48 
49                             //对于每个传入的消息都要调用 channelRead方法
50                             @Override
51                             protected void channelRead0(ChannelHandlerContext ctx, String msg) {
52                                 System.out.println(msg);
53                             }
54                         });
55 
56                     }
57 
58                 })
59                 .option(ChannelOption.SO_BACKLOG, 128)
60                 // 是否启用心跳保活机制。在双方TCP套接字建立连接后(即都进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。
61                 // childOption是用来给父级ServerChannel之下的Channels设置参数的
62                 .childOption(ChannelOption.SO_KEEPALIVE, true)
63                 // 绑定端口号
64                 .bind(8000);
65 
66     }
67 
68 }
View Code

 

代码解读:

  首先创建了 ServerBootstrap 启动类,构建了两个 EventLoopGroup 对象分别为 boss worker 对应着上述的线程模型的 mainReactor subReactor ,然后调用 ServerBootstrap 的group方法,channel方法,childHandler方法,option方法,childOption方法,bind方法。

  其中最重要的就是childHandler方法,也是消息处理的方法。

  看代码,我们大概可以猜到,里面做了哪些工作,通过 initChannel 方法构建了一个 NioSocketChannel 对象,取出其中的 ChannelPipeline ,然后又往 ChannelPipeline 中通过 addLast 方法添加 handler 对象。

建立客户端:
 1 public class NettyClient {
 2 
 3     public static void main(String[] args) throws InterruptedException {
 4         //客户端配置和启动的类
 5         Bootstrap bootstrap = new Bootstrap();
 6         //接受新连接线程,主要负责创建新连接
 7         EventLoopGroup group = new NioEventLoopGroup();
 8         //指定使用NioServerSocketChannel产生一个Channel用来接收连接
 9         bootstrap.group(group).channel(NioSocketChannel.class)
10                 .handler(new ChannelInitializer<Channel>() {
11                     @Override
12                     protected void initChannel(Channel ch) {
13                         ch.pipeline().addLast(new StringEncoder());
14                     }
15                 });
16         //连接服务端
17         Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
18 
19         while (true) {
20             //写数据
21             channel.writeAndFlush(new Date() + ": hello world!");
22             Thread.sleep(2000);
23         }
24     }
25 }
View Code

 

netty关键类解析

  ServerBootstrap, EventLoop,EventLoopGroup,ChannelPipeline,ChannelHandler,Channel,ChannelHandlerContext

我们结合上述的reactor模型进行分析,简单了解下它们各自处于什么位置:

  

逐个解释:

Channel 接口 (通道)

  channel是一个通道,在Nio网络编程模型中, 服务端和客户端进行IO数据交互(得到彼此推送的信息)的媒介就是Channel;

根据服务端和客户端,Channel可以分成两类:

  • 服务端: NioServerSocketChannel
  • 客户端: NioSocketChannel

Netty对Jdk原生的ServerSocketChannel进行了封装和增强封装成了NioXXXChannel

 

EventLoop 接口:

  EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。

  • 一个 EventLoopGroup 包含一个或者多个 EventLoop;
  • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
  • 一个 EventLoop 可能会被分配给一个或多个 Channel。
  • 注意,在这种设计中,一个给定 Channel 的 I/O 操作都是由相同的 Thread 执行的,实际上消除了对于同步的需要。

     

EventLoopGroup 是存放 EventLoop 的集合,与EventLoop的关系类似于 线程池和线程。

 

ChannelHandler

  Netty 的主要组件是 ChannelHandler,就是一个事件处理接口,数据的读写最终会在ChannelHandler里进行处理。

  ChannelHandler通常会分两种实现:

  ChannelInboundHandler :入站数据的处理实现

  ChannelOutboundHandler :出站数据的处理实现

 

ChannelPipeline,ChannelHandlerContext,ChannelHandler三者的关系:

  一个channel注册到EventLoop上后,会生成一个ChannelPipeline,为ChannelHandler的执行编排顺序, ChannelPipeline就是一个由 ChannelHandlerContext 组成的双向链表,之所以是链表结构,是因为一个Channel可能拥有多个ChannelHandler,而又为何双向,是因为ChannelInboundHandler和ChannelOutboundHandler 的执行顺序是相反的。

  ChannelHandlerContext就是用来包裹ChannelHandler的,与后者是一对一的关系,主要功能是管理它所关联的 ChannelHandler 和在 同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。

 
  上图也显示了入站和出站 ChannelHandler 可以被安装到同一个 ChannelPipeline 中。如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部 开始流动,并被传递给第一个 ChannelInboundHandler。这个 ChannelHandler 不一定 会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个 ChannelInboundHandler。最终,数据将会到达 ChannelPipeline 的尾端,届时,所有入站处理就都结束了。
 
  数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从 ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层。
 
扩展:
  ChannelHandler 可以通过添加、删除或者替换其他的 ChannelHandler 来实时地修改 ChannelPipeline 的布局(它也可以将它自己从 ChannelPipeline 中移除)。 这是 ChannelHandler 最重要的能力之一。
 
 

ByteBuf类--netty的数据容器

 
  网络数据的基本单位总是字节。Java NIO 提供了 ByteBuffer 作为它 的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。
Netty 的 ByteBuffer 替代品是 ByteBuf,一个强大的实现,既解决了 JDK API 的局限性, 又为网络应用程序的开发者提供了更好的 API。

 

ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 ByteBuf 读取时, 它的 readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的 writerIndex 也会被递增。

下图展示了一个空 ByteBuf 的布局结构和状态。 

 

 

  要了解这些索引两两之间的关系,请考虑一下,如果打算读取字节直到 readerIndex 达到 和 writerIndex 同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就 如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个 IndexOutOfBoundsException。

  名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或 者 get 开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作。 可以指定 ByteBuf 的最大容量。试图移动写索引(即 writerIndex)超过这个值将会触发异常。(默认的限制是 Integer.MAX_VALUE) 

 

netty如何解决 TCP粘包和半包问题的?

什么是粘包和半包?

  由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;

  分包产生的原因就简单的多:可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。

 

通常解决粘包半包问题的办法

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。


(1)在包尾增加分割符,比如回车换行符进行分割,例如FTP协议;linebase包和delimiter包下,分别使用LineBasedFrameDecoder和DelimiterBasedFrameDecoder,如果超过规定字节长度,会报错。
(2)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;fixed包下,使用FixedLengthFrameDecoder
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第 一个字段使用int32来表示消息的总长度,LengthFieldBasedFrameDecoder;。

 

posted on 2020-10-30 10:58  半城枫叶半城雨丶  阅读(498)  评论(0编辑  收藏  举报