Netty笔记

简介:JAVA IO 模型:BIO,NIO,AIO;NIO 核心组件Channel,Buffer,Selector;进行IO编程中,常采用两种模式:Reactor 和 Proactor;Netty 使用,Netty的核心类

Java IO 模型

  • 支持3种模型,BIO(同步阻塞) ,NIO(同步非阻塞) ,AIO(异步非阻塞)
  • NIO 客户端发出请求会注册到多路复用器上,多路复用器轮询到连接有IO操作的进行处理.适用于连接数多,连接时间短的,比如聊天服务器,弹幕系统,服务器之间通信,JDK 1.4 开始支持
  • AIO 引入异步通道的概念,采用Proactor模式,它的特点是先由操作系统完成再去通知服务器程序启动线程处理,适用与连接数多,连接时间长的应用,如相册服务器,JDK 1.7 支持

Java NIO

  • NIO 全称 为 Java non-blocking IO ,从JDK1.4开始,(同步非阻塞)
  • NIO 相关类都在 java.nio 包下
  • NIO 有3大核心组件, Channel Buffer Selector
  • NIO 面向缓冲区/块 编程,数据读取到一个它稍后处理的缓冲区,需要时可以在缓冲区中前后移动,这就增加了处理过程的灵活性,使用它可以提供非阻塞式的高伸缩网络
  • NIO 可以做到用一个线程来处理多个操作,假设有10000个请求过来,可以分配50-100个线程来处理,不像之前阻塞IO那样,必须分配10000个线程
  • Http2.0 使用了多路复用技术,做到了同一个连接并发处理多个请求
  • BIO 以流的方式处理数据,而NIO以块的方式处理数据,块的效率更高
  • BIO 是阻塞的,NIO是非阻塞的
  • BIO 基于字节流和字符流的操作,NIO基于Channel 和Buffer 进行的操作,Selector 用于监听多个通道的事件,因此使用单个线程就可以监听多个客户端通道

NIO 核心组件

Buffer

  • Buffer 就是一个内存块,底层是一个数组
  • Buffer 是一个抽象类,含有4个关键属性
  • 只读Buffer,聚合,分散Buffer
  • MappedByteBuffer
    1
    2
    3
    4
    mark:标识
    positon:当前数组的索引
    limit:当前数组最大
    capacity:当前数组的容量
ByteBuffer

Java中的基本数据类型(boolean 除外) ,都有一个Buffer类型与他对应,最常用的是ByteBuffer类

  • ByteBuffer支持类型化的put和get,put放入什么类型,get就应该使用相同的类型去取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//创建直接缓冲区
public static ByteBuffer allocateDirect(int capacity);
//设置缓冲区初始值
public static ByteBuffer allocate(int capacity);
//把一个数组放到缓冲区使用
public static ByteBuffer wrap(byte[] array,int offset, int length);
//从当前位置positon 上get ,get之后 postion+1
public byte get();
//从绝对位置index 上get
public byte get(int index);
// 从当前位置put,put 之后,put之后 postion+1
public ByteBuffer put(int index, byte b);
// 从绝对位置put
public ByteBuffer put(byte src);

Channel

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写
  • 通道可以从Buffer读数据,可以写数据到Buffer
  • BIO 中Stream 是单项的,Channel 是双向的
FileChannel
1
2
3
4
5
6
7
8
9
10
11
//从通道读取数据,并放入缓存区
public abstract int read(ByteBuffer dst);

//把缓存区的数据写入通道
public abstract int write(ByteBuffer src);

//从目标通道中,复制数据到当前通道
public abstract long transferFrom(ReadableByteChannel src,long position, long count)

//把数据从当前通道复制给目标通道
public abstract long transferTo(long position, long count, WritableByteChannel target)

Selector

Java NIO 用非阻塞的方式,使用一个线程,处理多个客户端连接,就会用到Selector.它是一个抽象类

  • Selector 能够检测到多个注册通道上是否有事件发生
  • 只有在连接真正有读写事件发生时,才会进行读写,
  • 避免多线程之间上下文切换
1
2
3
4
5
6
7
8
9
//得到一个Selector对象
public static Selector open();

//监控所有注册通道,当其中有IO操作时,将对应的SelectorKey加入到内部集合并返回
//无参数为阻塞方法,有时间为多久返回,立即返回的使用selectNow()
public abstract int select();

//从内部集合中得到所有的SelectorKey
public abstract Set<SelectionKey> selectedKeys();
SelectionKey

抽象类 表示Selector 与通道的注册关系,共四种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static final int OP_ACCEPT = 1 << 4; //有新的网络连接可以accept
public static final int OP_CONNECT = 1 << 3;//连接已经建立
public static final int OP_READ = 1 << 0; //读操作
public static final int OP_WRITE = 1 << 2; //写操作

