Netty学习文档
说到Netty, 我们应该知道Netty是基于NIO的再一次封装,那么,先让我们了解一下BIO,NIO的一些概念。
从下网上是java io 编程的发展体系图
- BIO
1.1 bio的基本介绍
- Java BIO就是传统的Java I/O编程,其相关类和接口在Java.io包下面。
- BIO(BlockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。
1.2 bio线程模型和工作机制
- 服务器端启动一个 ServerSocket。
- 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
- 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,再继续执行。
1.3 bio的一些问题
- 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write。
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
- NIO
2.1 NIO的基本介绍
- Java NIO 全称 Java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 NewIO),是同步非阻塞的。
- NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。【基本案例】
- NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器) 。
- NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
- Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
- HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。
2.2 NIO与BIO比较
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
- BIO 是阻塞的,NIO 则是非阻塞的。
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
- Buffer和Channel之间的数据流向是双向的
2.3 NIO 三大核心原理示意图
- 每个 Channel 都会对应一个 Buffer。
- Selector 对应一个线程,一个线程对应多个 Channel(连接)。
- 该图反应了有三个 Channel 注册到该 Selector //程序
- 程序切换到哪个 Channel 是由事件决定的,Event 就是一个重要的概念。
- Selector 会根据不同的事件,在各个通道上切换。
- Buffer 就是一个内存块,底层是有一个数组。
- 数据的读取写入是通过 Buffer,这个和 BIO是不同的,BIO 中要么是输入流,或者是输出流,不能双向,但是 NIO 的 Buffer 是可以读也可以写,需要 flip 方法切换 Channel 是双向的,可以返回底层操作系统的情况,比如 Linux,底层的操作系统通道就是双向的。
- Netty
3.1 Netty的介绍
- Netty 是由 JBOSS 提供的一个 Java 开源框架,现为 Github 上的独立项目。
- Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
- Netty 主要针对在 TCP 协议下,面向 Client 端的高并发应用,或者 Peer-to-Peer 场景下的大量数据持续传输的应用。
- Netty 本质是一个 NIO 框架,适用于服务器通讯相关的多种应用场景。
3.2 Netty的应用场景
互联网行业
- 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
- 典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框 架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。
游戏行业
- 无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。
- Netty 作为高性能的基础通信组件,提供了 TCP/UDP 和 HTTP 协议栈,方便定制和开发私有协议栈,账号登录服务器。
- 地图服务器之间可以方便的通过 Netty 进行高性能的通信。
大数据领域
- 经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信。
- 它的 NettyService 基于 Netty 框架二次封装实现。
3.3 为什么使用Netty
原生NIO存在的问题
- NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
- 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU100%。直到 JDK1.7 版本该问题仍旧存在,没有被根本解决。
Netty的优点
- Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。
- 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池。
- 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK5(Netty3.x)或 6(Netty4.x)就足够了。
- 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
- 安全:完整的 SSL/TLS 和 StartTLS 支持。
- 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。
3.4 线程模型
Netty的线程模型是基于主从Reactor的,所以我们得先了解一下Reactor线程模型,分为三种,①单Reactor单线程模型,②单Reactor多线程模型,③主从Reactor多线程模型
·单线程Reactor,模型图如下
多个客户端请求链接,然后Reactor通过selector轮询判断哪些通道是有事件发生的,如果是连接事件,就到Acceptor中建立连接;如果是其他读写事件,就有dispatch分发到对应的handler中进行处理。这种模式缺点就是Reactor和handler在一个线程中,如果Handler阻塞了,那么程序就阻塞了。
·Reactor多线程,模型图如下
处理流程
①多个客户端发起请求
②Reactor对象通过Selector监听客户端事件,通过dispatch进行分发
③如果是连接事件,分发给acceptor,通过accept方法处理连接请求,然后创建一个Handler对象响应事件
④如果不是连接请求,由Reactor对象调用对应handler对象进行处理;handler只响应事件,不做具体的业务处理,handler通过read方法读取数据以后,会把数据分发给线程池中某个线程进行业务处理,并且将处理结果返回给handler
⑤handler收到响应后,通过send方法把处理的结果返回给客户端client
相比于单Reactor单线程,这里将业务处理的事情交给了不同的线程去做,发挥了多核cpu性能,但是Reactor只有一个,所有事件的监听和响应都只有一个Reactor去做,并发性还是不好。
·主从Reactor多线程(Netty线程模型就是基于此),模型图如下
这个模型跟前面的单Reactor多线程模型的区别就是:专门搞了一个MainReactor来处理连接事件,如果不是连接事件就分发给SubReactor进行处理。图中的SubReactor只有一个,但是可以有多个去分发请求,所以性能就上去了。
①优点:父线程与子线程的交互简单、职责明确,父线程负责接收连接,子线程负责完成后续的业务处理;
②缺点:编程复杂度高
Netty线程模型简单版本
- BossGroup 线程维护 Selector,只关注 Accecpt
- 当接收到 Accept 事件,获取到对应的 SocketChannel,封装成 NIOScoketChannel 并注册到 Worker 线程(事件循环),并进行维护
- 当 Worker 线程监听到 Selector 中通道发生自己感兴趣的事件后,就进行处理(就由 handler),注意 handler 已经加入到通道
Netty线程模型复杂版本
- Netty 抽象出两组线程池 ,BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯
- NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
- 每个 BossGroup下面的NioEventLoop 循环执行的步骤有 3 步
- 轮询 accept 事件
- 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 workerGroup NIOEventLoop 上的 Selector
- 继续处理任务队列的任务,即 runAllTasks
- 每个 WorkerGroup NIOEventLoop 循环执行的步骤
- 轮询 read,write 事件
- 处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理
- 处理任务队列的任务,即 runAllTasks
- 每个 Worker NIOEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器。
3.5 Netty的核心组件
3.5.1BootStrap,ServerBootStrap
BootStrap意思是引导,一个Netty程序的开始总是从BootStrap开始,BootStrap作用是配置整个Netty,将各个组件联合起来,BootStrap是客户端的引导类,ServerBootStrap是服务器端的引导类
两个引导类中的一些常见方法和作用如下
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
这个方法应用于服务器端,用来设置两个事件循环组,BossEventGroup,WorkerEventGroup
public B group(EventLoopGroup group)
这个方法应用于客户端,用于客户端EventLoopGroup设置
public B channel(Class<? extends C> channelClass)
用于设置服务器端的通道实现
public <T> B option(ChannelOption<T> option, T value)
给serverChannel添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value)
服务端给接收到的通道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler)
该方法用来设置业务处理类(自定义的handler方法)
public ChannelFuture bind(int inetPort)
该方法用于服务器端,设置占用的端口号
public ChannelFuture connect(String inetHost, int inetPort)
用于客户端,用来与服务器端连接
3.5.2 Future,ChannelFuture
Netty中的所有操作都是异步的,不能立刻得知消息是否被正确执行。但是可以等一会儿,或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,用他们可以注册一个监听,当IO操作执行成功或者失败后自动触发监听事件
常见方法:
Channel channel(),返回当前正在进行 IO 操作的通道
ChannelFuture sync(),等待异步操作执行完毕
3.5.3 Channel
①Netty网络通信的组件,用于执行网络IO操作
②通过Channel可以获得当前网络连接的通道的状态
③通过Channel可以获取网络连接的一些参数,例如缓冲区大小
④Channel提供异步的网络IO操作,如建立连接,读写数据等;调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以在IO操作成功,失败或者取消时调用回调函数通知调用方
⑤不同协议有不同的阻塞类型与之对应
NioSocketChannel 异步的客户端TCP 连接
NioServerSocketChannel 异步的服务端 TCP连接
NioDatagramChannel 异步的 UDP 连接
3.5.4 Selector
Netty基于Selector实现IO多路复用,通过Selector一个线程可以监听多个连接的Channel事件
当向一个Selector注册Channel后,Selector就可以不断的查询这些注册的Channel是否有就绪IO事件,如果有进入响应的channelHandler进行处理,这样就可以一个Selector管理多个Channel
3.5.5 ChannelHandler及其实现类
ChannelHandler是一个接口,处理IO事件或者拦截IO操作,并且将其转发到ChannelpipeLine(业务处理链中的下一个处理程序)
ChannelHandler本身并没有提供多少方法,我们可以自定义一个handler类去继承ChannelInboundHandlerAdapter,然后根据业务去重写里面的方法
3.5.6 PipeLine和ChannelPipeLine
ChannelPipeLine是一个Handler集合,负责处理和拦截入站出站事件的操作;相当于贯穿一整个Netty的链,就相当于时保存channelHandler的list,用于处理和拦截channel的一系列操作
Netty中的每一个channel中有且仅有一个ChannelPipeLine与之对应,对应关系如下图
3.5.7 ChannelHandlerContext
保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
绑定了对应的pipeline和channel的信息,方便对channelHandler进行调用
常用方法
ChannelFuture close(),关闭通道
ChannelOutboundInvoker flush(),刷新
ChannelFuture writeAndFlush(Object msg),将数据写到ChannelPipeline 中当前 ChannelHandler 的下一个 ChannelHandler 开始处理(出站)
3.5.8 ChannelOption
Netty通过ChannelOption设置通道Channel的一些参数,如缓存大小
3.5.9 EventLoopGroup和其实现类NioEventLoopGroup
EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每一个EventLoop维护着一个Selector实例
EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。
通常一个服务端口即一个 ServerSocketChannel 对应一个 Selector 和一个 EventLoop 线程。BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理。
3.6 Netty基本使用
1.引入pom依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.20.Final</version>
</dependency>
2.编写服务端
/**
* netty服务端
* 简单使用
*/
public class Server {
public static void main(String[] args) {
//BossGroup 用于接收客户端连接请求的工作组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//WorkerGroup 用于接收客户端连接后处理读写请求的工作线程组 线程数量可以指定 默认为 cpu核数*2
EventLoopGroup workerGroup = new NioEventLoopGroup(4);
try {
//创建ServerBootstrap 服务端启动引导类 帮助我们创建Netty服务
ServerBootstrap b = new ServerBootstrap();
//用ServerBootstrap 创建启动器
b.group(bossGroup, workerGroup) //绑定工作组
.channel(NioServerSocketChannel.class) //设置通道类型 NioServerSocketServer 非阻塞
.option(ChannelOption.SO_BACKLOG, 1024) //针对服务端配置 设置tcp缓冲区
.childOption(ChannelOption.SO_SNDBUF, 32 * 1024) //针对客户端连接通道配置 设置发送数据的缓存大小
.childOption(ChannelOption.SO_RCVBUF, 32 * 1024) //设置读取数据的缓存大小
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持长连接
//初始化绑定服务通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
//对workerGroup的socketChannel设置处理器 数据传输过来的时候会进行拦截,可以设置多个handler
socketChannel.pipeline().addLast(new ServerHandler());
}
});
System.out.println("netty server is start ......");
//绑定一个端口并且同步, 生成一个channelFuture异步对象,通过isDone()等方法可以判断异步事件的执行情况
//启动服务器并且绑定端口,bind是异步操作,sync是等待异步操作执行完毕
ChannelFuture cf = b.bind(9000).sync();
// 等待服务端监听端口关闭,closeFuture是异步操作
// 通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭完成,内部调用的是Object的wait()方法
cf.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
//释放连接
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
3.编写处理器Handler,处理数据
/**
* 服务端监听器
*/
public class ServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道激活时触发此监听
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("====================服务端通道激活========================");
}
/**
* 当我们的通道里有数据进行读取的时候触发此监听
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
//NIO通信(传输的数据是什么?)
ByteBuf buf = (ByteBuf) msg;
//定义byte数组
byte[] req = new byte[buf.readableBytes()];
//从缓冲区取到数据 req
buf.readBytes(req);
//将读取到的数据转化成字符串
String body = new String(req, "utf-8");
System.out.println("服务端读取到数据:" + body);
//响应给客户端的数据
ctx.writeAndFlush(Unpooled.copiedBuffer("netty response data".getBytes()));
}catch (Exception e) {
e.printStackTrace();
}finally {
//释放数据
ReferenceCountUtil.release(msg);
}
}
/**
* 当服务端读取完数据后触发的监听
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("========================服务端读取数据完成========================");
}
/**
* 当读取数据出现异常时触发的监听
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("========================服务端读取数据出现异常=======================");
ctx.close();
}
}
4.编写客户端
public class Client {
public static void main(String[] args) throws InterruptedException {
//客户端需要一个事件循环组
EventLoopGroup workerGroup = new NioEventLoopGroup();
//辅助类,帮助创建Netty服务
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup) //绑定工作组
.channel(NioSocketChannel.class) //设置Nio模式
.handler(new ChannelInitializer<SocketChannel>() { //初始化绑定服务通道
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
socketChannel.pipeline().addLast(new ClientHandler());
}
});
System.out.println("netty client start......");
ChannelFuture cf = bootstrap.connect("127.0.0.1", 9000).syncUninterruptibly();
cf.channel().writeAndFlush(Unpooled.copiedBuffer("Netty client request client......".getBytes()));
//关闭连接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
}
}
5.编写客户端处理器
/**
* 客户端 监听器
*/
public class ClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道被激活时 触发此监听
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("====================客户端通道被激活========================");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
}
/**
* 客户端读取数据完成触发
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("====================客户端读取数据完成========================");
}
/**
* 读取数据异常时触发
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("====================数据读取异常===========================");
ctx.close();
}
}
6. 启动服务器客户端发送数据