netty学习笔记

预备

  • 什么是Netty?
    异步的、基于事件驱动的网络应用框架,用于快速开发高性能、高可靠的网络IO程序
    主要针对TCP协议下,面向Clients端的高并发应用
    本指是一个基于TCP的NIO框架,只是对原生的NIO做了一个封装。

  • 应用场景
    高性能RPC框架必不可少,例如Dubbo
    游戏行业中,处理大并发

I/O模型

Java共支持3中网络编程模型I/O模式:BIO、NIO、AIO

  • BIO(同步阻塞)
    服务器实现模式为一个连接一个线程

  • NIO(同步非阻塞)
    一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器上(selector),多路复用器轮询到连接有IO请求就进行处理,适合于连接数目多连接比较短的架构,例如聊天服务器弹幕系统等

  • AIO(异步非阻塞)
    AIO引入异步通道的概念,采用了Proactor模式,简化了程序的编写,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

NIO

  • NIO相关的类都放在java.nio包及其子包下

  • NIO三大核心:Channel、Buffer、Selector

  • NIO是面向缓冲区或面向块编程的,数据读取到一个缓冲区,需要时可在缓冲区前后移动

  • 三大组件的关系

    
    graph
    Thread ---> Selector
    Selector ---> Channel1
    Selector ---> Channel2
    Selector ---> Channel3

Channel1 —> Buffer1
Channel2 —> Buffer2
Channel3 —> Buffer3

Buffer1 —> Client1
Buffer2 —> Client2
Buffer3 —> Client3


1. 每一个Channel都会对应一个Buffer
2. Selector对应一个线程,一个线程对应多个Channel(连接)
3. 该图反映了三个Channel注册到该Selector
4. 程序切换到哪个Channel是由事件(Event)决定的
5. Selector会根据不同的事件在各个Channel上切换
6. Buffer本质是一个数组
7. 数据的读取和写入都是要通过Buffer,通过flip方法切换

##### Buffer
本质是一个可以读写的数据块,可以理解成一个容器对象(包含了一个数组),该对象提供了一组方法可以轻松的使用内存块。缓冲区对象内置了一些机制,能够追踪和记录缓冲区的状态变化情况,JDK提供了除了boolean之外的其他七种基本类型的Buffer

