Netty 快速开始(websocket)

一、网络IO的基本知识与概念

1. 同步、异步、阻塞、非阻塞概念

怎样理解阻塞非阻塞与同步异步的区别?
参考URL: https://www.zhihu.com/question/19732473

  • 同步和异步
    同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO 操作并等待或者轮询的去查看IO 操作是否就绪,而异步是指用户进程触发IO 操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO 完成的通知。

  • 阻塞和非阻塞
    阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。

总结: 同步异步与阻塞非阻塞的主要区别是针对对象不同

同步异步是针对调用者来说的。同步与异步:针对数据访问的方式,程序是主动去询问操作系统数据准备好了么,还是操作系统在数据准备好的时候通知程序。
 阻塞非阻塞是针对被调用者来说的。阻塞与非阻塞:针对函数(程序)运行的方式,在IO未就绪时,是等待就绪还是直接返回(执行别的操作)。比如: 被调用者收到一个请求后,做完请求任务后才给出反馈就是阻塞,收到请求直接给出反馈再去做任务就是非阻塞

IO多路复用是同步阻塞模型还是异步阻塞模型?

同步是需要主动等待消息通知,而异步则是被动接收消息通知,通过回调、通知、状态等方式来被动获取消息。IO多路复用在阻塞到select阶段时,用户进程是主动等待并调用select函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式。

2. IO模型

聊聊Linux 五种IO模型
参考URL: https://www.jianshu.com/p/486b0965c296
聊聊同步、异步、阻塞与非阻塞
参考URL: https://my.oschina.net/xianggao/blog/661085
[推荐,写的清晰简单]十年架构经验工程师,带你读懂IO/NIO模型
参考URL: https://baijiahao.baidu.com/s?id=1646205346507623593&wfr=spider&for=pc

  1. 阻塞IO
    如果数据没有准备就绪,就一直等待,直到数据准备就绪;整个进程会被阻塞。

    典型的阻塞IO模型的例子为: data = socket.read();如果数据没有就绪,就会一直阻塞在 read()方法。

  2. 非阻塞IO
    需不断询问内核是否已经准备好数据,非阻塞虽然不用等待但是一直占用CPU。

    典型的非阻塞IO模型一般如下:

    while(true){
    data = socket.read();
    if(data != error){
    // 处理数据 break;
    }
    }
    

    非阻塞IO又一个非常严重的问题,在while 循环中需要不断的去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。

  3. 多路复用IO NIO
    多路复用IO模型是目前使用的比较多的模型。Java NIO实际上就是多路复用IO,在多路复用IO模型中会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。

    在Java NIO中,是通过 selector.select()去查询每个通道是否有达到事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。

    多路复用IO为何比非阻塞IO模型的效率高?是因为在非阻塞IO中,不断的询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。

  4. 信号驱动IO模型
    在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

  5. 异步IO模型(asynchronous I/O)
    异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事情。而另一方面,从内核的角度,当它受到一个asynchronout read之后,它会立刻返回,说明read请求已经成功发起来,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。
    也就是说用户线程完全不需要实际的整个IO操作是如何进行的,只需要发起一个请求,当接收内核返回的成功信号时表示IO操作已完成,可以直接去使用数据了。也就是说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用IO函数进行实际的读写操作。注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。

3. NIO和IO有什么区别?

NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。

在这里插入图片描述

  1. 面向流与面向缓冲

    Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。

    Channel是传统IO中的Stream(流)的升级版,Stream是单向的、读写分离,Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。

  2. 阻塞与非阻塞IO
    Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

  3. 选择器(Selectors)
    Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

    使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。

4. Java NIO 工作流程

【推荐,写的非常清晰】Java NIO之Selector(选择器)
参考URL: https://www.cnblogs.com/snailclimb/p/9086334.html