//得到与之关联的Selector对象
public abstract Selector selector();
//得到与之关联的通道
public abstract SelectableChannel channel();
//得到与之关联的共享数据
public final Object attachment();
//设置或改变监听事件
public abstract int interestOps();
//是否可读
public final boolean isReadable() ;
//是否可写
public final boolean isWritable();

NIO 与零拷贝

  • Linux 2.1版本 提供了sendFile函数,其基本原理如下:
    数据根本不经过用户态,直接从内存缓冲区进入Socket Buffer ,同时,由于和用户态无关,就减少了一次上下文切换

Java AIO

  • JDK 7 引入 Asynchronous I/O 既AIO,在进行IO编程中,常采用两种模式:Reactor 和 Proactor ,Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理
  • AIO 即NIO2.0 ,叫做异步不阻塞IO,AIO 引入异步通道的概率,采用了Proactor模式,简化程序编写,有效的请求才会启动线程,他的特点是先由操作系统完成后才通知服务端程序启动线程去处理

IO/NIO/Netty 比较

IO

  1. 在多数情况下,大部分的线程是休眠的,资源的浪费
  2. 线程需要额外分配不同的栈内存
  3. 在JVM物理上可支持的最大线程数未到之前,线程的切换已成问题

NIO 优点

  1. 一个单独的线程可处理多个并发的连接
  2. 内存的管理与上下文的切换优化明显
  3. 当没有IO需要处理的时候,可以被指派其他任务

NIO 缺点

  • NIO 的类库 和 API 繁复,使用复制,需要熟练掌握 Selector, ServerSocketChannel,SocketChannel,ByteBuffer
  • 同时需要熟悉多线程编程,因为NIO编程涉及到Reactor模式
  • 同时需要处理其他的问题,如短线重连,网络闪烁,半包读写,失败缓存,网络堵塞和异常处理
  • JDK NIO 的BUG,如Epoll BUG

Netty

  • 设计

    用于多种传输类型的统一API,包括阻塞和非阻塞。简单但是强大的线程模型真正的无连接数据报
    socket支持链式的支持复用的逻辑组件(Chaining of logic components to support reuse)

  • 易用性

    大量的文档(Javadoc)和例子库除了JDK1.6+没有别的依赖(一些可选特性可能需要Java 1.7+和/或 额外的依赖)

  • 性能

    比core Java APIs更好的吞吐量和低延迟,因为池化和复用,减少了资源消耗,尽可能小的内存拷贝

  • 鲁棒性

    没有因为慢连接,快连接或者超载连接造成的OutOfMemeoryError。 在高速的网络上消除了NIO应用不公平的读/写比例

  • 安全

    完整的SSL/TLS和StartTLS的支持。 适用于受限的环境比如Applet或者OSGI中

Netty 简介

  • Netty 是 jboss 提供的一个Java开源框架,现在为 Github上的独立项目
  • Netty 是一个 异步的 ,基于事件驱动的网络应用框架,用以快速开发高性能,高可靠性的网络IO程序
  • Netty 主要针对在TCP协议下,面向Clients端的高并发应用,或者 Peer-to-Peer场景下的大量数据持续传输的应用
  • Netty 本质是一个NIO框架

Netty和Mina是Java世界非常知名的通讯框架。它们都出自同一个作者,Mina诞生略早,属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户netty.io。 Netty目前有两个分支:4.x和3.x,(5.x已经废弃) 4.0分支重写了很多东西,并对项目进行了分包

Netty 在一个高度上做了两件事情(技术上/结构上)

  1. 它的异步和事件驱动基于Java NIO实现,在高负载下能保证最好的应用性能和可扩展性
  2. 它包含了一系列用来解耦应用逻辑和网络层的设计模式,简化了开发的同时最大限度地提升了可测试性,模块化和可重用性。

Netty 优点

  • 设计优雅:统一API ,阻塞非阻塞,分类关注点,高度可定制的线程模型
  • 使用方便:没有其他依赖,高性能,延迟低,低消耗
  • 安全: 完成的SSL/TLS StartTLS支持

Netty 应用场景

互联网行业

  • 在分布式系统中,各节点之间需要远程调用服务,高性能的RPC框架必不可少,Netty 作为异步高性能通信框架,往往作为基础通信组件
  • 典型的应用:阿里的Dubbo 使用Netty进行通信

游戏行业

  • Netty 作为高性能基础通信组件,提供TCP/UDP 和 HTTP 协议栈,方便定制开发私有协议
  • 地图服务器之间可以方便的通过Netty进行高性能通信

大数据领域

  • Hadoop 的组件 AVRO 是基于Netty 框架的二次封装

Netty 线程模型

