Netty

发布时间 2023-06-04 10:16:00作者: Bingmous

Netty概述

Java BIO编程

  • 创建ServerSocket,绑定端口,接收客户端连接请求,为每个连接都创建一个线程处理,没有数据交互时进行阻塞

Java NIO编程

  • Non-blocking IO,同步非阻塞
    • 创建ServerSocketChannel,绑定端口,设置非阻塞
    • 创建Selector,向Selector注册ServerSocketChannel,监听事件(OP_ACCEPT,用于接收请求)
    • Selector.select()进行阻塞等待连接
    • 客户端连接后select()响应处理连接请求,接收请求后,将每个请求的channel注册到Selector,监听事件(OP_READ,用于读取数据)
    • Selector有连接事件或者读数据事件时进行处理该事件(如果有多个事件到来分别处理,事件驱动)
  • 三大核心部分:Channel,Buffer,Selector
    • 一个channel对应一个buffer
    • 多个channel可以注册到一个selector上
    • 一个selector对应一个线程
  • 是面向缓冲区或块编程,数据读取到缓冲区,使用缓冲区可以提供非阻塞的高伸缩网络
  • 事件驱动

Buffer

缓存,基本类型除了boolean类型,都有对应的Buffer类

Channel

类似于流,但有以下区别:

  • 通道可以同时进行读写,流只能读或者写
  • 通道可以实现异步读写,流的读写是同步的
  • 通道可以从缓冲区读数据,也可以写数据到缓冲区

常用channel

  • FileChannel(文件读写)
  • DatagramChannel(UDP数据读写)
  • ServerSocketChannel、SocketChannel(TCP数据读写)

Selector

可以注册多个channel到selector(封装成SelectionKey),通过selector的select方法可以获取到有事件发生的channel,返回SelectionKey,进而获取到有事件发的channel

Netty

NIO问题:

  • 类库及api繁杂,使用麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
  • 需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO编程涉及Reactor模式,需要对多线程和网络编程非常熟悉,才能写出高质量的NIO程序
  • 开发工作量和难度非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等
  • NIO的bug:如臭名昭著的epoll bug,导致selector空轮询,最终导致cpu 100%,直到jdk1.7问题仍然存在

Netty是由JBOSS提供的一个Java开源框架,Netty提供异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络IO程序。

Netty可以快速、简单的开发出一个网络应用,简化和流程化了NIO的开发过程。是目前最流行的NIO框架,在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,如elasticsearch、dubbo框架内部都使用了netty

线程模型

线程模型:传统阻塞IO服务模型、Reactor模型

根据reactor的数量和处理资源线程池的数量不同,有三种典型的实现:单Reactor单线程、单Reactor多线程、主从Reactor多线程(nginx、memcached、netty)

netty主要基于主从Reactor多线程模型做了一定的改进,主从Reactor有多个Reactor

Reactor:

  • 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只要在一个阻塞对象等待,无序阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
  • 基于线程池复用线程资源:不必为每个连接创建线程,将连接完成后的业务处理任务分配给线程处理,一个线程可以处理多个连接的业务

netty线程模型:

  • 有两个线程组,bossGroup、workGroup,前者用于处理连接,后者用于网络IO的读写
  • NioEventLoop表示一个不断循环执行处理任务的线程,每个NIOEventLoop都有一个Selector,用于监听绑定在其上的socket网络通道,内部采用串行化设计,从消息的读取、解码、处理、编码、发送,都由该线程NioEventLoop负责
  • NioEventLoopGroup下包含了多个NioEventLoop,每个NioEventLoop包含一个Selector、一个TaskQueue
  • 每个NioEventLoop的Selector上可以注册多个NioChannel,每个NioChannel只会绑定到一个NioEventLoop上(它的Selector),每个NioChannel都会绑定一个自己的ChannelPipline

异步自定义任务

  • 自定义普通任务:ctx.channle().eventLoop().execute()
  • 定时任务:ctx.channle().eventLoop().schedule()

异步模型原理

ChannelFuture,IO操作的异步结果

Netty核心模块组件

  • Bootstrap、ServerBootstrap
  • Future、ChannelFuture
  • Channel,涵盖了tcp及udp网络io及文件io
    • NioSocketChannel,异步客户端tcp socket连接
    • NioServerSocketChannel,异步服务端tcp socket连接
    • NioDatagramChannel,异步udp连接
    • NIOSctpChannel,异步客户端sctp连接
    • NioSctpServerChannel,异步的sctp服务器端连接
  • Selector
    • netty基于Selector对象实现IO多路复用,通过Selector实现一个线程可以监听多个连接的channel事件
    • 向一个Selector注册channel之后,Selector内部的机制就可以自动不断的查询这些注册的channel是否有已就绪的io事件(如可读、可写、连接完成等),这样就可以很简单的使用一个线程高效的管理多个channel
  • ChannelHandler,是一个接口,处理IO事件,并将其转发到ChannelPipeline的下一个处理程序中
  • Pipeline、ChannelPipeline,ChannelPipeline是Handler的一个集合,每个Handler对应一个ChannelHandlerContext(实际实现是DefaultChannelHandlerContext)
  • Unpooled类,netty提供的一个专门用来操作缓冲区的工具类(netty的数据容器)
    • ByteBuf,不需要flip,有读写指针
      • Unpooled.buffer(10),10字节大小的缓存
      • Unpooled.copiedBuffer("hello", StandardCharsets.UTF_8),从字符串创建,实际容量大小可能比内容大

