概念

1.从IO到NIO到Netty经历


https://www.jianshu.com/p/a4e03835921a

2.netty是什么

异步事件驱动程序,用于快速开发高性能服务端和客户端
封装了jdk底层BIO和NIO模型,提供高度可用的API,Netty底层IO模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从NIO模型变身为IO模型

自带编码解码器解决拆包粘包问题,用户只用关心业务逻辑
Netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程模型做到非常高效的并发处理
自带协议栈,无需用户关心(什么是协议栈,协议栈就是我自带了很多种协议,netty都帮你实现了,对于使用者来说我们不用关心这些事情.)

使用JDK自带的NIO需要了解太多的概念,编程复杂,一不小心bug横飞
Netty解决了JDK的很多包括空轮询在内的bug
自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
Netty社区活跃,遇到问题随时邮件列表或者issue
Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大

3.4种IO对比

Netty[笔记] - 图1
Netty[笔记] - 图2

4.选择Netty理由

曾经有两个项目组同时用到了NIO编程技术,一个项目组选择了自己开发NIO服务端,直接使用JDK原生的API,结果两个多月过去了,他们的NIO服务端始终无法稳定,问题频出.由于NIO通信是它们的核心组件之一,因此项目的进度受到了严重的影响,
另一个项目组直接使用Netty作为NIO服务端,业务的定制开发工作量非常小,测试表明,功能和性能都完全达标,项目组几乎没有在NIO服务端上花费额外的时间和精力,项目进展也非常顺利.
这两个项目组的不同遭遇告诉我们:开发出高质量的NIO程序并不是一个简单的事情,除去NIO固有的复杂性和BUG不谈,作为一个NIO服务端,需要能够处理网络的闪断,客户端的重复接入,客户端的安全认证,消息的编解码,半包读写等情况.如果你没有足够的NIO编程经验积累,一个NIO框架的稳定旺旺需要半年甚至更长的时间,更为糟糕的是,一旦在生产环境中发生问题,往往会导致跨节点的服务调用中断,严重的可能会导致整个集群环境都不可用,需要重启服务器,这种非正常停机会带来巨大的损失.
从可维护角度看,由于NIO采用了异步非阻塞编程模型,而且是一个IO线程处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境中的问题,我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大.

5.Netty优点

  1. API使用简单,开发门槛低
    2. 功能强大,预置了多种编解码功能,支持多种主流协议
    3. 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展
    4. 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
    5. 成熟,稳重,Netty修复了已经发现的所有的JDK NIO BUG ,业务开发人员不需要再为NIO的bug而烦恼
    6. 社区活跃,版本迭代周期短,发现的bug可以被及时修复,同时,更多的新功能会加入,

    6.不选择Java原生NIO编程的原因

  2. NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector,ServerSocketChannel,SocketChannel,ByteBuffer等.
    2. 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程,这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序.
    3. 可靠性能力补齐,工作量和难度都非常大,例如客户端面临断连重连,网络闪退,半包读写,失败缓存,网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大.
    4. JDK NIO的BUG 例如 epoll bug ,它会导致Selector空轮询,最终导致CPU 100%

    7.Netty运用场景


    高性能的,异步事件驱动非阻塞式NIO框架,它提供了对外TPC,UDP和文件传输支持,最关键的是通过Future-Listener机制
    Future-Listener就是一种异步监听回调机制,这种机制保证了延迟度几乎没有,只要你完成执行完之后状态立马返回,比如说我们的微博,你发送信息过去,它没有点开看,那么状态基本上是未读,当你发送对象点开微博的时候立马就返回已读信息.

    1分布式进程通信
    例如: hadoop、dubbo、akka等具有分布式功能的框架,底层RPC通信都是基于netty实现的,这些框架使用的版本通常都还在用netty3.x

    2、游戏服务器开发
    最新的游戏服务器有部分公司可能已经开始采用netty4.x 或 netty5.x

    8.为什么不用Netty5



    1. netty5 中使用了 ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显
    2. 多个分支的代码同步工作量很大
    3. 作者觉得当下还不到发布一个新版本的时候
    4. 在发布版本之前,还有更多问题需要调查一下,比如是否应该废弃 exceptionCaught, 是否暴露EventExecutorChooser等等。

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



    Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化。
    AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多。
    据说Linux上AIO不够成熟,处理回调结果速度跟不上处理需求,有点像外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈。
    作者原话:
    Not faster than NIO (epoll) on unix systems (which is true)




     Channel

    Channel相当于网络通讯的Socket,Netty把Socket抽象成了Channel,Channel是一个接口.

    Channel 是 NIO 基本的结构。它代表了一个用于连接到实体如硬件设备、文件、网络套接字或程序组件,能够执行一个或多个不同的 I/O 操作(例如读或写)的开放连接。
    现在,把 Channel 想象成一个可以“打开”或“关闭”,“连接”或“断开”和作为传入和传出数据的运输工具。

    Netty[笔记] - 图3

    一个Channel里面有一个ChannelPipeline,一个ChannelPipeline包含两个以上Context.

    基本的I/O 操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语。在基于Java 的网络编程中,其基本的构造是类Socket。Netty 的Channel 接口所提供的API,被用于所有的I/O 操作。大大地降低了直接使用Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
    由于Channel 是独一无二的,所以为了保证顺序将Channel 声明为java.lang.Comparable 的一个子接口。因此,如果两个不同的Channel 实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error。

    Channel 的生命周期状态
    ChannelUnregistered :Channel 已经被创建,但还未注册到EventLoop
    ChannelRegistered :Channel 已经被注册到了EventLoop
    ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
    ChannelInactive :Channel 没有连接到远程节点
    当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给ChannelPipeline 中的ChannelHandler,其可以随后对它们做出响应。

    最重要Channel 的方法
    eventLoop: 返回分配给Channel 的EventLoop
    pipeline: 返回分配给Channel 的ChannelPipeline
    isActive: 如果Channel 是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket 传输一旦连接到了远程节点便是活动的,而一个Datagram 传输一旦被打开便是活动的。
    localAddress: 返回本地的SokcetAddress
    remoteAddress: 返回远程的SocketAddress
    write: 将数据写到远程节点。这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷
    flush: 将之前已写的数据冲刷到底层传输,如一个Socket
    writeAndFlush: 一个简便的方法,等同于调用write()并接着调用flush()

     EventLoop和EventLoopGroup



    回想一下我们在NIO中是如何处理我们关心的事件的?在一个while循环中select出事件,然后依次处理每种事件。我们可以把它称为事件循环,这就是EventLoop。interface io.netty.channel. EventLoop 定义了Netty 的核心抽象,用于处理网络连接的生命周期中所发生的各种事件而已。

    EventLoop可以理解成为只有一个线程的线程池,线程池除了有线程以外,还会有阻塞队列,EventLoop继承了ScheduleExecutorService,所以还可以执行定时任务.
    一个任务或者一个事件被提交给了EventLoop去处理的时候,不一定会马上处理,有可能会马上处理,有可能会进入内部的阻塞队列,当我们创建channel的时候, 每一个EventLoop要处理一个channel,也就是所谓的socket,所以第一步要把channel注册到EventLoop上面去.

    EventLoopGroup 是一个线程池组,每当一个channel注册进来的时候,EventLoopGroup会从它内部重写取出一个EventLoop专门处理channel.

    一个channel的所有生命周期是由在注册的时候所指定的EventLoop来管,不会说中间替换的情况.不会说跨线程处理的情况,所以就不需要考虑线程安全问题.

    一个EventLoop在它的生命周期内只和一个Thread来绑定,一个EventLoop可以处理很多Channel,但是一个Channel只会对应一个EventLoop(由这个EventLoop来处理.)


    io.netty.util.concurrent 包构建在JDK 的java.util.concurrent 包上。而,io.netty.channel 包中的类,为了与Channel 的事件进行交互,扩展了这些接口/类。一个EventLoop 将由一个永远都不会改变的Thread 驱动,同时任务(Runnable 或者Callable)可以直接提交给EventLoop 实现,以立即执行或者调度执行。
    Netty[笔记] - 图4
    根据配置和可用核心的不同,可能会创建多个EventLoop 实例用以优化资源的使用,并且单个EventLoop 可能会被指派用于服务多个Channel。
    Netty的EventLoop在继承了ScheduledExecutorService的同时,只定义了一个方法,parent()。在Netty 4 中,所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。

    任务调度

    偶尔,你将需要调度一个任务以便稍后(延迟)执行或者周期性地执行。例如,你可能想要注册一个在客户端已经连接了5 分钟之后触发的任务。一个常见的用例是,发送心跳消息到远程节点,以检查连接是否仍然还活着。如果没有响应,你便知道可以关闭该Channel 了。

    线程管理

    在内部,当提交任务到如果当前)调用线程正是支撑EventLoop 的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。
    Netty[笔记] - 图5

    线程的分配

    服务于Channel 的I/O 和事件的EventLoop 则包含在EventLoopGroup 中。
    异步传输实现只使用了少量的EventLoop(以及和它们相关联的Thread),而且在当前的线程模型中,它们可能会被多个Channel 所共享。这使得可以通过尽可能少量的Thread 来支撑大量的Channel,而不是每个Channel 分配一个Thread。EventLoopGroup 负责为每个新创建的Channel 分配一个EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel。
    一旦一个Channel 被分配给一个EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的Thread)。请牢记这一点,因为它可以使你从担忧你的ChannelHandler 实现中的线程安全和同步问题中解脱出来。
    Netty[笔记] - 图6
    需要注意,EventLoop 的分配方式对ThreadLocal 的使用的影响。因为一个EventLoop 通常会被用于支撑多个Channel,所以对于所有相关联的Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上下文中,它仍然可以被用于在多个Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件。

    2.ChannelFuture 接口


    Netty 中所有的I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了ChannelFuture 接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
    可以将ChannelFuture 看作是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。

    异步通知

    ChannelFuture的作用是用来保存Channel异步操作的结果。
    我们知道,在Netty中所有的I/O操作都是异步的。这意味着任何的I/O调用都将立即返回,而不保证这些被请求的I/O操作在调用结束的时候已经完成。取而代之地,你会得到一个返回的ChannelFuture实例,这个实例将给你一些关于I/O操作结果或者状态的信息。
    对于一个ChannelFuture可能已经完成,也可能未完成。当一个I/O操作开始的时候,一个新的future对象就会被创建。在开始的时候,新的future是未完成的状态--它既非成功、失败,也非被取消,因为I/O操作还没有结束。如果I/O操作以成功、失败或者被取消中的任何一种状态结束了,那么这个future将会被标记为已完成,并包含更多详细的信息(例如:失败的原因)。请注意,即使是失败和被取消的状态,也是属于已完成的状态。
    每个 Netty 的 outbound I/O 操作都会返回一个 ChannelFuture;这样就不会阻塞。这就是 Netty 所谓的“自底向上的异步和事件驱动”。
    下面这张图来自于官方文档,用于说明各种状态的关系:
    Netty[笔记] - 图7
    各种各样的方法被提供,用来检查I/O操作是否已完成、等待完成,并寻回I/O操作的结果。 它的 addListener 方法注册了一个 ChannelFutureListener ,当操作完成时,可以被通知(不管成功与否)。

    sync()
    等待ChannelFuture直到完成为止,如果失败就抛异常,抛出失败的原因

     ChannelHandler、ChannelPipeline、ChannelHandlerContext