目前存在的线程模型:

  • 传统阻塞的IO服务模型
  • Reactor 模式
    • 单Reactor 单线程
    • 单Reactor 多线程
    • 主从Reactor 多线程 (Netty 基于此,做的一些改进)

单Reactor 单线程

优缺点

  • 优点:模型简单,没有多线程,进程通信,竞争问题,全部到在一个线程中完成
  • 缺点:性能问题,只有一个线程,无法发挥多核CPU 的性能,Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件
  • 缺点:可靠性问题,线程意外终止,或进入死循环,会导致整个系统通信模块不可用,不能接收和处理消息,造成节点故障
  • 使用场景:客户端有限,业务处理很快,如Redis在业务处理的时间复杂度

单Reactor 多线程

优缺点

  • 优点:可用充分利用多核cpu的处理能力
  • 缺点:多线程数据共享和访问比较复杂,reactor处理所有的事件监听和响应,在单线程运行,在高并发场景容易出现瓶颈

主从Reactor 多线程

优缺点

  • 优点:父线程与子线程的数据交互简单,职责明确,父线程只需要接受新连接,子线程是完成后续的业务处理
  • 优点:父线程与子线程的数据教会简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据
  • 缺点:编程复杂度高

Reactor 模式具有如下优点

  • 响应快,不必为单个同步事件所阻塞,虽然Reactor本身依然是同步的
  • 可以最大程度的避免复杂的多线程同步问题,避免多线程切换开销
  • 扩展性好,可以方便的通过Reactor实例个数来充分利用CPU资源
  • 复用性好,Reactor模式本身与具体的事件处理逻辑无关,具有很高的复用性

Netty 模型

  • Netty 抽象出两组线程池,BossGroup 专门负责接收客户端的连接,WorkerGroup专门负责网络的读写
  • BossGroup 和 WorkGroup 类型都是 NioEventLoopGroup
  • NioEventLoopGroup 相当于一个事件循环组,这个组含有多个事件循环,每一个事件循环是NioEventLoop
  • NioEventLoop 表示一个不断循环的执行处理任务的线程,每个NioEventLoop 都有一个Selector,用来监听绑定在其上的socket的网络通讯
  • NioEventLoopGroup 可以有多个线程,既可以含有多个NioEventLoop
  • 每个Boss NioEventLoop循环执行的步骤分3步
    1. 轮询accept事件
    2. 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个 worker NioEventLoop上的selector
    3. 处理任务队列的任务, 及runAllTasks
  • 每个Worker NioEventLoop 循环执行的步骤
    1. 轮询read ,write 事件
    2. 处理IO事件,既read,write 事件,在对应的NioSocketChannel处理
    3. 处理任务队列的任务,既runAllTasks
  • 每个Worker NioEventLoop 处理业务时,会使用pipeline,pipline中包含了channel
  • 任务队列中的Task 有3种典型的使用场景
    1. 用户程序自定义的普通任务
    2. 用户自定义的定时任务
    3. 非当前Reactor 线程调用的Channel的各种方法

Netty 核心类

BootStrap ,ServerBootstrap

  • 引导,配置整个Netty程序额,串联各个组件,Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类
1
2
3
4
5
6
7
8
9
10
11
12
13
//该方法用于服务端,用来设置两个EventLoop
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup);
//该方法用于客户端,用来设置一个EventLoop
public B group(EventLoopGroup group);
//设置服务端的通道实现
public B channel(Class<? extends C> channelClass);
//设置业务处理类(自定义的Handler)
public ServerBootstrap childHandler(ChannelHandler childHandler);

//设置服务端占用的端口
public ChannelFuture bind(int inetPort);
//客户端连接服务器
public ChannelFuture connect(String inetHost, int inetPort)

Future,ChannelFuture

Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是Future 和ChanelFuture是,他们可以注册一个监听,当操作执行成功或失败时,监听会自动触发注册的监听事件

1
2
Channel channel() 返回当前正在进行的IO操作通道
ChannelFuture sync() 等待异步操作执行完毕

Channel

NioSocketChannel //异步客户端 TCP Socket连接
NioServerSocketChannel //异步的服务端 TCP Socket 连接
NioDatagramChannel 异步的UDP连接
NioSctpChannel 异步的客户端Sctp连接
NioSctpServerChannel 异步的Sctp 服务器端连接,这些通道涵盖了UDP 和TCP网络IO 及文件IO

Selector

  • Netty 基于Selector 对象实现IO多路复用,通过Selector 一个线程可以监听多个连接的Channel事件
  • 当向一个Selector 中注册Channel 后,Selector 内部的机制就可以自动不断的检查这些注册的Channel是否有已就绪的IO事件,这样程序就可以很简单的使用一个线程高效的管理多个Channel

ChannelHandler 及其实现类