心跳机制

IdleStateHandler

WebSocket长连接

WebSocket是http协议的升级,会通过http返回101状态码进行升级协议

Protobuf

netty提供了一些编解码器:StringEncoder/StringDecoder、ObjectEncoder/ObjectDecoder(底层使用的仍然是java序列化技术,而java序列化本身效率就不高,无法跨语言、序列化后体积大,是二进制编码的5倍多,序列化性能低)

编写proto文件,使用插件生成java实体类,在handler中加入protobuf的编解码器及业务handler

Netty提供的编解码器

  • ReplayingDecoder,扩展了ByteToMessage类,使用这个类就不用调用readableBytes()方法判断字节数了,参数S指定用户状态管理的类型,Void表示不需要状态管理
    • 缺点:
      • 并不是所有的ByteBuf都支持,如果调用了一个不被支持的方法,会抛出UOE
      • 在某些情况下可能稍慢于ByteToMessageDecoder,比如当网络缓慢并且消息格式复杂时,消息会被拆分成多个碎片,速度会变慢
  • LineBasedFrameDecoder,使用行尾控制字符(\n或\r\n)
  • DelimiterBasedFrameDecoder,使用自定义的特殊字符作为消息的分隔符
  • HttpObjectDecoder,http数据的编解码器
  • LengthFieldBasedFrameDecoder,通过指定长度来标识整包消息,这样就可以自动处理粘包和半包消息

TCP粘包和拆包

tcp是面向连接的、面向流的、提供高可靠服务。收发两端都要有一一成对的socket,发送端为了提高发送效率,使用了优化算法nagle算法,将多次间隔较小且数据量小的数据合成一个大的数据块 然后进行封包。虽然提高了效率,但是接收端就很难分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。

由于tcp无消息保护边界,需要在接收端处理消息边界问题,也解释粘包、拆包问题。

使用自定义协议解决

如每次发送消息先发送一个int字节的数据长度,再发送该长度字节的数据

源码分析

启动源码

  • NioEventLoopGroup的创建
  • ServerBootstrap的创建

三大核心组件

  • ChannelPipeline,ChannelHandler,ChannelHandlerContext

心跳handler

  • IdleStateHandler,ReadTimeoutHandler,WriteTimeoutHandler,检测连接有效性

IdleStateHandler,当连接的空闲时间(读或写)太长时,将会触发一个IdleStateEvent事件,可以在Inbound中重写userEventTriggered方法处理该事件

ReadTimeoutHandler,在指定的时间内,如果没有读事件就会抛出异常,并自动关闭连接

WriteTimeoutHandler,一个写操作在指定的时间内没有完成时,就会抛出异常,并关闭连接

EventLoop

线程池

  • handler中加入线程池
    ctx.channel().eventLoop().execute(),实际执行的也是当前handler的线程

  • context中添加线程池

在netty中做耗时的,不可预料的操作,比如数据库、网络请求、会严重影响netty对socket的处理速度。解决办法是将耗时任务添加到异步线程池中

  • handler中加入线程池:在handler中创建一个DefaultEventExecutorGroup,被所有handler共享
  • context中添加线程池:在创建server时,创建一个DefaultEventExecutorGroup,添加handler时指定该group,那么handler的处理会优先使用该group
    • AbstractChannelHandlerContext的invokeChannelRead方法执行时,会判断executor是不是在当前线程,如果在当前线程会直接执行,如果不在当前线程(这个handler在之前提交的group中),会异步提交到group中执行

比较:

  • 在handler中添加异步,更加自由,比如如果需要访问数据库就异步,不需要就不异步,异步会拖长接口响应时间,因为需要将任务放入task中,如果io时间很短,task很多,可能一个循环下来,都没时间执行整个task,导致响应时间过长。
  • 在context中添加时netty的标准方式(加入到队列),这么做会将整个handler都交给设置的业务线程池,不管耗不耗时,都加入到队列,不够灵活
  • 从灵活性考虑,第一种较好

自定义协议实现RPC

  • 定义公共接口,消费者端使用代理获取服务代理,在进行rpc时连接服务端进行通信调用