1.ChannelHandler 接口


从应用程序开发人员的角度来看,Netty 的主要组件是ChannelHandler,程序员的主要工作就是写各种ChannelHandler和应用各种ChannelHandler.它充当了所有处理入站和出站数据的应用程序逻辑的容器。

ChannelHandler 的方法是由网络事件触发的。事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式(比如网络传输的是0101串儿,我们这个0101串儿转化成JavaBean的话,我们就要自己定义实现一个ChannelHandler,我们在这个ChannelHandler就可以给0101串儿转化成JavaBean),例如各种编解码,或者处理转换过程中所抛出的异常。

举例来说,ChannelInboundHandler 是一个你将会经常实现的子接口。这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler 直接冲刷数据然后输出到对端。应用程序的业务逻辑通常实现在一个或者多个ChannelInboundHandler 中。
这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被应用程序的业务逻辑所处理。

ChannelHandler 的生命周期

一个ChannelHandler作用主要是放到ChannelPipeline里面去,在ChannelHandler被添加到ChannelPipeline 中或者被从ChannelPipeline 中移除时都会触发相应的事件。

这些方法中的每一个都接受一个ChannelHandlerContext 参数。

handlerAdded 当把ChannelHandler 添加到ChannelPipeline 中时被调用
handlerRemoved 当从ChannelPipeline 中移除ChannelHandler 时被调用
exceptionCaught 当处理过程中在ChannelPipeline 中发生异常的时候调用

Netty 定义了下面两个重要的ChannelHandler 子接口:
ChannelInboundHandler——当有数据被接收的时候,这个handler就会被调用
ChannelOutboundHandler——当有数据流出的时候,这个handler就会被调用

ChannelHandler的适配器


有一些适配器类可以将编写自定义的ChannelHandler 所需要的工作降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter。
你可以使用ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter类作为自己的ChannelHandler 的起始点。这两个适配器分别提供了ChannelInboundHandler和ChannelOutboundHandler 的基本实现。通过扩展抽象类ChannelHandlerAdapter,它们获得了它们共同的超接口ChannelHandler 的方法。
ChannelHandlerAdapter 还提供了实用方法isSharable()。如果其对应的实现被标注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline。
Netty[笔记] - 图8

SimpleChannelHandler

处理消息接收和写

messageReceived接收消息
hannelConnected新连接,通常用来检测IP是否是黑名单
channelDisconnected链接关闭,可以再用户断线的时候清楚用户的缓存数据等

channelDisconnected与channelClosed的区别?

channelDisconnected只有在连接建立后断开才会调用
channelClosed无论连接是否成功都会调用关闭资源

| package com.server;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
/**

  • 消息接受处理类
  • @author -琴兽-

    /
    public class HelloHandler extends SimpleChannelHandler {

    /**

    • 接收消息
      /
      @Override
      public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
      /
      网络传输不是直接传字节流的,netty是用ChannelBuffer封装了起来/
      // ChannelBuffer message = (ChannelBuffer) e.getMessage();
      // String s = new String(message.array());//转成数组再转成String 才能打印
      // System.out.println(s);

      String s = (String) e.getMessage();
      System.out.println(s);



      /
      向客户端回写数据/
      // ChannelBuffer channelBuffer = ChannelBuffers.copiedBuffer(“hi”.getBytes());
      // ctx.getChannel().write(channelBuffer);


      //回写数据
      ctx.getChannel().write(“hi”);
      super.messageReceived(ctx, e);
      }

      /*

    • 捕获异常
      /
      @Override
      public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
      System.out.println(“exceptionCaught”);
      super.exceptionCaught(ctx, e);
      }

      /*

    • 新连接
    • 使用方法:
    • 服务器有些客人是用正常的服务端访问,有些人是写服务器,比如我们程序员,不停的写
    • 如果一秒钟发送的消息很多,就会使得我服务器压力变大,就会搞崩服务器,


      /
      @Override
      public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
      System.out.println(“channelConnected”);
      super.channelConnected(ctx, e);
      }

      /*

    • 必须是链接已经建立,关闭通道的时候才会触发
    • 这里可以检测玩家下线清空 玩家缓存.
      /
      @Override
      public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
      System.out.println(“channelDisconnected”);
      super.channelDisconnected(ctx, e);
      }

      /*

    • channel关闭的时候触发
      /
      @Override
      public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
      System.out.println(“channelClosed”);
      *super
      .channelClosed(ctx, e);
      }
      } | | —- |



    ChannelInboundHandler 接口

    处理入站数据以及各种状态变化

    下面列出了接口 ChannelInboundHandler 的生命周期方法。这些方法将会在数据被接收时或者与其对应的Channel 状态发生改变时被调用。正如我们前面所提到的,这些方法和Channel 的生命周期密切相关。

    channelRegistered 当Channel 已经注册到它的EventLoop 并且能够处理I/O 时被调用
    channelUnregistered 当Channel 从它的EventLoop 注销并且无法处理任何I/O 时被调用
    channelActive 当Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
    channelInactive 当Channel 离开活动状态并且不再连接它的远程节点时被调用
    channelReadComplete 当Channel上的一个读操作完成时被调用
    channelRead 当从Channel 读取数据时被调用
    ChannelWritabilityChanged
    当Channel 的可写状态发生改变时被调用。可以通过调用Channel 的isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过Channel.config().setWriteHighWaterMark()和Channel.config().setWriteLowWaterMark()方法来设置
    userEventTriggered 当ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用。

    SimpleChannelInboundHandler

| package com;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

public class WebSocketHandler extends SimpleChannelInboundHandler {
/*

  1. * 覆盖了 channelRead0() 事件处理方法。
  2. * 每当从服务端读到客户端写入信息时,
  3. * 其中如果你使用的是 Netty 5.x 版本时,
  4. * 需要把 channelRead0() 重命名为messageReceived()<br />
  5. */<br />

@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println(“收到消息: “ + msg.text());
ctx.channel().writeAndFlush(new TextWebSocketFrame(“我已经收到了 “));

}

/*

  1. * 覆盖channelActive 方法在channel被启用的时候触发(在建立连接的时候)
  2. * 覆盖了 channelActive() 事件处理方法。服务端监听到客户端活动
  3. */<br />

public void channelActive(ChannelHandlerContext ctx) throws Exception {

  1. **_
  2. _super**.channelActive(ctx);<br />

}

/*

  1. * 每当从服务端收到新的客户端连接时,触发这个方法
  2. */<br />

@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println(“handlerAdded!!!”);
}

/*

  1. * (non-Javadoc)<br />
  2. * .覆盖了 handlerRemoved() 事件处理方法。
  3. * 每当从服务端收到客户端断开时
  4. */<br />

@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println(“handlerRemoved!!!”);
}

/*

  1. * exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,
  2. * 即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。
  3. * 在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。
  4. * 然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,
  5. * 比如你可能想在关闭连接之前发送一个错误码的响应消息。
  6. */<br />

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {


  1. _super**.exceptionCaught(ctx, cause);<br />

}

} | | —- |




ChannelOutboundHandler 接口

处理出站数据并且允许拦截所有的操作。

出站操作和数据将由ChannelOutboundHandler 处理。它的方法将被Channel、Channel-
Pipeline 以及ChannelHandlerContext 调用。
所有由ChannelOutboundHandler 本身所定义的方法:
bind(ChannelHandlerContext,SocketAddress,ChannelPromise)
当请求将Channel 绑定到本地地址时被调用
connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise)
当请求将Channel 连接到远程节点时被调用
disconnect(ChannelHandlerContext,ChannelPromise)
当请求将Channel 从远程节点断开时被调用
close(ChannelHandlerContext,ChannelPromise) 当请求关闭Channel 时被调用
deregister(ChannelHandlerContext,ChannelPromise)
当请求将Channel 从它的EventLoop 注销时被调用
read(ChannelHandlerContext) 当请求从Channel 读取更多的数据时被调用
flush(ChannelHandlerContext) 当请求通过Channel 将入队数据冲刷到远程节点时被调用
write(ChannelHandlerContext,Object,ChannelPromise) 当请求通过Channel 将数据写到远程节点时被调用

ChannelInboundHandlerAdapter和SimpleChannelInboundHandler的使用区分


一般用netty来发送和接收数据都会继承SimpleChannelInboundHandler和ChannelInboundHandlerAdapter这两个抽象类,那么这两个到底有什么区别呢?
在客户端的业务Handler继承的是SimpleChannelInboundHandler,而在服务器端继承的是ChannelInboundHandlerAdapter。
最主要的区别就是SimpleChannelInboundHandler在接收到数据后会自动释放掉数据占用的Bytebuffer资源(自动调用Bytebuffer.release()),也就是说,消息被读取后,会自动释放资源.
而为何服务器端(ChannelInboundHandlerAdapter)不能用呢,因为我们想让服务器把客户端请求的数据发送回去,而服务器端有可能在channelRead方法返回前还没有写完数据,因此不能让它自动release(释放数据)。

2.ChannelPipeline 接口


ChannelPipeline 是ChannelHandler的一个所谓的容器,如果ChannelHandler有多个的话,就需要给多个ChannelHandler串起来放到ChannelPipeline里面去,ChannelHandler就像香肠一样一个一个的挂在ChannelPipeline里面.
所有的ChannelHandler不管是出站还是入站,都需要挂在到ChannelPipeline上面,既然是挂载上面,肯定需要顺序,你可以把ChannelPipeline理解为所谓的链表.
addFirst、addBefore、addAfter、addLast 方法都可以改变顺序,我们挂载ChannelHandler顺序很重要,需要注意顺序,并且非常重要.
假如我们做个网络程序,第一需要所谓的加解密,第二个需要格式化的转换, 我网络上的数据过来了,A Handler负责解密,B Handler负责格式的转换,这样的话肯定是B Handle(格式转换)排在前面,因为网络传输的时候数据都是0101串儿的加密后的密文,转化成0101的明文,再进行格式转换成JavaBean.