1
2
ChannelHandler 是一个接口,处理IO事件或拦截IO操作,并将其转发到其ChannelPipeline 中的下一个处理程序
ChannelHandler 本身并诶呦提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类

  • CHannelInboundHandler用于处理入站IO事件
  • ChannelOutBoundHandler用于处理出站IO操作
  • ChannelInboundHandlerAdapter 用于处理入站IO事件 -适配器
  • ChannelOutboundHandlerAdapter 用于处理出站的IO操作 -适配器
  • ChannelDuplexHandler 处理出入站事件

Pipeline 和ChannelPipeline

  • 在Netty中的每个Channel 都有且仅有一个 ChannelPipeline与其的对应
  • 一个Channel包含一个ChannelPipeline,而ChannelPipeline 中又维护一个由ChannelHandlerContext 组成的双向链表,并且每个ChannelHandlerContext中又关联着一个CHannelHandler
  • 入站事件和出站事件是一个双向链表,入站事件从链表的head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler 两种类型不干扰
1
2
3
ChannelPipeline addFirst(ChannelHandler ...handlers) 把一个业务处理类,添加到链表中第一个位置

ChannelPipeline addLast(ChannelHandler ...handlers) 把一个业务处理类,添加到链表中最后位置

EventLoopGroup 和其实现类NioEventLoopGroup

  • EventLoopGroup 是一组EventLoop抽象,Netty为更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例
  • EventLoopGroup 提供next 接口,可以从组里按照一定的规律或得其中一个EventLoop来处理任务,在Netty服务端编程中,我们一般都需要提供两个EventLoopGroup 如BossEventLoopGroup and WorkerEventLoopGroup
  • 通常一个服务端口及是一个ServerSocketChannel对应一个Selector和一个EventLoop线程,BossEvenetLoop 负责接收客户端的连接并将SocketChannel 交给WorkerEventLoopGroup
  • BossEventLoopGroup 通常是一个单线程EventLoop, EventLoop维护着一个注册了ServerSocketChannel 的Selector 实例 BossEventLoop不断轮询Selector 将连接时间分离出来

ChannelHandlerContext

  • 保存Channel 相关信息的所有上下文,同时关联一个ChannelHandel对象,同时也绑定了对应的pipeline 和 Channel
1
2
3
4
5
6
// 关闭通道
ChannelFuture close()
//刷新
ChannelOutboundInvoker flush()
//将数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出站)
ChannelFuture writeAndFlush(Object msg)

ChannelOption

Netty 在创建Channel实例后,一般需要设置ChannelOption参数,如下

  • ChannelOption.SO_BACKLOG

    对应的TCP/IP 协议 listen 函数中backlog 参数,用于初始化服务器可连接队列大小,服务端处理客户端连接请求时候,服务端将不能处理的客户端连接是顺序处理的,所以同一时间只能处理一个客户端连接,新的客户端的请求放到队列中等待处理,backlog 参数指定了队列的大小

  • ChannelOption.SO_KEEPALIVE

    一直保持连接活动状态

Unpooled

Netty 提供的一个专门用来操作缓冲区的工具类
通过给定的数据和字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 但有区别)

1
public static ByteBuf copiedBuffer(CharSequence string, Charset charset)

含有3个重要属性
0 – [discarbale bytes] – readerIndex – [readable bytes] – writeIndex – [writeableByte] – capacity

Netty 编解码器

Netty 自身提供的编解码器

  • StringEncoder StringDecoder
  • ObjectEncoder ObjectDecoder

缺陷

Netty 本身自带的ObjectDecoder/Encoder 可以用来实现POJO的编码和解码,其底层实现任然是Java 序列化技术,那么会有以下问题当Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。

  • 无法跨语言
  • 序列化后体积太大,是二进制的5倍多
  • 性能低

Google Protobuf

适合 RPC 的数据交换格式
http+Json -> tcp + protobuf

  • protobuf 是以message 的方式来管理书籍的
  • 支持跨平台,跨语言的
  • 高性能,高可靠
  • 使用protobuf编译器能自动生成代码

自身编解码器

当Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。

Netty提供一系列实用的编解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler

ByteToMessageDecoder , ReplayingDecoder

  • ReplayingDecoder扩展了ByteToMessageDecoder类,使用这个类,我们不必调用readableBytes()方法。参数S指定了用户状态管理的类型,其中Void代表不需要状态管理

  • 局限性

    并不是所有的 ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException。ReplayingDecoder 在某些情况下可能稍慢于ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢

其他编解码器

  • LineBasedFrameDecoder:这个类在Netty内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。

  • DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符。

  • HttpObjectDecoder:一个HTTP数据的解码器

  • LengthFieldBasedFrameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。

TCP 粘包与拆包

TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的

解决方案

使用自定义协议 + 编解码器 来解决
关键就是要解决 服务器端每次读取数据长度的问题, 这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的TCP 粘包、拆包