简介: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
4mark:标识
positon:当前数组的索引
limit:当前数组最大
capacity:当前数组的容量
ByteBuffer
Java中的基本数据类型(boolean 除外) ,都有一个Buffer类型与他对应,最常用的是ByteBuffer类
- ByteBuffer支持类型化的put和get,put放入什么类型,get就应该使用相同的类型去取
1 | //创建直接缓冲区 |
Channel
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写
- 通道可以从Buffer读数据,可以写数据到Buffer
- BIO 中Stream 是单项的,Channel 是双向的
FileChannel
1 | //从通道读取数据,并放入缓存区 |
Selector
Java NIO 用非阻塞的方式,使用一个线程,处理多个客户端连接,就会用到Selector.它是一个抽象类
- Selector 能够检测到多个注册通道上是否有事件发生
- 只有在连接真正有读写事件发生时,才会进行读写,
- 避免多线程之间上下文切换
1 | //得到一个Selector对象 |
SelectionKey
抽象类 表示Selector 与通道的注册关系,共四种
1 | public static final int OP_ACCEPT = 1 << 4; //有新的网络连接可以accept |
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
- 在多数情况下,大部分的线程是休眠的,资源的浪费
- 线程需要额外分配不同的栈内存
- 在JVM物理上可支持的最大线程数未到之前,线程的切换已成问题
NIO 优点
- 一个单独的线程可处理多个并发的连接
- 内存的管理与上下文的切换优化明显
- 当没有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 在一个高度上做了两件事情(技术上/结构上)
- 它的异步和事件驱动基于Java NIO实现,在高负载下能保证最好的应用性能和可扩展性
- 它包含了一系列用来解耦应用逻辑和网络层的设计模式,简化了开发的同时最大限度地提升了可测试性,模块化和可重用性。
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步
- 轮询accept事件
- 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个 worker NioEventLoop上的selector
- 处理任务队列的任务, 及runAllTasks
- 每个Worker NioEventLoop 循环执行的步骤
- 轮询read ,write 事件
- 处理IO事件,既read,write 事件,在对应的NioSocketChannel处理
- 处理任务队列的任务,既runAllTasks
- 每个Worker NioEventLoop 处理业务时,会使用pipeline,pipline中包含了channel
- 任务队列中的Task 有3种典型的使用场景
- 用户程序自定义的普通任务
- 用户自定义的定时任务
- 非当前Reactor 线程调用的Channel的各种方法
Netty 核心类
BootStrap ,ServerBootstrap
- 引导,配置整个Netty程序额,串联各个组件,Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类
1 | //该方法用于服务端,用来设置两个EventLoop |
Future,ChannelFuture
Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是Future 和ChanelFuture是,他们可以注册一个监听,当操作执行成功或失败时,监听会自动触发注册的监听事件
1 | Channel channel() 返回当前正在进行的IO操作通道 |
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 | ChannelHandler 是一个接口,处理IO事件或拦截IO操作,并将其转发到其ChannelPipeline 中的下一个处理程序 |
- 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 | ChannelPipeline addFirst(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 | // 关闭通道 |
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 粘包、拆包