当有一个数据从网络上通过socket传递到我们的channel,channel在调用我们ChannelHandler里面的ChannelPipeline的时候,入站的数据和事件都又入站处理器进行处理,Netty在运行过程中会通过你实现的接口来判定在挂载的时候ChannelHandler到底是入站处理器还是出站处理器.

虽然ChannelInboundHandle 和ChannelOutboundHandle 都扩展自ChannelHandler,但是Netty 能区分ChannelInboundHandler实现和ChannelOutboundHandler 实现,并确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递。


Netty[笔记] - 图9
ChannelPipeline上的方法
addFirst、addBefore、addAfter、addLast
将一个ChannelHandler 添加到ChannelPipeline 中
remove 将一个ChannelHandler 从ChannelPipeline 中移除
replace 将ChannelPipeline 中的一个ChannelHandler 替换为另一个ChannelHandler
get 通过类型或者名称返回ChannelHandler
context 返回和ChannelHandler 绑定的ChannelHandlerContext
names 返回ChannelPipeline 中所有ChannelHandler 的名称
ChannelPipeline 的API 公开了用于调用入站和出站操作的附加方法。

3.ChannelHandlerContext


当我们把ChannelHandler和ChannelPipeline进行绑定(ChannelHandler添加到ChannelPipeline)的时候,Netty会为我们ChannelHandler自动加一个,ChannelHandlerContext上下文,这一个ChannelHandlerContext代表它们之间的绑定关系,

ChannelHandlerContext的主要功能是管理它所关联的ChannelHandler和在同一个ChannelPipeline中的其他ChannelHandler之间的交互,比如第一个入站处理器把数据交给第二个入站处理器,就由ChannelHandlerContext来调用第二个ChannelHandler.

通过使用作为参数传递到每个方法的ChannelHandlerContext,事件可以被传递给当前ChannelHandler 链中的下一个ChannelHandler。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。

ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联,每当有ChannelHandler 添加到ChannelPipeline 中时,都会创建ChannelHandler-Context。ChannelHandlerContext 的主要功能是管理它所关联的ChannelHandler 和在同一个ChannelPipeline 中的其他ChannelHandler 之间的交互。

ChannelHandlerContext 有很多的方法,其中一些方法也存在于Channel 和Channel-Pipeline 本身上,但是有一点重要的不同。如果调用Channel 或者ChannelPipeline 上的这些方法,它们将沿着整个ChannelPipeline 进行传播。而调用位于ChannelHandlerContext上的相同方法,则将从当前所关联的ChannelHandler 开始,并且只会传播给位于该ChannelPipeline 中的下一个(入站下一个,出站上一个)能够处理该事件的ChannelHandler。
Netty[笔记] - 图10
ChannelHandlerContext 的API
alloc 返回和这个实例相关联的Channel 所配置的ByteBufAllocator
bind 绑定到给定的SocketAddress,并返回ChannelFuture
channel 返回绑定到这个实例的Channel
close 关闭Channel,并返回ChannelFuture
connect 连接给定的SocketAddress,并返回ChannelFuture
deregister 从之前分配的EventExecutor 注销,并返回ChannelFuture
disconnect 从远程节点断开,并返回ChannelFuture
executor 返回调度事件的EventExecutor
fireChannelActive 触发对下一个ChannelInboundHandler 上的channelActive()方法(已连接)的调用
fireChannelInactive 触发对下一个ChannelInboundHandler 上的channelInactive()方法(已关闭)的调用
fireChannelRead 触发对下一个ChannelInboundHandler 上的channelRead()方法(已接收的消息)的调用
fireChannelReadComplete 触发对下一个ChannelInboundHandler 上的channelReadComplete()方法的调用
fireChannelRegistered 触发对下一个ChannelInboundHandler 上的fireChannelRegistered()方法的调用
fireChannelUnregistered 触发对下一个ChannelInboundHandler 上的fireChannelUnregistered()方法的调用
fireChannelWritabilityChanged 触发对下一个ChannelInboundHandler 上的fireChannelWritabilityChanged()方法的调用
fireExceptionCaught 触发对下一个ChannelInboundHandler 上的fireExceptionCaught(Throwable)方法的调用
fireUserEventTriggered 触发对下一个ChannelInboundHandler 上的fireUserEventTriggered(Object evt)方法的调用
handler 返回绑定到这个实例的ChannelHandler
isRemoved 如果所关联的ChannelHandler 已经被从ChannelPipeline中移除则返回true
name 返回这个实例的唯一名称
pipeline 返回这个实例所关联的ChannelPipeline
read 将数据从Channel读取到第一个入站缓冲区;如果读取成功则触发一个channelRead事件,并(在最后一个消息被读取完成后)通知ChannelInboundHandler 的channelReadComplete
(ChannelHandlerContext)方法
当使用ChannelHandlerContext 的API 的时候,有以下两点:
l ChannelHandlerContext 和ChannelHandler 之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
l 如同我们在本节开头所解释的一样,相对于其他类的同名方法,ChannelHandler Context的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。




 内置通信传输模式


NIO io.netty.channel.socket.nio 使用java.nio.channels 包作为基础——基于选择器的方式.

Epoll io.netty.channel.epoll 由 JNI 驱动的 epoll()和非阻塞 IO。这个传输支持只有在Linux 上可用的多种特性,如SO_REUSEPORT,比NIO 传输更快,而且是完全非阻塞的。将NioEventLoopGroup替换为EpollEventLoopGroup , 并且将NioServerSocketChannel.class 替换为EpollServerSocketChannel.class 即可。Netty对NIO的Epoll做了特殊的相关优化,需要注意,如果你在代码里面指定了EpollServiceSocketChannel的话,意味着你的代码只能在Linux上面运行,无法在Window上运行.
从性能上讲Epoll要比NIO要高的,因为NIO底层是用的JDK为我们提供的,JDK为了实现一次编译到处运行,JDK把操作系统的某些方面做了一些相关的屏蔽,所以在性能上面会有点欠缺.
在NIO里面统一使用的水平触发,在Epoll使用的是边缘触发,

OIO io.netty.channel.socket.oio 使用java.net 包作为基础——使用阻塞流,你可以理解为就是普通的BIO.

Local io.netty.channel.local 可以在VM 内部通过管道进行通信的本地传输,如果你的两个网络通讯的对端放在同一个虚拟机里面的话,如果你使用Local模式的话,就不会走网卡了,会直接通过Unix的管道直接进行传输.避免了你的数据通过网卡进行流转.一般情况下你在本地测试的时候也可以用Local,不过我感觉没什么用.

Embedded io.netty.channel.embedded Embedded 传输,允许使用ChannelHandler 而又不需要一个真正的基于网络的传输。在测试ChannelHandler 实现时非常有用,主要是用来做测试用的,类似对每个Handler进行单元测试.

 Bootstrap和ServerBootstrap




Bootstrap是服务端和客户端启动必须要用的东西,Bootstrap初始化做了很多的事情.服务端和客户端代表两种不同的网络行为,如果你是监听传过来的连接,你就是服务器端.如果你是尝试去建立连接,这就是客户端.

因此,有两种类型的引导:一种用于客户端(简单地称为Bootstrap),而另一种(ServerBootstrap)用于服务器。


比较Bootstrap 类

Bootstrap ServerBootstrap
网络编程中的作用 连接到远程主机和端口 绑定到一个本地端口
EventLoopGroup 的数目 1 2


区别:
1. ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用.
2. Boostrap只需要一个EventLoopGroup,但是一个ServerBootstrap 则需要两个(也可以是同一个实例)。

ServerBootstrap的EventLoopGroup线程组包含了一组NIO线程,专门用于网络事件的处理,实际上他们是Reactor线程组,使用两个线程组的原因是一个用于服务端接受客户端的连接,另一个用于进行SocketChannel的网络读写.





在引导过程中添加多个ChannelHandler

Netty 提供了一个特殊的ChannelInboundHandlerAdapter 子类:
public abstract class ChannelInitializer ext ends ChannelInboundHandlerAdapter
它定义了下面的方法:
protect ed abstract void initChannel(C ch) throws Exception;
这个方法提供了一种将多个ChannelHandler 添加到一个ChannelPipeline 中的简便方法。你只需要简单地向Bootstrap 或ServerBootstrap 的实例提供你的ChannelInitializer 实现即可,并且一旦Channel 被注册到了它的EventLoop 之后,就会调用你的initChannel()版本。在该方法返回之后,ChannelInitializer 的实例将会从ChannelPipeline 中移除它自己。




 ChannelOption

配置在网络连接的时候各种网络连接的相关属性

ChannelOption的各种属性在套接字选项中都有对应。
1、ChannelOption.SO_BACKLOG
ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,
服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
2、ChannelOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,
比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR    就无法正常使用该端口。
3、ChannelOption.SO_KEEPALIVE
Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
4、ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF
ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
5、ChannelOption.SO_LINGER
ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送
6、ChannelOption.TCP_NODELAY
ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

 ByteBuf

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


ByteBuf API 的优点:
它可以被用户自定义的缓冲区类型扩展;
通过内置的复合缓冲区类型实现了透明的零拷贝;
容量可以按需增长(类似于JDK 的StringBuilder);
在读和写这两种模式之间切换不需要调用ByteBuffer 的flip()方法;
读和写使用了不同的索引;
支持方法的链式调用;
支持引用计数;
支持池化。
ByteBuf 维护了两个不同的索引,名称以read 或者write 开头的ByteBuf 方法,将会推进其对应的索引,而名称以set 或者get 开头的操作则不会
如果打算读取字节直到readerIndex 达到和writerIndex 同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个IndexOutOf-BoundsException。
可以指定ByteBuf 的最大容量。试图移动写索引(即writerIndex)超过这个值将会触发一个异常。(默认的限制是Integer.MAX_VALUE。)

分配

堆缓冲区
最常用的ByteBuf 模式是将数据存储在JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。可以由hasArray()来判断检查ByteBuf 是否由数组支撑。如果不是,则这是一个直接缓冲区
直接缓冲区
直接分配在直接内存上的就是直接缓冲区
直接缓冲区是另外一种ByteBuf 模式。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。
ByteBufAllocator
Netty 通过interface ByteBufAllocator分配我们所描述过的任意类型的ByteBuf 实例。