在这里插入图片描述
工作原理核心就2个概念 ,一个是 Selector(选择器),一个是通道Channel

  • Selector(选择器)
    其中select 调用可能是阻塞的,也可以是非阻塞的。但是read/write是非阻塞的!

    Selector(选择器)的使用方法介绍
    参考URL: https://www.cnblogs.com/snailclimb/p/9086334.html

    源码关键字: Selector、SelectableChannel

  • 通道Channel
    主要实现类:

    • FileChannel:用于读取、写入、映射和操作文件的通道。
    • DatagramChannel:通过UDP读写网络中的数据通道。
    • SocketChannel:通过tcp读写网络中的数据。
    • ServerSocketChannel:可以监听新进来的tcp连接,对每一个连接都创建一个SocketChannel。

二、netty

1. 什么是netty?

官网: https://netty.io/

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty是一个异步事件驱动的网络应用框架
用于快速开发可维护的高性能协议服务器和客户端。

在实际的网络开发中,其实很少使用Java NIO原生的API。主要有以下原因:

  • 原生API使用单线程模型,不能很好利用多核优势,如果自己去写多线程结合起来比较麻烦;
  • 原生API是直接使用的IO数据,没有做任何封装处理,对数据的编解码、TCP的粘包和拆包、客户端断连、网络的可靠性和安全性方面没有做处理;

Netty基于Java NIO,并且做了一些优化,netty提供更高层次的封装,提供更为丰富的功能。

2. 什么是Channel?

自顶向下深入分析Netty(六)–Channel总述
参考URL: https://www.cnblogs.com/549294286/p/10785365.html

在JDK中就有Channel的概念了. 数据的读写都要通过Channel进行。Netty对JDK原生的ServerSocketChannel进行了封装和增强封装成了NioXXXChannel。 一个是服务端Chanel(NioServerSocketChannel),另一个是客户端Channel(NioSocketChannel)。

Netty的Channel只是把新的和老的IO进行了更高层的封装。在 Netty 中, Channel 是一个 Socket 连接的抽象, 它为用户提供了关于底层 Socket 状态(是否是连接还是断开) 以及对 Socket 的读写等操作。 每当 Netty 建立了一个连接后, 都会有一个对应的 Channel 实例。

并且,有父子channel 的概念。 服务器连接监听的channel ,也叫 parent channel。 对应于每一个 Socket 连接的channel,也叫 child channel。

相对于原生的JdkChannel, Netty的Channel增加了如下的组件

  • id 标识唯一身份信息
  • 可能存在的parent Channel
  • 管道 pepiline
  • 用于数据读写的unsafe内部类
  • 关联上相伴终生的NioEventLoop

Channel通过ChannelPipeline中的多个Handler处理器,Channel使用它处理IO数据。

Channel中的所有Io操作都是异步的,一经调用就马上返回,于是Netty基于Jdk原生的Future进行了封装, ChannelFuture, 读写操作会返回这个对象,实现自动通知IO操作已完成。

当一个Channel不再使用时,须调用close()或者close(ChannelPromise)方法释放资源。

总结:在netty中,Channel相当于一个Socket的抽象,它为用户提供额关于Socket状态(连接是否断开)以及对Socket的读、写等操作。每当Netty创建一个连接,都创建一个与其对应的Channel实例。

Netty-Channel架构体系

[推荐-作者写的很全面]深入理解 Netty-Channel架构体系
参考URL: https://www.cnblogs.com/ZhuChangwu/p/11204057.html
[推荐-作者写的很全面-源码分析]自顶向下深入分析Netty(六)–Channel总述
参考URL: https://www.cnblogs.com/549294286/p/10785365.html

