什么是Netty?
在网络编程这个系列文章中,之前在讲解的东西仅仅只是一个模型,如果真在要在工作中去实际应用还要不断完善、扩展、优化。比如TCP拆包和粘包问题,或者是数据接收的大小等等问题都需要认证的去思考,而这些是需要大量是实际项目经历的。所以Socket网络通信不是一件简单的事情。
Netty是一个异步事件驱动的网络通信框架,用于简化网络编程的过程,不用去编写复杂的代码逻辑去实现通信,也不需要去考虑性能问题,不需要考虑编解码、半包读写问题,这些功能Netty都实现好了,只需要使用就可以了。这个从侧面也回答了为什么要使用Netty?
Java NIO存在的常见问题
NIO的类库和API繁杂,使用麻烦,需要熟练的掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
需要具备其他额外技术,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,必须对多线程和网络编程非常熟悉,才能编写出高质量的IO程序。
可靠性能力不齐全,开发工作量和难度都非常大,例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞、异常码流处理等。NIO编程的特点就是功能开发相对容易,但是可靠性齐全工作量和难度非常大。
JDK的BUG,比如Epoll BUG,会导致Selector空轮询,最终导致CPU 100%(已修复)。
Netty
什么是Netty?
Netty是由JBOSS提供的一个Java开源框架,Netty是一个高性能、异步事件驱动的NIO框架,提供了对TCP、UDP和文件传输的支持,可用于快速开发可维护的高性能协议服务器和客户端。
Netty是基于Java NIO的一个C/S网络应用框架,所有IO操作都是异步非阻塞的,通过Future-Lisener机制,用户可以很方便的主动获取或者通过通知机制获得IO操作结果。
Netty提供了一个新的方法来使得开发网络应用程序很容易和具有很强的扩展性。Netty内部是很复杂的,但是Netty提供了简单易用的API。
Netty也是最主流的NIO框架,其健壮性、可定制性、可扩展性在同类框架都是首屈一指的。
特性
设计:各种传输类型,阻塞和非阻塞套接字统一的API使用,灵活简单,功能强大的线程模型,无连接的DatagramSocket支持链逻辑,易于重用。
易用:大量的文档和例子,出了依赖JDK1.6,没有额外的依赖关系,某些功能依赖JDK1.7,其他特性可能有相关的依赖,但都是可选的。
性能:比Java API提供的NIO具有更好的吞吐量和更低的延迟,因为线程池和重用,所以消耗较少的资源,尽量减少不必要的内存拷贝。
健壮性:链接快或慢,不会导致更多的错误,在告诉网络的程序中不会出现不公平的读写。
安全性:完整的SSL/TLS和Start TLS支持可以在Applet或OSGI这些受限的环境中运行。
社区:活跃,版本迭代周期短,发现的BUG可以及时修复。
Netty优点
API简单,开发门槛低。
功能强大,预置了多种编解码功能,支持多种主流协议。
定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展。
性能高,通过与其他业界主流的NIO框架对比Netty的综合性能是最优之一。
传输快,正是利用了零拷贝的特点。
成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要在为NIO的BUG而烦恼。
Netty常见应用场景
互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RCP框架使用,典型的有Dubbo,默认就是使用Netty作为基础通信组件,用于实现各个进程节点之间的内部通信。
游戏行业:无论是手游服务端还是大型的网络游戏,Netty作为高性能的基础通信组件,本身就提供了TCP/URL和Http协议栈,非常方便的定制和开发私有协议栈。
大数据:经典Hadoop高性能通信和序列化组件Avro的RPC框架,默认就是采用Netty进行跨节点通信。Spark和Scala的通信模型也开始默认是Netty。
备注:Hadoop、Spark、Scala通信模型、RocketMQ、Dubbox等框架的底层通信都是用Netty。
总体架构
传输服务:支持BIO和NIO。
容器集成:支持OSGI、JBOSSMC、Spring、Guice容器。
协议支持:Http、Protobuf、二进制、文本、WbeSocket等常见协议,还支持实行编解码逻辑实现自定义协议。
核心:可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象。
Netty模块组件
ServerBootstrap、Bootrap
顾名思义引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件。
Netty中的Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
Future、ChannelFuture
在Netty中所有IO操作都是异步的,不能立刻得知消息是否被正确处理但是可以过一会等执行完毕或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,它们可以注册一个监听,当操作执行成功或者失败时,监听会自动触发注册的监听事件。
Channel
Netty网络通信的组件,能够用于执行网络I/O操作,Channel为用户提供了,1、当前网络链接的通道的状态、2、网络链接的配置参数、3、提供网络IO操作(如建立端口、读写、绑定端口),以及IO操作处理程序。
不同的协议,不同的阻塞类型的链接都有不同的Channel类型与之对应,常见的Channel类型有:
NioSockethannel:异步的客户端TCP Socket链接。
NioServerSocketChannel:异步的服务端TCP Socket链接。
NioDatagramChannel:异步的UDP链接。
NipSctpChannel:异步的客户端Stcp链接。
NioSctpServerChannel:异步的Sctp的服务端链接,这些通道涵盖了UDP和TCP网络IO以及文件IO。
Selector
Netty基于Selector对象实现了I/O多路复用,通过Selector,一个县城可以监听多个链接的Channel事件,当向一个Selector中注册Channel后,Selector内部的机制就可以自动不断的查询这些注册的Channel是否有已经就绪的IO事件(例如可读、可写、网络链接完成等),这样程序就可以很简单的使用一个线程高效的管理多个Channel。
NioEventLoop
NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时,会调用NioEventLoop的run方法,执行IO任务和非IO任务。
IO任务是指selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
非IO任务,中添加到taskQueue中的任务,如register0、bin0等任务,由runAllTasks方法触发。
这两种任务的执行时间由变量ioRatio控制,默认是50,则表示允许非IO任务执行时间与IO任务的执行时间相等。
NioEventLoopGroup
管理EventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应一个线程。
ChannelHandler
ChannelHandler仅仅作为一个接口,处理IO事件或拦截IO操作,并将其准发到ChannelPipeline(业务处理链)的下一个处理程序。
适配器子类:
ChannelInboundHandlerAdapter:处理入站IO事件。
ChannelOutboundHandlerAdapter:处理出站IO操作。
ChannelDuplexHandler:处理入栈和出站操作。
ChannelHandlerContext
保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象。
ChannelPipline
保存了ChannleHandler的List,用于处理或拦截Channel的入栈事件和出站操作,ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。
一个Channel包含了一个ChannelPipline,而ChannelPipeline中又维护了一个由ChannelHandlerContext组成的双向链表,并且每个ChannelHandlerContext又关联着一个ChannelHandler。入站事件和出站事件都在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的andler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。
Netty - HelloWolrd
import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.util.ReferenceCountUtil;
/** * 服务端 */ publicclass N01HelloServer { publicstaticvoid main(String[] args) throws InterruptedException {
//NioEventLoopGroup是用来处理IO操作的多线程事件循环器,Netty提供了许多不同的EventLoopGroup实现,用来处理不同的传输协议。
//这个是服务端用来接收客户端的链接的 EventLoopGroup connectEventLoopGroup = new NioEventLoopGroup();
//这个是用来与客户端进行网络通信的(网络读写) EventLoopGroup dataEventLoopGroup = new NioEventLoopGroup();
//启动服务辅助工具类,用于服务器的启动一系列性相关配置参数 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(connectEventLoopGroup, dataEventLoopGroup) //绑定两个线程组 .channel(NioServerSocketChannel.class) //指定NIO的模式为服务端 .option(ChannelOption.SO_BACKLOG, 1024) //设置TCP缓冲区大小 .option(ChannelOption.SO_SNDBUF, 32 * 1024) //设置发送缓冲区大小 .option(ChannelOption.SO_RCVBUF, 32 * 1024) //设置接收缓冲区大小 .option(ChannelOption.SO_KEEPALIVE, true) //保持连接 // .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) //维持链接的线程内存池配置 // .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) //维持读写的线程内存池配置 .childHandler(new ChannelInitializer<SocketChannel>() { //管道初始化时,配置处理管道的数据。 @Override protectedvoid initChannel(SocketChannel ch) throws Exception { //具体接收数据的处理类 ch.pipeline().addLast(new HelloServerHandler()); } });
//进行端口绑定 ChannelFuture channelFuture1 = serverBootstrap.bind(1111).sync();
/* * 挂起线程去接收数据 * main方法是主线程,如果没有把这个挂起的话,就随着主线程的结束而结束了 * 所以此处可以理解为阻塞 */ channelFuture1.channel().closeFuture().sync();
System.out.println("-----------");
//多端口绑定 // ChannelFuture channelFuture2 = serverBootstrap.bind(2222).sync(); // channelFuture2.channel().closeFuture().sync();
//关闭 connectEventLoopGroup.shutdownGracefully(); dataEventLoopGroup.shutdownGracefully(); } } /** * 数据处理类 * ChannelHandlerAdapter是实现了ChannelHandler,进而不用重写很多方法 */ class HelloServerHandler extends ChannelHandlerAdapter { //class HelloServerHandler implements ChannelHandler {
@Override publicvoid channelActive(ChannelHandlerContext ctx) throws Exception { //通道活跃 System.out.println("服务端通道处于活跃状态"); }
/* * 通道数据处理 */ @Override publicvoid channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { //转换为Netty-Buffer类型的数据,传输就是Netty-Buffer类型 ByteBuf buf = (ByteBuf) msg; //和Java的NIO做对比会发现,Netty提供的这个Buffer不需要flip,内部已经自动实现了。 byte[] data = newbyte[buf.readableBytes()]; buf.readBytes(data); System.out.println("服务端响应数据:" + new String(data)); //返回给客户端响应 ctx.write(Unpooled.copiedBuffer("你好,客户端".getBytes())); // .addListener(ChannelFutureListener.CLOSE); //写完的策略(关闭链接) ctx.flush(); } finally { //释放 ReferenceCountUtil.release(msg); } }
/* * 读取完毕的处理 */ @Override publicvoid channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("读完了......"); ctx.flush(); }
@Override publicvoid exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { //出现异常 cause.printStackTrace(); ctx.close(); } } |
import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.ReferenceCountUtil;
/** * 客户端 */ publicclass N01HelloClient { publicstaticvoid main(String[] args) throws InterruptedException { //创建链接服务端 NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
//创建启动类 Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup) //绑定链接的线程组 .channel(NioSocketChannel.class) //指定链接模式 .handler(new ChannelInitializer<SocketChannel>() { //数据处理 @Override protectedvoid initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new HChannelHandler()); //绑定这个管道的处理类 } });
//绑定端口 ChannelFuture channelFuture1 = bootstrap.connect("127.0.0.1", 1111).sync();
//写数据给服务端 channelFuture1.channel().write(Unpooled.copiedBuffer("你好,服务端1。".getBytes())); channelFuture1.channel().write(Unpooled.copiedBuffer("你好,服务端2。".getBytes())); channelFuture1.channel().write(Unpooled.copiedBuffer("你好,服务端3。".getBytes())); //如果不刷新到缓冲区,会发现数据一直没发出去的 channelFuture1.channel().flush();
//另外的一个方法 channelFuture1.channel().writeAndFlush(Unpooled.copiedBuffer("阿罗哈".getBytes()));
//挂起线程接收数据 channelFuture1.channel().closeFuture().sync();
//多端口绑定 // ChannelFuture channelFuture2 = bootstrap.connect("127.0.0.1", 2222); // channelFuture2.channel().closeFuture().sync();
//优雅的关闭 eventLoopGroup.shutdownGracefully(); } } /** * 客户端处理类 */ class HChannelHandler extends ChannelHandlerAdapter {
@Override publicvoid channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("客户端处于活跃状态"); }
/* * 读写数据 */ @Override publicvoid channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { ByteBuf byteBuf = (ByteBuf) msg; byte [] data = newbyte[byteBuf.readableBytes()]; byteBuf.readBytes(data); System.out.println("客户端读取的数据:" + new String(data));
} finally { /* * 读取和自己创建的的ByteBuf要手动释放。 * 写出的ByteBuf时,由Netty负责释放,不需要手动的调用release()方法。 * 源码: * 可以看到ReferenceCountUtil.release(msg);其实是ByteBuf.release()方法, * 这个方法是从ReferenceCounted接口继承俄日来的包装。 * Netty4中的ByteBuf使用了引用计数,并且实现了一个可选的ByteBuf池,每一个新分配的ByteBuf的引用计数值为1, * 每对这个ByteBuf对象增加一个引用,需要调用ByteBuf.retain()方法,而每减少一个引用,需要调用ByteBuf.release()方法。 * 当这个ByteBuf对象的引用计数为0时,表示对象可回收,其他实现了ReferenceCounted接口的一样是同理。 */ ReferenceCountUtil.release(msg); } }
@Override publicvoid channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("读取结束,刷新到缓冲区......"); ctx.flush(); }
@Override publicvoid exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { //出现异常 cause.printStackTrace(); ctx.close(); } } |
编码过程小结
1、初始化2个NioEventLoopGroup,其中connectEventLoopGroup用于Accetpt链接,建立事件并分发请求,dataEventLoopGroup用于处理IO读写事件和业务逻辑。
2、基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型、链接参数、配置出入站事件的handler。
3、绑定端口,开始干活。
Netty服务端工作过程
Connect Group(Master)的NioEventLoop循环执行任务步骤包含:
1、轮询accept事件。
2、处理accept IO事件,与Client建立连接,生成NioSocketChannel,并将NioScoketChannel注册到某个Data Group(Worker)的NioEventLoop的Selector上的处理任务队列中的任务runAllTasks。任务队列中的任务包含用户调用eventLoop.execute或schedule执行的任务,或者其他线程提交的到该eventLoop的任务。
Data Group(Worker)的NioEventLoop循环执行任务步骤包含:
1、轮询read、write事件。
2、处理IO事件,即read、write事件,在NioScoketChannel可读、可写事件发生时,进行处理。
3、处理任务队列中的任务,runAllTasks。
任务队列中的task的常见应用场景:
1、用户程序自定义的普通任务。
2、非当前Reactor线程调用Channel的各种方法,例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write类方法向该用户推送消息,就会进入到这种场景。最终的write会提交到任务队列中后被异步消费。
3、用户自定义定时任务。