```java
public static void main(String[] args) {
    IntBuffer buffer = IntBuffer.allocate(3);  
    for (int i = 0; i < buffer.capacity(); i++) {        
       buffer.put(i);    
    }    
    // 读写转换(重要)
    buffer.flip();    

    // 是否还存在值
    while (buffer.hasRemaining())  {        
        // get方法内部存在一个游标        
        System.out.println(buffer.get());    
    }
}
  • Buffer类(所有Buffer父类)定义了所有缓冲区都具有的四个属性
    1. Capacity:容量,创建时指定,不能改变
    2. Limit:缓冲区的当前终点,不能对超过该值的位置进行读写操作
    3. Position:游标,每次读写数据都会改变
    4. Mark:标记,很少主动修改
Channel

类似于流,但可以双向读写(实际上channel是Stream中的一个属性)
Channel在Java中是一个接口,常用的实现类:FileChannel、ServerSocketChannel、SocketChannel等
使用transferFrom/transterTo方法实现文件的快速拷贝
提供了MappedByteBuffer支持直接在堆外内存中修改文件,而不用操作系统多拷贝一次

本地文件写示例:
file

关于Buffer和Channel的注意事项和细节
  1. ByteBuffer支持类型化put和get,例如intput,doubleput等,put放入的是什么类型,get就应该按put的顺序使用相应的类型,否则会造成BufferUnderflowException
  2. 可以将一个普通的Buffer转换成只读的Buffer
  3. NIO还童工了MappedByteBuffer,可以让文件直接在堆外内存进行修改
  4. NIO还支持通过多个Buffer即Buffer数组来完成读写操作,即Scattering和Gathering(channel的read和write方法支持传入一个buffer数组,仅此而已)
  5. 在Channel的两端都存在Buffer
Selector

能够检测到多个注册的通道上是否有事件发生(注册的时候会传入需要监听的事件ID),只有在channel有真正的读写事件时才会读写
Selector内部有一个专门存放SelectionKey集合,内部的SelectionKey代表一个发生事件的channel,同时该SelectionKey会携带发生的事件ID。调用select()方法返回所有发生事件的channel集合
file

  • 能注册的事件
    1. read id为1 有数据读入
    2. write id为4 有数据读出
    3. connect id为8 有新的channel已经建立
    4. accept id为16 有新的连接可以accept,监听到该事件就需要手动创建channel进行连接
NIO与零拷贝

java程序中,常用的零拷贝有mmap(内存映射)和sendFile
零拷贝:从操作系统角度看,是没有CPU拷贝的意思,即内核缓冲区中没有重复的数据

  • mmap
    mmap通过内存映射,使用DMA技术将文件映射到内核缓冲区中(原本还应该从内核缓冲区复制到用户缓冲区),同时,用户空间可以共享内核空间的数据(涉及CPU的内核态和用户态),可以减少内核空间到用户空间的拷贝次数(不是真正的零拷贝),适合小文件传输

  • sendFile
    数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于用户态完全无关,就减少了一次上下文切换(不是真正的零拷贝),适合大文件传输

  • NIO使用channel的transferTo()方法使用零拷贝
    linux中,任何文件只调用一次该方法即可,windows中,该方法最大传输8M文件,更大的文件需要分段传输

Netty概述

  • 原生NIO存在的问题
    1. NIO的类库和API使用麻烦
    2. 需要具备额外技能,要熟悉java多线程编程,因为NIO涉及到Reactor模型,所以必须要对多线程和网络编程很熟悉
    3. 开发工作量和难度较大
    4. NIO的bug,例如Epoll bug,它会导致Selector空轮询,最终导致CPU 100%

file

Netty的高性能架构设计
目前存在的线程模型
  1. 传统阻塞IO模型
  2. Reactor模式,根据Reactor的数量和处理资源池线程的数量不同,有3中典型实现
    1. 单Reactor单线程
    2. 单Reactor多线程
    3. 主从Reactor多线程,Netty线程模型主要是基于主从Reactor多线程模型,并作出一定改进
  • Reactor模式

    1. 基于IO复用模型,多个连接公用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接,当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
    2. 基于线程池复用线程资源,不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程处理,一个线程可以处理多个连续的业务
  • 单Reactor单线程
    当Reactor监听到事件后,如果时accept请求,则交给Acceptor处理连接请求,如果是读写事件,则将请求转发给Handler(因为是单线程,所以Handler只有一个),Handler负责处理对应的读或写的业务。在该模型中,只有一个线程负责Handler,所以当有大量请求时,仍然会发生阻塞

  • 单Reactor多线程
    当Reactor监听到事件后,如果时accept请求,则交给Acceptor处理连接请求,如果是读写事件,则创建一个新的Handler,Handler读写数据之后,会分发给后面的Worker线程池中的某个线程处理业务。因为Worker是由多个线程处理的,故能承受更大的并发。

  • 主从Reactor多线程
    单Reactor遇到高并发时,单个Reactor容易成为瓶颈。故在单Reactor多线程的基础上,创建了一个主Reactor,所有的请求需要先通过主Reactor,主Reactor负责建立连接,再将读写请求转发给从Reactor,这里的从Reactor就是单Reactor多线程中的那个Reactor。可以有多个线程,每个线程都是一个单Reactor多线程的模型,他们均由同一个主Reactor管理
    file

  • Netty模型
    类似于主从Reactor多线程模型,只不过主从Reactor多线程模型中的主Reactor变成了一个包含多个Reactor的对象(NioEventLoopGroup类,称之为Boss Group,其中包含多个NioEventLoop对象,每个NioEventLoop是一个无限循环的事件监听器线程),主从Reactor多线程模型中的从Reactor也是一个NioEventLoopGroup类,称之为Worker Group。当请求过来的时候,Boss Group中的某个Reactor(即Selector)会且只会监听accept请求(连接请求),然后与client建立连接,生成NioSocketChannel对象,并将该对象注册到Worker Group中的某个Selector,通过pipeline中的handler处理读写请求,pipeline中包含了很多的handler及filter等。每个NioSocketChannel都会和一个pipeline双向绑定

    1. BossGroup和WorkerGroup含有的子线程(NioEventLoop)的个数默认是CPU核数 * 2
    2. 默认情况下新的channel会交替使用WorkerGroup中的线程
    3. pipeline本质是一个双向链表,可以多个pipeline连接在一起
    4. 每个NIOEventLoop都有一个taskQueue和一个scheduleTaskQueue,可以将读写操作放在taskQueue中,例如如果是希望十秒之后再返回数据给客户端,或者定时任务,或者推送消息,但是又不想阻塞线程。注:taskQueue只是一个线程,如果创建了两个延时任务,第一个十秒,第二个二十秒,则第二个任务执行需要到第三十秒。
Netty的异步模型

Netty的IO操作都是异步的,包括 Bind、Write、Connect等操作都会简单地返回一个ChannelFuture(接口)
调用者并不能立即得到调用结果,而是通过 Future-Listener 机制,用户可以主动获取或者通过通知机制获取IO操作的结果
Netty的异步模型是建立在future和callback之上的,callback就是回调。Future的核心思想:假设一个方法func非常耗时,就可以再调用func的时候立马返回一个Future,后续可以通过Future取监控func的处理过程(即:Future-Listener机制)

Netty核心模块组件

file

Bootstrap、ServerBootsrp类

一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类
例如:如果要创建一个Netty服务端,则先创建一个BossGroup和一个WorkerGroup类,然后创建一个ServerBootstrap类,在其构造器中传入上述两个Group,然后调用它的channel方法设置服务端通道实现(一般就是NioSocketChannel),再调用其他方法配置诸如pipeline等,调用bind方法设置端口号等

Future、ChannelFuture类

异步操作IO

Channel类

用于处理IO读写请求,不同的协议、不同的阻塞类型需要用不同的channel,例如TCP连接使用NioSocketChannel、UDP连接使用NioDatagramChannel等

Selector

对应Netty模型的BossGroup、WorkerGroup

ChannelHandler接口

处理IO事件或拦截IO事件,并将其转发到其ChannelPipeline中的下一个处理程序,该接口有较多实现,例如
ChannelInboundHandler用于处理入站IO事件
ChannelOutboundHandler用于处理出站IO事件
出站入站指的是如果事件运动方向是从客户端到服务端即为出站
我们通常需要继承这些类然后重写其中的部分事件方法,例如数据读取完毕等事件

Pipeline和ChannelPipeline

ChannelPipeline是一个Handler的集合,负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链
Netty中每个Channel有且仅有一个ChannelPipeline与之对应(双向绑定)
一个Channel包含了一个ChannelPipeLine,而ChannelPipeLine中又维护了一个由ChannelHandlerContext组成的双向链表,并且每个ChannelHandlerContext中又关联着一个ChannelHandler
出站请求从Pipeline中的第一个ChannelHandlerContext走到最后一个ChannelHandlerContext,出站反之

ChannelHandlerContext

保存了Channel相关的所有上下文信息,同时关联一个ChannelHandler对象。即ChannelHandlerContext中包含了一个具体的事件处理器ChannelHandler

EventLoopGroup接口、NioEventLoopGroup实现类

EventLoopGroup是一组EventLoop的抽象,为了利用多核CPU资源,一般有多个EventLoop同时工作,每个EventLoop维护着一个Sekector实例。
Netty一般需要两个EventLoopGroup:BoosEventLoopGroup和WorkerEventLoopGroup。BoosEventLoopGroup只处理Accept事件,WorkerEventLoopGroup处理具体的事件

Unpooled类

Netty提供了一个专门用来操作缓冲区的工具类,例如可以通过给定的数据和字符编码返回一个ByteBuf对象(类似于NIO中的ByteBuffer但是有区别)
file
注:上图中的getByte方法使用错误,应该是readByte(),否则writerIndex不会移动

Netty 编解码机制

编写网络应用程序时,因为数据在网络中以二进制字节码进行传输,所以在发送接收的时候就需要编解码
Codec(编解码器)的组成分为:decoder和encoder

Netty自身提供了一些codec,例如
StringEncoder/StringDecoder:对字符串进行编/解码
ObjectEncoder/ObjectDecoder:对Java对象进行编/解码

  • Netty自带的ObjectEncoder可以实现POJO对象或各种业务对象的编解码,底层使用的任然是Java序列化结束,所以存在以下问题:
    1. 无法跨语言,服务端和客户端必须都是Java
    2. 序列化后体积较大,是二进制编码的5倍多
    3. 序列化性能较低

基于上述问题,Google提出了Protobuf项目,全称 Google Protocol Buffers

Protobuf

Protobuf是一种轻便高效的结构化数据存储格式,但和json不同。
使用Protobuf编译器能自动生成代码,Protobuf是将类的定义使用.proto文件进行描述(需手动写),然后通过protoc.exe编译器根据proto文件自动生成.java文件
同时,需要在netty的pipeline中添加proto的编解码器
file

file

  • .proto文件
    file

但是这样就有一个问题,如何传输多种类型的对象,并且接收端可以判断传输的对象类型
更新后的proto文件
file

发送端可以随机发送不同类型的对象
file

接收端根据解码器解析出的类型进行判断
file

  • 由于不可能直到远程节点是否会一次性发送一个完整的信息,tcp有可能出现粘包和拆包的问题,所以Netty提供了ByteToMessageDecoder这个类对入站数据进行缓冲,直到它准备好被处理,MessageToMessageDecoder就实现了该抽象类。编码器同理

粘包与拆包

TCP粘包和拆包基本介绍

TCP是面向连接,面向流的。收发两端都要有一一对应的socket,因此,发送端为了将多个包更有效地发送给接收端,tcp使用了优化方法(Nagle算法),将多次间隔较小且数据量较小的数据合并成为一个大的数据块,然后进行封包。这种做法虽然提高了效率,但是接收端就难以分辨出完整的数据包了,因为面向流的通信时无消息保护边界的
举例:
file

解决方案

使用自定义协议 + 编解码器 解决,关键是要解决服务端每次读取数据的长度

Leave a Comment