名称 描述
buffer() 返回一个基于堆或者直接内存存储的ByteBuf
heapBuffer() 返回一个基于堆内存存储的ByteBuf
directBuffer() 返回一个基于直接内存存储的ByteBuf
compositeBuffer() 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf
ioBuffer() 返回一个用于套接字的I/O 操作的ByteBuf,当所运行的环境具有sun.misc.Unsafe 支持时,返回基于直接内存存储的ByteBuf,否则返回基于堆内存存储的ByteBuf;当指定使用PreferHeapByteBufAllocator 时,则只会返回基于堆内存存储的ByteBuf。

可以通过Channel(每个都可以有一个不同的ByteBufAllocator 实例)或者绑定到ChannelHandler 的ChannelHandlerContext 获取一个到ByteBufAllocator 的引用。
Netty[笔记] - 图11
Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和Unpooled-ByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。
Netty4.1默认使用了PooledByteBufAllocator。
Unpooled 缓冲区
Netty 提供了一个简单的称为Unpooled 的工具类,它提供了静态的辅助方法来创建未池化的ByteBuf
实例。
buffer() 返回一个未池化的基于堆内存存储的ByteBuf
directBuffer()返回一个未池化的基于直接内存存储的ByteBuf
wrappedBuffer() 返回一个包装了给定数据的ByteBuf
copiedBuffer() 返回一个复制了给定数据的ByteBuf
Unpooled 类还可用于ByteBuf 同样可用于那些并不需要Netty 的其他组件的非网络项目。

随机访问索引/顺序访问索引/读写操作

如同在普通的Java 字节数组中一样,ByteBuf 的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是capacity() - 1。使用那些需要一个索引值参数(随机访问,也即是数组下标)的方法(的其中)之一来访问数据既不会改变readerIndex 也不会改变writerIndex。如果有需要,也可以通过调用readerIndex(index)或者writerIndex(index)来手动移动这两者。顺序访问通过索引访问
有两种类别的读/写操作:
get()和set()操作,从给定的索引开始,并且保持索引不变;get+数据字长(bool.byte,int,short,long,bytes)
read()和write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
更多的操作
isReadable() 如果至少有一个字节可供读取,则返回true
isWritable() 如果至少有一个字节可被写入,则返回true
readableBytes() 返回可被读取的字节数
writableBytes() 返回可被写入的字节数
capacity() 返回ByteBuf 可容纳的字节数。在此之后,它会尝试再次扩展直到达到maxCapacity()
maxCapacity() 返回ByteBuf 可以容纳的最大字节数
hasArray() 如果ByteBuf 由一个字节数组支撑,则返回true
array() 如果 ByteBuf 由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException 异常

可丢弃字节

为可丢弃字节的分段包含了已经被读过的字节。通过调用discardRead-Bytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为0,存储在readerIndex 中,会随着read 操作的执行而增加(get*操作不会移动readerIndex)。
缓冲区上调用discardReadBytes()方法后,可丢弃字节分段中的空间已经变为可写的了。频繁地调用discardReadBytes()方法以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节必须被移动到缓冲区的开始位置。建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候。
Netty[笔记] - 图12

可读字节

ByteBuf 的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的readerIndex 值为0。

可写字节

可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex 的默认值为0。任何名称以write 开头的操作都将从当前的writerIndex 处开始写数据,并将它增加已经写入的字节数。
Netty[笔记] - 图13

索引管理

调用markReaderIndex()、markWriterIndex()、resetWriterIndex()和resetReaderIndex()来标记和重置ByteBuf 的readerIndex 和writerIndex。
也可以通过调用readerIndex(int)或者writerIndex(int)来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException。
可以通过调用clear()方法来将readerIndex 和writerIndex 都设置为0。注意,这并不会清除内存中的内容。

查找操作

在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf()方法。
较复杂的查找可以通过调用forEach Byte()。
代码展示了一个查找回车符(\r)的例子。
Netty[笔记] - 图14

派生缓冲区

派生缓冲区为ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:
duplicate();
slice();
slice(int, int);
Unpooled.unmodifiableBuffer(…);
order(ByteOrder);
readSlice(int)。
每个这些方法都将返回一个新的ByteBuf 实例,它具有自己的读索引、写索引和标记索引。其内部存储和JDK 的ByteBuffer 一样也是共享的。
ByteBuf 复制 如果需要一个现有缓冲区的真实副本,请使用copy()或者copy(int, int)方法。不同于派生缓冲区,由这个调用所返回的ByteBuf 拥有独立的数据副本。

引用计数

引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第4 版中为ByteBuf引入了引用计数技术, interface ReferenceCounted。

工具类

ByteBufUtil 提供了用于操作ByteBuf 的静态的辅助方法。因为这个API 是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。
这些静态方法中最有价值的可能就是hexdump()方法,它以十六进制的表示形式打印ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录ByteBuf 的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。
另一个有用的方法是boolean equals(ByteBuf, ByteBuf),它被用来判断两个ByteBuf实例的相等性。

资源释放

当某个ChannelInboundHandler 的实现重写channelRead()方法时,它要负责显式地释放与池化的ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法ReferenceCountUtil.release()
Netty 将使用WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。
1、对于入站请求,Netty的EventLoo在处理Channel的读操作时进行分配ByteBuf,对于这类ByteBuf,需要我们自行进行释放,有三种方式,或者使用SimpleChannelInboundHandler,或者在重写channelRead()方法使用ReferenceCountUtil.release()或者使用ctx.fireChannelRead继续向后传递;
2、对于出站请求,不管ByteBuf是否由我们的业务创建的,当调用了write或者writeAndFlush方法后,Netty会自动替我们释放,不需要我们业务代码自行释放。

 粘包/半包问题

Netty[笔记] - 图15
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

客户端要向服务端发送请求,发送了两个请求,give me a coffee 和give me a tea 两个请求,正常情况下服务端应该是依次接收两个请求分别处理,但是因为某些原因,服务端可能不会那么顺序的收到两个请求,它两个请求可能是粘在一起的,只是收到一个请求,收到的数据可能是这样的give me a coffeegive me a tea ,也就是两个请求粘在一起了,这样的话服务器就无法去解析这个请求,这种现象就是粘包现象.

还有情况是先收到 give me 然后再收到a coffeegive me a tea ,同样是两个请求,但是服务器也无法解析.这种现象叫做分包现象.分包现象就是一个请求的数据分别在不同的阶段收到.

1.TCP粘包/半包发生的原因


由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象
UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。
分包产生的原因就简单的多:可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。
更具体的原因有三个,分别如下。
1. 应用程序写入数据的字节大小大于套接字发送缓冲区的大小
2. 进行MSS大小的TCP分段。MSS是最大报文段长度的缩写。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度
3. 以太网的payload大于MTU进行IP分片。MTU指:一种通信协议的某一层上面所能通过的最大数据包大小。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成托干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上。

出现粘包和分包的根本原因是什么?

是我们服务端根本没有办法去区分你请求是否到底是传送了哪些数据,因为服务端和客户端没有商量好.所以粘包和分包出现的原因是因为没有稳定的数据结构.

解决粘包分包问题的话通常会采取两种措施

1. 采用分隔符分隔的方式
比如说我发送请求之后我加上一个下划线(_)的结束符,服务端可能收到数据还是原来的,但是没有关闭,服务端没有接收到预先约定好的结束分隔符前,等待继续接收数据,后面的数据来了,再接着读取,如果读取到了分隔符了,分隔符之前的数据就成为了一个请求投递到业务层去,剩余的部分接着读,直到读到结束分隔符….以此类推…

这种解决方法效率稍微差点,因为要挨个检查

2. 长度+数据的方式
我在写数据的时候,我在数据前面拿四个字节声明一下发送过来的数据到底有多长,所以服务端收到数据之后先读取前面的长度,比如长度为16,那么后面16长度的数据就是第一次请求,如果没达到长度就接着等第二次数据来了,如果数据够了就凑够一个请求投递到业务层去.

2.解决粘包半包问题


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

1. 在包尾增加分隔符(特殊的的字符串),比如回车换行符进行分割,
2. 消息的固定长度,比如说规定死了每次只发送100个,如果你在收到报文的时候从第0个开始就按照100,100进行切分,如果你收到50个,就等着,等把下一个50个再发送过来你就可以认为是一个完整的报文了.
3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度,LengthFieldBasedFrameDecoder。
比如说你发送100个长度的报文,前面多出两个字节用来告知当前发送的报文的长度, 这样总长度就变成了102个长度的报文了,但是前两个字节标识了当前的报文是多长的.你收到报文的长度了,先读取报文前两个字节,就能得知当前的报文长度是多少了.(最灵活,也是最复杂的.)

 编解码器框架

(一)什么是编解码器


每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节(0101串儿),客户端传给服务端之后,需要给0101串儿转成我服务端需要的数据格式,服务端也是这样,服务端接收到客户端的0101串儿之后需要转成客户端需要的数据格式.
数据格式的转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将字节流从一种格式转换为另一种格式。


什么是编码器
将我们的自己定义的相关的业务类型的数据转换成我们适合于传输的格式(一般来讲适合传输的格式就是字节流)
什么是解码器
把网络上的字节流转换成我们应用程序所需要的我们这种消息格式

(二)解码器


ByteToMessageDecoder将字节解码为消息(是入站处理器,)
MessageToMessageDecoder。 将一种消息类型解码为另一种,可能网络传过来的数据将字节解码为消息之后可能还不是我应用程序所需要的消息,可能中间还需要进行一次消息类型的解码才能是我的应用所接受的消息

因为解码器是负责将入站数据从一种格式转换到另一种格式的,所以Netty 的解码器实现了ChannelInboundHandler。
什么时候会用到解码器呢?很简单:每当需要为ChannelPipeline 中的下一个Channel-InboundHandler 转换入站数据时会用到。此外,得益于ChannelPipeline 的设计,可以将多个解码器链接在一起,以实现任意复杂的转换逻辑。

将字节解码为消息


抽象类ByteToMessageDecoder
将字节解码为消息(或者另一个字节序列)是一项如此常见的任务,以至于Netty 为它提供了一个抽象的基类:ByteToMessageDecoder。由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。
它最重要方法
decode(ChannelHandlerContext ctx,ByteBuf in,List out)
这是你必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的ByteBuf,以及一个用来添加解码消息的List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该List,或者该ByteBuf 中没有更多可读取的字节时为止。然后,如果该List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个ChannelInboundHandler。

将一种消息类型解码为另一种