如下图:从顶级接口Channel开始,在接口中定义了一套方法当作规范,紧接着的是来两个抽象的接口实现类,在这个抽象类中对接口中的方法,进行了部分实现,然后开始根据不同的功能分支,分成服务端的Channel和客户端的Channel
在这里插入图片描述
Channel的分类:
根据服务端和客户端,Channel可以分成两类(这两大类的分支见上图):

  • 服务端: NioServerSocketChannel
  • 客户端: NioSocketChannel
  1. AbstractChannel
    AbstractChannel实现Channel接口,比较重要的对象是pipeline和unsafe,它们提供对read,write,bind等操作的具体实现。

  2. AbstractNioChannel
    AbstractNioChannel继承AbstractChannel,从这个类开始涉及到JDK的socket。

    这里定义真正的Socket Channel(SelectableChannel),关心的事件,注册后的key。将Socket设置为非阻塞,这是所有异步IO的关键。这里重点要关注一下register函数,这个函数是将Channel和事件循环进行关联的关键。每个事件循环都有一个自己的selector,channel实际上是注册到了相应eventloop的selector中,这也是Nio Socket编程的基础。
    从这个类中已经可以看到netty的channel是如何和socket 的nio channel关联的了,以及channel是如何和eventloop关联的了。

Unsafe

Channel重要的内部接口 unsafe
Netty中,真正帮助Channel完成IO读写操作的是它的内部类unsafe。

Unsafe?直译中文为不安全,这曾给我带来极大的困扰。如果你是第一次遇到这种接口,一定会和我感同身受。一个Unsafe对象是不安全的?**这里说的不安全,是相对于用户程序员而言的,也就是说,用户程序员使用Netty进行编程时不会接触到这个接口和相关类。**为什么不会接触到呢?因为类似的接口和类是Netty的大量内部实现细节,不会暴露给用户程序员。

源码如下, 很多重要的功能在这个接口中定义, 下面列举的常用的方法

interface Unsafe {
//  把channel注册进EventLoop
void register(EventLoop eventLoop, ChannelPromise promise);
 
 // todo 给channel绑定一个 adress,
void bind(SocketAddress localAddress, ChannelPromise promise);

// 把channel注册进Selector
void deregister(ChannelPromise promise);

// 从channel中读取IO数据
void beginRead();

// 往channe写入数据
void write(Object msg, ChannelPromise promise);
...
...

接着往下看,下面来到Channel接口的直接实现类,AbstractChannel 他是个抽象类, AbstractChannel重写部分Channel接口预定义的方法, 它的抽象内部类AbstractUnsafe实现了Channel的内部接口unsafe

总结: channel的io操作是unsafe内部类完成的。服务端从channel,读取出新连接NioMessageUnsafe,客户端从channel,读取出数据NioByteUnsafe。

3. EventLoop 线程与线程组

在Netty 中,每一个 channel 绑定了一个thread 线程。一个 thread 线程,封装到一个 EventLoop , 多个EventLoop ,组成一个线程组 EventLoopGroup。

EventLoop 这个相当于一个处理线程,是Netty接收请求和处理IO请求的线程。 EventLoopGroup 可以理解为将多个EventLoop进行分组管理的一个类,是EventLoop的一个组。

三、netty常用类

1. Bootstrap

[推荐]Bootstrap — 客户端
参考URL: https://www.jianshu.com/p/63b890528766
Netty源码分析之客户端启动流程(Bootstrap)
参考URL: https://www.jianshu.com/p/22bea53023c8

Bootstrap 是 Netty 提供的一个便利的工厂类,可以通过它来完成 Netty 的客户端Netty 初始化。

1.1 netty client 连接超时设置

bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);

1.2 对 connect 的 future 设置监听器

Bootstrap bootstrap = new Bootstrap();
...
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
bootstrap.connect("hostname", 80).addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        if (!future.isSuccess()) {
            System.err.println("Connect to host error: " + future.cause());
        }
    }
});

1.3 连接服务器后,代码让阻塞等待

很多Netty的例子都在末尾加上了这句话:future.channel().closeFuture().sync();
不是没执行,是主线程到这里就 wait 子线程退出了,子线程才是真正监听和接受请求的。

            // 链接服务器
            ChannelFuture f = bootstrap.connect(host, port).sync();
            // 阻塞,等待客户端链路关闭;连接断开时,这句代码才会往下执行
            f.channel().closeFuture().sync();

2. ServerBootstrap

Netty Bootstrap(图解)|秒懂
参考URL: https://www.cnblogs.com/crazymakercircle/p/9998643.html#eventloop-%E7%BA%BF%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B%E7%BB%8

3. netty中的ByteBuf

netty中的ByteBuf
参考URL: https://www.cnblogs.com/duanxz/p/3724448.html

网络数据的基本单位总是字节。Java NIO 提供了 ByteBuffer 作为字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。

Netty 的 使用的是 ByteBuf,用于替代JDK的 ByteBuffer ,一个强大的实现,既解决了 JDK API 的局限性, 又为网络应用程序的开发者提供了更好的 API。

总结:ByteBuf是Netty整个结构里面最为底层的模块,主要负责把数据从底层I/O 读到ByteBuf,然后传递给应用程序,应用程序处理完成之后再把数据封装成ByteBuf写回I/O。

3.1 ByteBuf的创建

Netty中设计了一个专门负责分配ByteBuf的接口:ByteBufAllocator。该接口有一个抽象子类和两个实现类,分别对应了用来分配池化的ByteBuf和非池化的ByteBuf。

有了Allocator之后,Netty又为我们提供了两个工具类:Pooled、Unpooled,分类用来分配池化的和未池化的ByteBuf,进一步简化了创建ByteBuf的步骤,只需要调用这两个工具类的静态方法即可。

以Unpooled类为例,查看Unpooled的源码可以发现,他为我们提供了许多创建ByteBuf的方法,但最终都是以下这几种,只是参数不一样而已:

// 在堆上分配一个ByteBuf,并指定初始容量和最大容量
public static ByteBuf buffer(int initialCapacity, int maxCapacity) {
    return ALLOC.heapBuffer(initialCapacity, maxCapacity);
}
// 在堆外分配一个ByteBuf,并指定初始容量和最大容量
public static ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
    return ALLOC.directBuffer(initialCapacity, maxCapacity);
}
// 使用包装的方式,将一个byte[]包装成一个ByteBuf后返回
public static ByteBuf wrappedBuffer(byte[] array) {
    if (array.length == 0) {
        return EMPTY_BUFFER;
    }
    return new UnpooledHeapByteBuf(ALLOC, array, array.length);
}
// 返回一个组合ByteBuf,并指定组合的个数
public static CompositeByteBuf compositeBuffer(int maxNumComponents){
    return new CompositeByteBuf(ALLOC, false, maxNumComponents);
}

4. EventLoopGroup

当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。

为了解决上述问题,Netty采用了串行化设计理念,从消息的读取、编码以及后续 ChannelHandler 的执行,始终都由 IO 线程 EventLoop 负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。

EventLoopGroup 是一组 EventLoop 的抽象,一个 EventLoopGroup 当中会包含一个或多个 EventLoop,EventLoopGroup 提供 next 接口,可以从一组 EventLoop 里面按照一定规则获取其中一个 EventLoop 来处理任务。

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

  1. 一个EventLoopGroup包含一个或者多个EventLoop。
  2. 一个EventLoop在它的生命周期内只和一个Thread绑定。
  3. 所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理。
  4. 一个Channel在它的生命周期内只注册于一个EventLoop。
  5. 一个EventLoop可能会被分配给一个或多个Channel。

在这种设计中,一个给定Channel的I/O操作都是由相同的Thread执行的,实际上消除了对于同步的需要。

总结: EventLoop和Thread是一对一绑定的。一个netty程序启动时,至少要指定一个EventLoopGroup。

4.1 EventLoop 任务执行者 - NioEventLoop

【Netty】学习NioEventLoop
参考URL: https://www.jianshu.com/p/8a7519c8997d

  • nioEventloop(简称loop),其内部持有一个thread对象,而该loop的run方法正是由该线程执行的。
  • loop还持有两个最重要的对象–selector和queue,前者就是我们的多路复用器后者则是存放我们的任务队列。

NioEventLoop启动后主要的工作