在两个消息格式之间进行转换(例如,从String->Integer)
decode(ChannelHandlerContext ctx,I msg,List out)
对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随后会被传递给ChannelPipeline中的下一个ChannelInboundHandler
MessageToMessageDecoder,T代表源数据的类型

TooLongFrameException

当前传递给解码器的消息定义的很大,假如当前消息可能有1000个字节,这时候假如每次只能传递100个字节,这意味着Netty需要在内存中开辟一份空间把每次传递的100个字节的消息,如果发现不够解码就把这100个字节的消息,Netty就存起来,下次又来100个字节,发现还不够,就接着存起来.一直存存存,直到有10个100字节的时候,这个时候刚好可以组成一个消息报文的时候,然后Netty就把这个整体正规的解码后进行传递,传递给应用程序.

由于Netty 是一个异步框架,所以需要在网络传递过来的字节可以解码之前必须要在内存中缓冲它们。但是这种情况下会产生问题,Netty存储的数据有可能特别大,这种情况下Netty的内存很有可能会撑爆了.
为了解除这个常见的顾虑,Netty 提供了TooLongFrameException 类,其将由解码器在帧超出程序员设置的阈值指定的大小限制时抛出这个异常.防止Netty因为内存爆满而出问题.

为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个TooLongFrameException(随后会被ChannelHandler.exceptionCaught()方法捕获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如HTTP)可能允许你返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。
Netty[笔记] - 图16

(三)编码器


解码器的功能和解码器的作用正好相反,将消息编码为字节流,

Netty 提供了一组类,用于帮助你编写具有以下功能的编码器:
1.将消息编码为字节;MessageToByteEncoder
2.将消息编码为消息:MessageToMessageEncoder,T代表源数据的类型

将消息编码为字节

encode(ChannelHandlerContext ctx,I msg,ByteBuf out)
encode()方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为ByteBuf 的(类型为I 的)出站消息。该ByteBuf 随后将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler

将消息编码为消息

encode(ChannelHandlerContext ctx,I msg,List out)
这是你需要实现的唯一方法。每个通过write()方法写入的消息都将会被传递给encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler

(四)编解码器类

又可以进行编码又可以进行解码
Netty 的抽象编解码器类正好用于这个目的,因为它们每个都将捆绑一个解码器/编码器对,以处理我们一直在学习的这两种类型的操作。这些类同时实现了ChannelInboundHandler 和ChannelOutboundHandler 接口。

为什么我们并没有一直优先于单独的解码器和编码器使用这些复合类呢?因为通过尽可能地将这两种功能分开,最大化了代码的可重用性和可扩展性,这是Netty 设计的一个基本原则。
相关的类:
抽象类ByteToMessageCodec 又可以实现解码又可以实现编码
抽象类MessageToMessageCodec



(五)Netty内置的编解码器和ChannelHandler


Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相当繁琐的事务上本来会花费的时间与精力。

通过SSL/TLS 保护Netty 应用程序

案例:ZJJ_Netty_2019/11/23_21:41:45_y7v1u



SSL方式可以给你的浏览器变成Https.

SSL和TLS这样的安全协议,它们层叠在其他协议之上,用以实现数据安全。我们在访问安全网站时遇到过这些协议,但是它们也可用于其他不是基于HTTP的应用程序,如安全SMTP(SMTPS)邮件服务器甚至是关系型数据库系统。
为了支持SSL/TLS,Java 提供了javax.net.ssl 包,它的SSLContext 和SSLEngine类使得实现解密和加密相当简单直接。Netty 通过一个名为SslHandler 的ChannelHandler实现利用了这个API,其中SslHandler 在内部使用SSLEngine 来完成工作。

Netty 还提供了开源的OpenSSL 工具包(www.openssl.org)的SSLEngine 实现。这个OpenSsl-Engine 类提供了比JDK 提供的SSLEngine 实现更好的性能,
Netty默认会使用OpenSslEngine。如果在当前项目里面找不到OpenSSL 工具包,Netty 将会使用JDK 的实现方式。

在大多数情况下,SslHandler 将是ChannelPipeline 中的第一个ChannelHandler,因为采用了SSL的话,从网络上流传过来的字节流是密文,肯定需要SslHandler去解码(密文转成明文)才能交给后面的Handler去处理,不然后面的Handler会无法识别.

HTTP 系列

案例:ZJJ_Netty_2019/11/23_21:41:45_y7v1u



HTTP 是基于请求/响应模式的:客户端向服务器发送一个HTTP 请求,然后服务器将会返回一个HTTP 响应。

Netty 提供了多种编码器和解码器以简化对这个协议的使用。
一个HTTP 请求/响应可能由多个数据部分组成(HTTP报文格式会有头部信息,还会有相关的http请求报文体,还会有http最后的结尾符,这几部分组合起来才是一个完整的http报文).

HttpRequestEncoder 将HttpRequest、HttpContent 和LastHttpContent 消息编码为字节
HttpResponseEncoder 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节
HttpRequestDecoder 将字节解码为HttpRequest、HttpContent 和LastHttpContent 消息
HttpResponseDecoder 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息
Netty[笔记] - 图17

聚合HTTP 消息
由于HTTP 的请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty 提供了一个聚合器,它可以将多个消息部分合并为FullHttpRequest 或者FullHttpResponse 消息。通过这样的方式,你将总是看到完整的消息内容。
Netty[笔记] - 图18
HTTP 压缩
当使用HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些CPU 时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。Netty 为压缩和解压缩提供了ChannelHandler 实现,它们同时支持gzip 和deflate 编码。
Netty[笔记] - 图19
使用HTTPS
启用HTTPS 只需要将SslHandler 添加到ChannelPipeline 的ChannelHandler 组合中。

空闲的连接和超时

检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务,Netty 特地为它提供了几个ChannelHandler 实现。
IdleStateHandler 当连接空闲时间太长时,将会触发一个IdleStateEvent 事件。然后,你可以通过在你的ChannelInboundHandler 中重写userEventTriggered()方法来处理该IdleStateEvent 事件。
ReadTimeoutHandler 如果在指定的时间间隔内没有收到任何的入站数据,则抛出一个Read-TimeoutException 并关闭对应的Channel。可以通过重写你的ChannelHandler 中的exceptionCaught()方法来检测该Read-TimeoutException。
WriteTimeoutHandler 如果在指定的时间间隔内没有任何出站数据写入,则抛出一个Write-TimeoutException 并关闭对应的Channel 。可以通过重写你的ChannelHandler 的exceptionCaught()方法检测该WriteTimeout-Exception。

 序列化问题

Java序列化的目的主要有两个:
1.网络传输
2.对象持久化
当选行远程跨迸程服务调用时,需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。而当远程服务读取到ByteBuffer对象或者字节数组时,需要将其解码为发送时的Java 对象。这被称为Java对象编解码技术。
Java序列化仅仅是Java编解码技术的一种,由于它的种种缺陷,衍生出了多种编解码技术和框架

(一)Java序列化的缺点

案例:ZJJ_Netty_2019/11/23_21:34:09_ltphc


Java序列化从JDK1.1版本就已经提供,它不需要添加额外的类库,只需实现java.io.Serializable并生成序列ID即可,因此,它从诞生之初就得到了广泛的应用。
但是在远程服务调用(RPC)时,很少直接使用Java序列化进行消息的编解码和传输,这又是什么原因呢?下面通过分析.Tava序列化的缺点来找出答案。
1 无法跨语言
对于跨进程的服务调用,服务提供者可能会使用C十+或者其他语言开发,当我们需要和异构语言进程交互时Java序列化就难以胜任。由于Java序列化技术是Java语言内部的私有协议,其他语言并不支持,对于用户来说它完全是黑盒。对于Java序列化后的字节数组,别的语言无法进行反序列化,这就严重阻碍了它的应用。
2 序列化后的码流太大
通过一个实例看下Java序列化后的字节数组大小。
3序列化性能太低
无论是序列化后的码流大小,还是序列化的性能,JDK默认的序列化机制表现得都很差。因此,我们边常不会选择Java序列化作为远程跨节点调用的编解码框架。

(二)序列化 – 内置和第三方的MessagePack实战

内置

Netty内置了对JBoss Marshalling和Protocol Buffers的支持

案例:ZJJ_Netty_2019/11/23_21:53:10_4hhm0


集成第三方MessagePack实战(LengthFieldBasedFrame详解)

案例:ZJJ_Netty_2019/11/23_22:00:56_dmtu2

①LengthFieldBasedFrame详解

maxFrameLength:表示的是包的最大长度,
lengthFieldOffset:指的是长度域的偏移量,表示跳过指定个数字节之后的才是长度域;
lengthFieldLength:记录该帧数据长度的字段,也就是长度域本身的长度;
lengthAdjustment:长度的一个修正值,可正可负;
initialBytesToStrip:从数据帧中跳过的字节数,表示得到一个完整的数据包之后,忽略多少字节,开始读取实际我要的数据
failFast:如果为true,则表示读取到长度域,TA的值的超过maxFrameLength,就抛出一个 TooLongFrameException,而为false表示只有当真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。
数据包大小: 14B = 长度域2B + “HELLO, WORLD”(单词HELLO+一个逗号+一个空格+单词WORLD)
Netty[笔记] - 图20
长度域的值为12B(0x000c)。希望解码后保持一样,根据上面的公式,参数应该为:
1. lengthFieldOffset = 0
2. lengthFieldLength = 2
3. lengthAdjustment 无需调整
4. initialBytesToStrip = 0 - 解码过程中,没有丢弃任何数据

数据包大小: 14B = 长度域2B + “HELLO, WORLD”
Netty[笔记] - 图21
长度域的值为12B(0x000c)。解码后,希望丢弃长度域2B字段,所以,只要initialBytesToStrip = 2即可。
1. lengthFieldOffset = 0
2. lengthFieldLength = 2
3. lengthAdjustment 无需调整
4. initialBytesToStrip = 2 解码过程中,丢弃2个字节的数据

数据包大小: 14B = 长度域2B + “HELLO, WORLD”。长度域的值为14(0x000E)
Netty[笔记] - 图22
长度域的值为14(0x000E),包含了长度域本身的长度。希望解码后保持一样,根据上面的公式,参数应该为:
1. lengthFieldOffset = 0
2. lengthFieldLength = 2
3. lengthAdjustment = -2 因为长度域为14,而报文内容为12,为了防止读取报文超出报文本体,和将长度字段一起读取进来,需要告诉netty,实际读取的报文长度比长度域中的要少2(12-14=-2)
4. initialBytesToStrip = 0 - 解码过程中,没有丢弃任何数据