1.select() -- 检测IO事件,轮询注册到selector上面的io事件
2.processSelectedKeys() -- 处理io事件
3.runAllTasks() -- 处理外部线程扔到TaskQueue里面

四、websocket

1. 什么是websocket

websocket,在单个TCP链接上进行全双工的通讯协议,使客户端和服务器端能够实时通信。

  • HTTP是运行在TCP协议传输层上的应用协议,而WebSocket是通过HTTP协议协商如何连接,然后独立运行在TCP协议传输层上的应用协议。
  • Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
  • websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信。

2. 相关常用类

2.1 SimpleChannelInboundHandler

SimpleChannelInboundHandler 继承自 ChannelInboundHandlerAdapter

SimpleChannelInboundHandler是抽象类,而ChannelInboundHandlerAdapter是普通类。

SimpleChannelInboundHandler支持泛型的消息处理,而ChannelInboundHandlerAdapter不支持泛型

重写 channelRead0() 方法

在客户端,当 channelRead0() 方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。

3. netty websocket客户端使用流程

[推荐-写的比较详细]WebSocket快速上手
参考URL: https://www.pianshen.com/article/9344104913/
基于Netty的websocket client 和server
参考URL: https://blog.csdn.net/u010939285/article/details/81231221

建议参考作者文章。

总结:实现分成客户端 和服务器端。

3.1 maven 引入

maven 引入

        <!--netty-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId> <!-- Use 'netty-all' for 4.0 or above -->
            <version>4.1.34.Final</version>
        </dependency>

3.2 netty websocket客户端实现

1)客户端的核心类就是自定义一个XxxxWebSocketClientHandler 类继承SimpleChannelInboundHandler。
客户端中WebSocketClientHandler实现中的this.handshaker,是通过WebSocketClientHandshakerFactory工厂类创建,传参定义了一些握手细节。

   @Override
   /**
    * 连接建立成功后,发起握手请求
    */
   public void channelActive(ChannelHandlerContext ctx) throws Exception {
       System.out.println("连接成功!" + ctx.name());
       this.handshaker.handshake(ctx.channel());
   }

	WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, true, EmptyHttpHeaders.INSTANCE, 1280000)

2) websocket客户端往服务器端写消息
使用Channel类实例channel的 writeAndFlush方法写消息给服务器端。

	channel.writeAndFlush(new TextWebSocketFrame(msg));

3.3 过程常见问题总结

Netty handler中 channelInactive方法不断触发

Netty:channelInactive、exceptionCaught方法不断触发
参考URL: https://www.jianshu.com/p/903498747f47

自定义类继承SimpleChannelInboundHandler,覆盖channelInactive方法。

    /**
     * 客户端与服务器端断开连接的时候调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.warn("websocket client disconnected.");
        client.start();
    }

Netty 是先将 Channel 关闭后,再回调 channelInactive 的,也就是说执行到 channelInactive 时,channel早就关闭了! 但是什么导致channel被关闭呢?

总结: 关闭Channel之前,先清除掉Channel中的各种handler。

注意: 经过测试:channelInactive方法只有在 bootstrap.connect成功建立连接之后,关闭channel时才会触发。举例,bootstrap.connect如果因为timeout 连接失败,说明channel都没有创建成功,这种情况是不会调channelInactive方法的。所以一些重连场景需要考虑这种情况,如果第一次就没有连接上,就不能依靠该方法尝试重连。该方法只适合连接成功了,channel关闭后,回调channelInactive方法,你编码让重连场景。

java.nio.channels.ClosedChannelException异常

情况一:有可能是你的代码有问题,也有可能仅是客户端主动关闭了连接,导致服务端的写失败。
情况二:
在开发过程中,进行单元测试时,你的另一个项目在跑,占用了连接,造成nio读写操作失败,将另一个项目停止解决。

五、参考

Netty 4.0中的那些变化
参考URL: https://blog.csdn.net/alex_xfboy/article/details/93920019

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章