在长度域前添加2个字节的Header。长度域的值(0x00000C) = 12。总数据包长度: 17=Header(2B) + 长度域(3B) + “HELLO, WORLD”
Netty[笔记] - 图23
长度域的值为12B(0x000c)。编码解码后,长度保持一致,所以initialBytesToStrip = 0。参数应该为:
1. lengthFieldOffset = 2
2. lengthFieldLength = 3
3. lengthAdjustment = 0无需调整
4. initialBytesToStrip = 0 - 解码过程中,没有丢弃任何数据

Header与长度域的位置换了。总数据包长度: 17=长度域(3B) + Header(2B) + “HELLO, WORLD”
Netty[笔记] - 图24
长度域的值为12B(0x000c)。编码解码后,长度保持一致,所以initialBytesToStrip = 0。参数应该为:
1. lengthFieldOffset = 0
2. lengthFieldLength = 3
3. lengthAdjustment = 2 因为长度域为12,而报文内容为12,但是我们需要把Header的值一起读取进来,需要告诉netty,实际读取的报文内容长度比长度域中的要多2(12+2=14)
4. initialBytesToStrip = 0 - 解码过程中,没有丢弃任何数据
带有两个header。HDR1 丢弃,长度域丢弃,只剩下第二个header和有效包体,这种协议中,一般HDR1可以表示magicNumber,表示应用只接受以该magicNumber开头的二进制数据,rpc里面用的比较多。总数据包长度: 16=HDR1(1B)+长度域(2B) +HDR2(1B) + “HELLO, WORLD”
Netty[笔记] - 图25
长度域的值为12B(0x000c)
1. lengthFieldOffset = 1 (HDR1的长度)
2. lengthFieldLength = 2
3. lengthAdjustment =1 因为长度域为12,而报文内容为12,但是我们需要把HDR2的值一起读取进来,需要告诉netty,实际读取的报文内容长度比长度域中的要多1(12+1=13)
4. initialBytesToStrip = 3 丢弃了HDR1和长度字段
带有两个header,HDR1 丢弃,长度域丢弃,只剩下第二个header和有效包体。总数据包长度: 16=HDR1(1B)+长度域(2B) +HDR2(1B) + “HELLO, WORLD”
Netty[笔记] - 图26
长度域的值为16B(0x0010),长度为2,HDR1的长度为1,HDR2的长度为1,包体的长度为12,1+1+2+12=16。
1. lengthFieldOffset = 1
2. lengthFieldLength = 2
3. lengthAdjustment = -3因为长度域为16,需要告诉netty,实际读取的报文内容长度比长度域中的要 少3(13-16= -3)
4. initialBytesToStrip = 3丢弃了HDR1和长度字段

 如何进行单元测试

一种特殊的Channel 实现——EmbeddedChannel,它是Netty 专门为改进针对ChannelHandler 的单元测试而提供的。
将入站数据或者出站数据写入到EmbeddedChannel 中,然后检查是否有任何东西到达了ChannelPipeline 的尾端。以这种方式,你便可以确定消息是否已经被编码或者被解码过了,以及是否触发了任何的ChannelHandler 动作。
writeInbound(Object… msgs)
将入站消息写到EmbeddedChannel 中。如果可以通过readInbound()方法从EmbeddedChannel 中读取数据,则返回true
readInbound()
从EmbeddedChannel 中读取一个入站消息。任何返回的东西都穿越了整个ChannelPipeline。如果没有任何可供读取的,则返回null
writeOutbound(Object… msgs)
将出站消息写到EmbeddedChannel中。如果现在可以通过readOutbound()方法从EmbeddedChannel 中读取到什么东西,则返回true
readOutbound()
从EmbeddedChannel 中读取一个出站消息。任何返回的东西都穿越了整个ChannelPipeline。如果没有任何可供读取的,则返回null
finish() 将EmbeddedChannel 标记为完成,并且如果有可被读取的入站数据或者出站数据,则返回true。这个方法还将会调用EmbeddedChannel 上的close()方法。
入站数据由ChannelInboundHandler 处理,代表从远程节点读取的数据。出站数据由ChannelOutboundHandler 处理,代表将要写到远程节点的数据。
使用writeOutbound()方法将消息写到Channel 中,并通过ChannelPipeline 沿着出站的方向传递。随后,你可以使用readOutbound()方法来读取已被处理过的消息,以确定结果是否和预期一样。 类似地,对于入站数据,你需要使用writeInbound()和readInbound()方法。
在每种情况下,消息都将会传递过ChannelPipeline,并且被相关的ChannelInboundHandler 或者ChannelOutboundHandler 处理。
Netty[笔记] - 图27

1.测试入站消息

我们有一个简单的ByteToMessageDecoder 实现。给定足够的数据,这个实现将产生固定大小的帧。如果没有足够的数据可供读取,它将等待下一个数据块的到来,并将再次检查是否能够产生一个新的帧。
这个特定的解码器将产生固定为3 字节大小的帧。因此,它可能会需要多个事件来提供足够的字节数以产生一个帧。

Netty[笔记] - 图28

2.测试出站消息

在测试的处理器—AbsIntegerEncoder,它是Netty 的MessageToMessageEncoder 的一个特殊化的实现,用于将负值整数转换为绝对值。
该示例将会按照下列方式工作:
持有AbsIntegerEncoder 的EmbeddedChannel 将会以4 字节的负整数的形式写出站数据;
编码器将从传入的ByteBuf 中读取每个负整数,并将会调用Math.abs()方法来获取其绝对值;
编码器将会把每个负整数的绝对值写到ChannelPipeline 中。
Netty[笔记] - 图29

3.测试异常处理

应用程序通常需要执行比转换数据更加复杂的任务。例如,你可能需要处理格式不正确的输入或者过量的数据。在下一个示例中,如果所读取的字节数超出了某个特定的限制,我们将会抛出一个TooLongFrameException。
这是一种经常用来防范资源被耗尽的方法。设定最大的帧大小已经被设置为3 字节。如果一个帧的大小超出了该限制,那么程序将会丢弃它的字节,并抛出一个TooLongFrameException。位于ChannelPipeline 中的其他ChannelHandler 可以选择在exceptionCaught()方法中处理该异常或者忽略它。
Netty[笔记] - 图30



 Netty进阶和实战

(一)实现UDP单播和广播

TCP和UDP最大区别是TCP是无连接的,没有持久化连接的概念,也就是说,你每次传递的消息都是一个所谓单独的传输单元,而且UDP并没有TCP里面所谓的纠错机制,我A端发送B端,如果B端没有给我发送应答报文,我当前报文我是要重新发的.

UDP是我不管你是否收到还是怎样,我发送完了就不管了.UDP虽然在应用过程中可能会有安全问题,可能会丢包问题,但是有很多的适用场景的.
1. UDP不需要建立连接,没有重发机制,性能快.用完就走,不像TCP还会有三次握手啥的.
2. UDP在实际应用中小数据比如视频传输,音频传输这种丢包我们是允许的.这种场景在UDP中完全是一个合适的方式.

在TCP里面有所谓的粘包半包问题,UDP里面是没有粘包半包问题的(UDP是你有多少数据我就一把梭,它不会对数据包进行所谓的合并发送,有多少数据就一并发送过去了,所以每一个数据包发送的都是完整的,也就没有所谓的粘包半包问题了).

单播的传输模式,定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。面向连接的协议和无连接协议都支持这种模式,TCP就是典型的单播模式,UDP也是有单播.
广播——传输到网络(或者子网)上的所有主机.

1.Netty 的UDP 相关类


interface AddressedEnvelopeextends ReferenceCounted
顶级接口 , 定义一个消息,其包装了另一个消息并带有发送者和接收者地址。其中M 是消息类型;A 是地址类型.
在实际开发的时候我们会用AddressedEnvelope的子类DatagramPacket 数据报的包.


class DefaultAddressedEnvelopeimplements AddressedEnvelope提供了interface AddressedEnvelope的默认实现
class DatagramPacket extends DefaultAddressedEnvelope implements ByteBufHolder

扩展了DefaultAddressedEnvelope 以使用ByteBuf 作为消息数据容器。DatagramPacket是final类不能被继承,只能被使用。
通过content()来获取消息的实际内容
通过sender();来获取发送者的消息
通过recipient();来获取接收者的消息。

interface DatagramChannel extends Channel
扩展了Netty 的Channel 抽象以支持UDP 的多播组管理
class NioDatagramChannnel extends AbstractNioMessageChannel implements DatagramChannel
定义了一个能够发送和接收Addressed-Envelope 消息的Channel 类型
Netty 的DatagramPacket 是一个简单的消息容器,DatagramChannel 实现用它来和远程节点通信。类似于在我们先前的类比中的明信片,它包含了接收者(和可选的发送者)的地址以及消息的有效负载本身。

2.UDP单播


ZJJ_Netty_2019/11/24_10:52:36_6mvg6

3.UDP广播


UDP广播的实现方式和UDP单播的实现方式差不多,唯一的区别就是配置稍稍有点不一样而已.

案例:ZJJ_Netty_2019/11/24_11:52:48_sq8d4

(二)服务器推送技术-短轮询和Comet


用户在使用网络应用的时候,不需要一遍又一遍的去手动刷新就可以及时获得更新的信息,大家平时在上各种视频网站时,对视频节目进行欢乐的吐槽和评论,会看到各种弹幕,当然,他们是用flash技术实现的,对于我们没有用flash的应用,一样可以实现弹幕。又比如在股票网站,往往可以看到,各种股票信息的实时刷新,上面的这些都是基于服务器推送技术。

比如扫码登陆,你用你手机扫码,你在你手机上的动作,淘宝网是不知道的,这里就是使用了服务器推送技术,还有进行支付的时候,扫码也是在手机上操作的,我们扫完码以后网页自动转到付款成功的页面,这就是一个服务器的推送技术.
http协议本身是无法实现服务器推送技术的 ,http协议本身是无状态的,还一个就是单向性的,这是http协议的两大特点.
为了解决无状态,我们出现了cookie和session.
什么是单向性,就是http协议只能由http的客户端(浏览器)向服务端发起请求,然后服务器做一个应答,但是服务器是不允许向客户端发起请求的,是没有这种搞法的,这就是http协议的相关的特性 .

为了解决http的无状态和单向性实现手段有:

(三)Ajax短轮询

案例:ZJJ_Netty_2019/11/24_14:47:06_9k5cf


就是用一个定时器不停的去网站上请求数据,好处就是服务端基本不用改造,实现起来简单
缺点:

服务器的压力大和资源很浪费
我客户端发送给数据给服务器的时候,不管服务器的数据是否更新,按照Http协议的要求,我的服务器端是必须要因为客户端一个应答的.如果有数据就给浏览器返回数据,没有数据就给浏览器返回一个空数据,对我们的服务器来讲是存在一定的压力的.
我每次请求过来的时候不一定有这个相关的数据过来,意味着这一次网络消耗的带宽以及服务器内部的不管是IO和CPU都是存在一定的浪费的.
数据同步的不及时(存在延迟),当我浏览器发起请求的时候,服务器应答完了,在我应答完了以后我们的服务端刚好有一个数据更新了,对于我们的客户端来讲要拿到最新的数据的时候必须要等下一次浏览器请求过来以后我才能把我服务端当前最新的数据返回给浏览器.
这就是数据延迟,如果你轮询设置的间隔时间越长,那么延迟就越大.如果你缩小的话,就会给你的服务器的资源带来很大的压力.

(四)Comet

基于Http长连接


服务器推”是一种很早就存在的技术,以前在实现上主要是通过客户端的套接口,或是服务器端的远程调用。因为浏览器技术的发展比较缓慢,没有为“服务器推”的实现提供很好的支持,在纯浏览器的应用中很难有一个完善的方案去实现“服务器推”并用于商业程序。,因为 AJAX 技术的普及,gmail等等在实现中使用了这些新技术;同时“服务器推”在现实应用中确实存在很多需求。称这种基于 HTTP长连接、无须在浏览器端安装插件的“服务器推”技术为“Comet”。

(五)基于 AJAX 的长轮询

DeferredResult
Spring mvc的控制层接收用户的请求之后,如果要采用异步处理,那么就要返回DeferedResult<>泛型对象。在调用完控制层之后,立即回返回DeferedResult对象,此时驱动控制层的容器主线程,可以处理更多的请求。
可以将DeferedResult对象作为真实响应数据的代理,而真实的数据是该对象的成员变量result,它可以是String类型,或者ModelAndView类型等。
业务处理完毕之后,要执行setResult方法,将真实的响应数据赋值到DeferedResult对象中。此时,容器主线程会继续执行getResult方法,将真实数据响应到客户端。

(六)SSE

严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。
也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。
SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。
总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。
SSE 也有自己的优点。

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

    1.HTTP 头信息

    服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。
    Content-Type: text/event-stream
    Cache-Control: no-cache
    Connection: keep-alive
    上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam。

    2.信息格式

    每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。
    [field]: value\n
    上面的field可以取四个值。

  • data

  • event
  • id
  • retry

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。例子 : this is a test stream\n\n
data 字段
数据内容用data字段表示。
data: message\n\n
如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。
data: begin message\n
data: continue message\n\n
下面是一个发送 JSON 数据的例子。
data: {\n
data: “foo”: “bar”,\n
data: “baz”, 555\n
data: }\n\n
id 字段
数据标识符用id字段表示,相当于每一条数据的编号。
id: msg1\n
data: message\n\n
浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。
event 字段
event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: bar\n
data: a bar event\n\n
上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。
retry 字段
服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。
retry: 10000\n
两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。

(七)技术比较


淘宝和京东用的什么?Ajax短轮询,这说明什么?这些技术并没有什么优劣之分,只有合不合适业务的问题。京东的痛点是什么?要用有限的资源来为千万级甚至上亿的用户提供服务,如果是用长连接,对于接入的服务器,比如说Nginx,是很大的压力,光是为用户维持这个长连接都需要成百上千的Nginx的服务器,这是很划不来的。因为对于京东这类购物网站来说,用户的浏览查询量是远远大于用户下单量的,京东需要注重的是服务更多的用户,而且相对于用户浏览页面的图片等等的流量而言,这点带宽浪费占比是很小的。所以我们看京东的付款后的实现,是用的短轮询机制,而且时长放大到了5秒。
第二个就是用户的来源是很广的,你不知道用户用什么浏览器,所以要最大的支持各种浏览器.
对于扫码登录或者支付啥的,你实时性差一点点是没啥太大关系的.所以大型互联网是用ajax轮询的多.
Netty[笔记] - 图31

网页版微信用的就是Servlet异步长轮询,原因是网页版微信比较关注数据的实时性,对消息的实时性要求很高,我发了消息出去别人要很快就能看到.
SSE和WebSocket相比的优势。最大的优势就是便利:不需要添加任何新组件,用任何你习惯的后端语言和框架就能继续使用。你不用为新建虚拟机、弄一个新的IP或新的端口号而劳神,就像在现有网站中新增一个页面那样简单。可以称为既存基础设施优势。

SSE还是一个HTTP协议,WebSocket是一个完全独立的协议.

SSE的第二个优势是服务端的简洁。相对而言,WebSocket则很复杂,不借助辅助类库基本搞不定。WebSocket能做的,SSE也能做,反之亦然,但在完成某些任务方面,它们各有千秋。WebSocket是一种更为复杂的服务端实现技术,但它是真正的双向传输技术,既能从服务端向客户端推送数据,也能从客户端向服务端推送数据。

 WebSocket通信


什么是WebSocket?
短轮询和Servlet异步(长轮询)和SSE都是所谓的单工通讯,只能一方向另外一方发起请求.

WebSocket是一个规范,是全双工的,客户端和服务器端可以同时进行请求数据的交流,这是一个全双工,WebSocket比较复杂,因为是一个全新的独立的新的协议.



特点
l HTML5中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
l 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。
l 采用新的协议,后端需要单独实现
l 客户端并不是所有浏览器都支持

1.WebSocket通信握手

Websocket借用了HTTP的协议来完成一部分握手
客户端的请求:
Connection 必须设置 Upgrade,表示客户端希望连接升级。
Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。
Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
服务器端:
Upgrade: websocket
Connection: Upgrade
依然是固定的,告诉客户端即将升级的是 Websocket 协议,而不是mozillasocket,lurnarsocket或者shitsocket。
然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。
后面的, Sec-WebSocket-Protocol 则是表示最终使用的协议。
至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行

2.WebSocket通信-STOMP


WebSocket是个规范,在实际的实现中有HTML5规范中的WebSocket API、WebSocket的子协议STOMP。
STOMP(Simple Text Oriented Messaging Protocol)
l 简单(流)文本定向消息协议
l STOMP协议的前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。是属于消息队列的一种协议, 和AMQP, JMS平级. 它的简单性恰巧可以用于定义websocket的消息体格式. STOMP协议很多MQ都已支持, 比如RabbitMq, ActiveMq。
l 生产者(发送消息)、消息代理、消费者(订阅然后收到消息)
STOMP是基于帧的协议

3.WebSocket通信-STOMP

WebSocket是个规范,在实际的实现中有HTML5规范中的WebSocket API、WebSocket的子协议STOMP。
STOMP(Simple Text Oriented Messaging Protocol)
l 简单(流)文本定向消息协议
l STOMP协议的前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。是属于消息队列的一种协议, 和AMQP, JMS平级. 它的简单性恰巧可以用于定义websocket的消息体格式. STOMP协议很多MQ都已支持, 比如RabbitMq, ActiveMq。
l 生产者(发送消息)、消息代理、消费者(订阅然后收到消息)
STOMP是基于帧的协议

4.WebSocket通信实现

SpringBoot

①基于Stomp的聊天室/IM的实现

Netty[笔记] - 图32
具体实现:参考 stomp模块下的代码

②和WebSocket的集成

具体实现:参考 ws模块下的代码

5.Netty

由IETF 发布的WebSocket RFC,定义了6 种帧,Netty 为它们每种都提供了一个POJO 实现。同时Netty也为我们提供很多的handler专门用来处理数据压缩,ws的通信握手等等。
Netty[笔记] - 图33
具体实现:参考 netty-ws模块下的代码

 高级通信服务实现-设计自己的协议栈

1.定义


协议就是类似于双方沟通的时候约定的通话的一种格式.

通信协议从广义上区分,可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好。绝大多数的私有协议传输层都基于TCP/IP,所以利用Netty的NIO TCP协议栈可以非常方便地进行私有协议的定制和开发。
私有协议本质上是厂商内部发展和采用的标准,除非授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己制订,协议实现细节不愿公开,只在企业自己生产的设备之间使用的协议。私有协议具有封闭性、垄断性、排他性等特点。

2.跨节点通信

在传统的Java应用中,通常使用以下4种方式进行跨节点通信。
(1)通过RMI进行远程服务调用;
(2)通过Java的Socket+Java序列化的方式进行跨节点调用;
(3)利用一些开源的RPC框架进行远程服务调用,例如Facebook的Thrift,Apache的Avro等;
(4)利用标准的公有协议进行跨节点服务调用,例如HTTP+XML、RESTful+JSON或者WebService。
跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应消息进行编解码。在请求和应答消息本身以外,也需要携带一些其他控制和管理类指令,例如链路建立的握手请求和响应消息、链路检测的心跳消息等。当这些功能组合到一起之后,就会形成私有协议。

3.协议栈功能设计

协议栈功能描述

Netty协议栈承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下。
(1)基于Netty的NIO通信框架,提供高性能的异步通信能力;
(2)提供消息的编解码框架,可以实现POJO的序列化和反序列化;
(3)提供基于IP地址的白名单接入认证机制;
(4)链路的有效性校验机制;
(5)链路的断连重连机制。

通信模型

Netty[笔记] - 图34
(1)Netty协议栈客户端发送握手请求消息,携带节点ID等有效身份认证信息;
(2)Netty协议栈服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和IP地址合法性校验,校验通过后,返回登录成功的握手应答消息;
(3)链路建立成功之后,客户端发送业务消息;
(4)链路成功之后,服务端发送心跳消息;
(5)链路建立成功之后,客户端发送心跳消息;
(6)链路建立成功之后,服务端发送业务消息;
(7)服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。
备注:需要指出的是,Netty协议通信双方链路建立成功之后,双方可以进行全双工通信,无论客户端还是服务端,都可以主动发送请求消息给对方,通信方式可以是TWO WAY或者ONE WAY。双方之间的心跳采用Ping-Pong机制,当链路处于空闲状态时,客户端主动发送Ping消息给服务端,服务端接收到Ping消息后发送应答消息Pong给客户端,如果客户端连续发送N条Ping消息都没有接收到服务端返回的Pong消息,说明链路已经挂死或者对方处于异常状态,客户端主动关闭连接,间隔周期T后发起重连操作,直到重连成功。

消息定义

Netty协议栈消息定义包含两部分:
消息头;消息体。
Netty消息定义表

名称 类型 长度 描述
Header Header 变长 消息头定义
Body Object 变长 对于请求消息,它只是方法的参数,对于响应消息,它是返回值

Netty协议消息头定义(Header)

名称 类型 长度 描述
crcCode Int 32 Netty消息校验码
Length Int 32 整个消息长度
sessionID Long 64 会话ID
Type Byte 8 0:业务请求消息
1:业务响应消息
2:业务one way消息
3握手请求消息
4握手应答消息
5:心跳请求消息
6:心跳应答消息
Priority Byte 8 消息优先级:0~255
Attachment Map 变长 可选字段,由于推展消息头

链路的建立

Netty协议栈支持服务端和客服端,对于使用Netty协议栈的应用程序而言,不需要刻意区分到底是客户端还是服务器端,在分布式组网环境中,一个节点可能既是客户端也是服务器端,这个依据具体的用户场景而定。
Netty协议栈对客户端的说明如下:如果A节点需要调用B节点的服务,但是A和B之间还没有建立物理链路,则有调用方主动发起连接,此时,调用方为客户端,被调用方为服务端。
考虑到安全,链路建立需要通过基于Ip地址或者号段的黑白名单安全认证机制,作为样例,本协议使用基于IP地址的安全认知,如果有多个Ip,通过逗号进行分割。在实际的商用项目中,安全认证机制会更加严格,例如通过密钥对用户名和密码进行安全认证。
客户端与服务端链路建立成功之后,由客户端发送握手请求消息,握手请求消息的定义如下
(1) 消息头的type字段值为3;
(2) 可选附件数为0;
(3) 消息头为空
(4) 握手消息的长度为22个字节
服务端接收到客户端的握手请求消息之后,如果IP校验通过,返回握手成功应答消息给客户端,应用层链路建立成功。握手应答消息定义如下:
(1)消息头的type字段值为4
(2)可选附件个数为0;
(3)消息体为byte类型的结果,0:认证成功;-1认证失败;
链路建立成功之后,客户端和服务端就可以互相发送业务消息了。

链路的关闭

由于采用长连接通信,在正常的业务运行期间,双方通过心跳和业务消息维持链路,任何一方都不需要主动关闭连接。
但是,在以下情况下,客户端和服务端需要关闭连接:
(1)当对方宕机或者重启时,会主动关闭链路,另一方读取到操作系统的通知信号得知对方REST链路,需要关闭连接,释放自身的句柄等资源。由于采用TCP全双工通信,通信双方都需要关闭连接,释放资源;
(2)消息读写过程中,发生了I/O异常,需要主动关闭连接;
(3)心跳消息读写过程发生了I/O异常,需要主动关闭连接;
(4)心跳超时,需要主动关闭连接;
(5)发生编码异常等不可恢复错误时,需要主动关闭连接。

可靠性设计

Netty协议栈可能会运行在非常恶劣的网络环境中,网络超时、闪断、对方进程僵死或者处理缓慢等情况都有可能发生。为了保证在这些极端异常场景下Netty协议栈仍能够正常工作或者自动恢复,需要对他的可靠性进行统一规划和设计。

①心跳机制

在凌晨等业务低谷时段,如果发生网络闪断、连接被Hang住等问题时,由于没有业务消息,应用程序很难发现。到了白天业务高峰期时,会发生大量的网络通信失败,严重的会导致一段时间进程内无法处理业务消息。为了解决这个问题,在网络空闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。
当读或者写心跳消息发生I/O异常的时候,说明已经中断,此时需要立即关闭连接,如果是客户端,需要重新发起连接。如果是服务端,需要清空缓存的半包信息,等到客户端重连。

②重连机制

如果链路中断,等到INTEVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL后再次发起重连,直到重连成功。
为了保持服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL时间之后再发起重连,而不是失败后立即重连。
为了保证句柄资源能够及时释放,无论什么场景下重连失败,客户端必须保证自身的资源被及时释放,包括但不现居SocketChannel、Socket等。
重连失败后,需要打印异常堆栈信息,方便后续的问题定位。

③重复登录保护

当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。
服务端接收到客户端的握手请求消息之后,首先对IP地址进行合法性校验,如果校验成功,在缓存的地址表中查看客户端是否已经登录,如果登录,则拒绝重复登录,返回错误码-1,同时关闭TCP链路,并在服务端的日志中打印握手失败的原因。
客户端接收到握手失败的应答消息之后,关闭客户端的TCP连接,等待INTERVAL时间之后,再次发起TCP连接,知道认证成功。
为了防止由服务端和客户端对链路状态理解不一致导致的客户端无法握手成功问题,当服务端连续N次心跳超时之后需要主动关闭链路,清空改客户端的地址缓存信息,以保证后续改客户端可以重连成功,防止被重复登录保护机制拒绝掉。

④测试

1、 正常情况
2、 客户端宕机,服务器应能清除客户端的缓存信息,允许客户端重新登录
3、 服务器宕机,客户端应能发起重连
4、在LoginAuthRespHandler中进行注释,可以模拟当服务器不处理客户端的请求时,客户端在超时后重新进行登录。

 //*2019年11月17日21:15:03

(一)IdleStateHandler(心跳检测)


假如用户长时间没有读或者写,长时间没有收到服务端发送的数据,或者是没有给服务端写数据的话,那么这个会话就是占着茅坑不拉屎,就需要给它强制下线,因为长时间未操作.
IdleStateHandler就可以做这个事情,

心跳其实就是一个普通的请求,特点数据简单,业务也简单,心跳检测不可能带很多数据过来.

心跳的目的:
1. 对于服务端来说

最大作用就是定时清除一些没用的会话(长时间没读写的连接会话)
客户端忽然断电或者拔掉网卡,tcp是无法及时通知服务端我客户端已经断开了,服务器其实是一直保持这个会话的.当你客户端又重新连接就又是新的会话,并不会重新连接到老的会话, 所以老的会话就相当于僵尸进程一样,就存在那里什么也不做,系统肯定要定时清除那东西.
所以对于服务端来说,它会让你客户端提供一个依据,需要你客户端定时去发送一个请求来证明你客户端处于正常连接状态.我们把这个业务命名为心跳.

2. 对客户端来说:
网络不好然后会话会断掉,对于做一个好的框架不可能说,你断了我一定要等有一个用户来请求的时候获取会话获取不到才请求重连,有一些需要客户端自己检测到你自己断开了需要重连.客户端自己尝试给服务端发送请求,发送不成功获取会话失败了就尝试重连.
心跳也可以用来检测网络延迟,

心跳检测根据自己的业务需求去考虑要不要去做.




ChannelEvent.getState() 是显示会话状态,

@Override

protected void initChannel(Channel ch) throws Exception {

ch.pipeline().addLast(new IdleStateHandler(5, 5, 10));



Netty[笔记] - 图35

(二)消息和管道(pipeline)问题




当前的一个handler如何往下面的一个handler传递一个对象(handler和handler是如何传递对象的)


| bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast(“decoder”, new StringDecoder());
pipeline.addLast(“encoder”, new StringEncoder());
pipeline.addLast(“hiHandler”, new HiHandler());
return pipeline;
}
}); | | —- |


handler加到了pipeline(管道)里面了,所以一个管道里面会有多个handler,不要看有些类是StringDecoder或者是StringEncoder,但是点击去StringDecoder和StringEncoder源码进去看它们也是继承了ChannelHandler,所以它们都是handler

结论,一个管道中会有多个handler,

handler和handler之间是如何流转的?

Netty[笔记] - 图36
结论:
当客户端发送一个消息到服务端以后,netty会把整个消息对象封装成一个channelEvent ,然后把channelEvent传递到整个管道(pipeline)里面,pipeline里面会有很多handler,消息进入管道以后handler1会先去处理,handler1处理完消息以后它会产生一个新的事件channelEvent1然后把这个事件会往下面的handler2里面传递,handler2处理完以后不一定会产生一个事件,可能会产生多个事件,比如说当前handler处理完之后要产生两个channelEvent事件对象,两个事件对象都会接着往handler3里面传递,handler3就会收到两个事件对象,handler3处理完可能会产生新的事件对象然后接着往下传递.直到最后一个handler处理完所有的事件对象就会结束.

事件的源头是在哪里?
是在抽象的selector类里面或者是在worker线程里面,worker是处理所有客户端的一个读写的线程.


handler往下传递对象的方法是sendUpstream(event)


| public class MyHandler1 extends SimpleChannelHandler {

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {

ChannelBuffer buffer = (ChannelBuffer) e.getMessage();

byte[] array = buffer.array();
String message = new String(array);
System.out.println(“handler1:” + message);

//传递消息给下一个handler
ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), “abc”, e.getRemoteAddress()));
ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), “efg”, e.getRemoteAddress()));
} | | —- |

| /**

  • MyHandler1的传递过来的信息在MyHandler2里面会进行处理
    /
    public class MyHandler2 extends SimpleChannelHandler {

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {

    String message = (String) e.getMessage();

    System.out.println(*”handler2:”
    + message);
    } | | —- |


    server端

    给handler加到管道里面

| //设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {

@Override
public ChannelPipeline getPipeline() throws Exception {

ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast(“handler1”, new MyHandler1());
pipeline.addLast(“handler2”, new MyHandler2());
return pipeline;
}
}); | | —- |


(三)socket字节流攻击


解决粘包分包现象是:
长度+数据
这种方式正常情况下是没问题的
但是会有一些特殊的情况,比如说客户端发的数据不对,本来的数据长度是10个字节,但是发的长度只有八个长度. 然后服务端读的时候数据就混乱了.

还有就是假如有人知道你的数据包是这种结构,他可能会恶意攻击,他可能给你的长度声明的很大,比如说:

长度声明 Integer.max 就是int的最大长度,服务端的内存是受不了的,缓冲区会被塞爆的.这种称之为Socker攻击(字节流式的攻击),

怎么避免?

设定一定的大小,让buffer不能缓冲那么大数据,正常情况下客户端请求的数据长度也就是20~50字节,那么我们服务端可以设置个2048个字节.
只要你客户端请求的数据超过2048就给你缓存清掉就行.

在FrameDecoder的子类:

| /防止字节流式攻击/
if (buffer.readableBytes() > 2048) {
buffer.skipBytes(buffer.readableBytes());//全部清理掉
} | | —- |


但是存在问题, 把当前的buffer都清除掉以后,可能下次来的数据不是开头数据.


所以数据结构应该定义成:

包头+长度+数据

长度是数据的长度
包头是一个标识,表名这是请求的数据的开头内容.