1、netty是什么?
在使用netty之前,我们首先要知道netty的优势在哪,它能解决什么问题。
我们不妨直接引用netty官网上最顶部的内容:
翻译一下就是:netty是一款异步的事件驱动的网络程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。
注意上方加粗的关键词:
- 支持异步是netty高效的重要支撑,在传统java API中,我们使用NIO需要较为繁琐的编码,而netty为我们封装了易于使用的API,我们甚至能够通过一行代码将整个服务由OIO转为NIO!
- 事件驱动指的是netty处理网络消息的方式,框架内部为我们提供了完善的接口/抽象类,其中的各个方法代表了在网络传输过程中发生的各种事件,在我们编码过程中只需要完善各个方法,就可以处理各种网络通信!
- 快速开发的快速不仅仅在于netty提供了恰当的处理网络通信的框架逻辑,更在于其内部提供了丰富的成熟的编解码器,我们只需要将它们加入我们的pipline,便可以轻易的处理如HTTP/HTTPS/Websocket/IMAP/ProtoBuf等等协议
- 高性能于netty有着许多原因,包括但不限于支持NIO、零拷贝、多线程优化等。而我们只需要使用netty框架,便能轻易的享受前人给予我们的强大性能。
我们将从以下几方面来介绍netty:
- netty的工作流程:大致了解netty是如何处理网络信息的
- ChannelHandler:netty中重要的网络信息处理组件
- 编解码器:作为特殊的ChannelHandler为使用者提供了方便的协议解析
- EventLoop:netty内部基于Reactor模型对事件进行分发
- ByteBuf:netty提供的高效且易用的字节容器
- ChannelFuture:netty自身定义的future模型,在执行异步操作时使用
- 引导客户端与服务器启动:最后一步,如何启动我们的网络服务
2、netty的工作流程
对于入门使用netty而言,一图便可以了解netty的工作流程:
接下来我们对这张图进行解释:
- channel:就如同JavaNIO中所做的那样,netty将对网络IO的操作抽象为channel,因此我们不再操作基础的socket,而是封装过后的channel。
- pipline:由管道流向netty的事件,需要经过重重的ChannelHandler加以处理,ChannelHandler便是netty提供给我们的、编码者大部分精力需要花费的框架结构。pipline则是将一个个ChannelHandler顺序连接起来的抽象。
- ChannelHandler:如上文所说,ChannelHandler是我们处理网络信息的主体,我们编码的ChannelHandler其本身继承于netty所定义ChannelHandler,而其各个方法本身就是为了响应一个个不同的事件,这也是netty事件驱动的主要体现。
- 需要指出的是,这里的进站事件可以理解为netty读取外界(如客户端)传入信息的行为;出站事件是指向客户端发送报文的行为,往往是作为入站事件的响应而出现的(当然也有主动发送如广播、定时消息等)。
3、ChannelHandler
概念
ChannelHandler从大类上分为两类:ChannelInboundHandler与ChannelOutboundHandler。分别对应处理了上图中的入站事件和出站事件,这也是很多作者会像下方这样描述pipline与ChannelHandler的关系的原因(图源网络):
而事实上这样的图很容易给读者造成误导,认为ChannelInboundHandler与ChannelOutboundHandler是在两条不同的线上。
而实际它们都串联在pipline上,只是在流经时会对是处理入站事件的handler还是出站事件的handler加以判别。例如入站事件遇到了出站事件的ChannelOutboundHandler会直接跳过。
实践
通常我们继承的ChannelHandler来自下图:
- ChannelHandler是公共的抽象父类,我们通常不会使用。
- ChannelInboundHandler与ChannelOutboundHandler接口中定义了netty中各种事件,是事件驱动的规范,通过继承它们我们可以实现自己的ChannelHandler。
- ChannelHandlerAdapter为我们封装了接口的简单实现,因此继承它们我们不用为每个方法编写自己的实现,只需要将注意力集中在我们所需要的事件驱动方法上。
一个简单的继承于SimpleChannelInboundHandler(其是ChannelInboundHandlerAdapter的子类,帮助我们在处理完消息后释放消息的内存引用,这是ChannelInboundHandlerAdapter不会做到的,因此我们也通常继承它)的Handler如下所示:
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf>{
//当channel连接事件发生时的响应
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rock!",CharsetUtil.UTF_8));
}
//当channel读入数据事件发生时的响应
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf in) throws Exception {
System.out.println("Client received:"+in.toString(CharsetUtil.UTF_8));
}
}
复制代码
4、编解码器
概念
编解码器其实就是一类特殊的ChannelHandler,以MessageToMessageDecoder为例,其继承关系如下:
而往往为一类事物中的某种专门抽象出子类,代表着其必然有着特殊的意义。
在网络通信中,我们要传递信息往往需要协议的封装,而协议通常是规范的、不变的,如果每次都要为了处理协议而编写大量代码无疑是冗余的。而在其他如文件IO方面,也需要特定的编码器支持。
因此编解码器出现了,netty提供了可拔插的编解码器构件,能够方便地帮助我们处理各个协议。我们可以这样区分各种编码器:
-
Encoder
- MessageToByteEncoder //将消息编码为字节
- MessageToMessageEncoder //将消息编码为消息
-
Decoder
- ByteToMessageDecoder //将字节解码为消息
- MessageToMessageDecoder //将消息解码为消息
-
Codec //相当于Encoder+Decoder
加入编解码器后的pipline如下所示
实践
一个简单的Decoder如下所示(DatagramPacket是netty定义的一种消息容器,作为泛型成为传入消息的载体):
public class LogEventDecoder extends MessageToMessageDecoder<DatagramPacket> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket, List<Object> list) throws Exception {
ByteBuf buf=datagramPacket.content();//获取数据,ByteBuf见第六小节
//处理数据
int index=buf.indexOf(0, buf.readableBytes(), LogEvent.SEPARATOR);
index=buf.indexOf(index+1, buf.readableBytes(), LogEvent.SEPARATOR);
String filename=buf.slice(0, index).toString(CharsetUtil.UTF_8);
String logMsg=buf.slice(index+1, buf.readableBytes()-index-1).toString(CharsetUtil.UTF_8);
LogEvent logEvent=new LogEvent(datagramPacket.sender(),filename, logMsg,System.currentTimeMillis());
//将解码后消息传递给下一个handler
list.add(logEvent);
}
}
复制代码
而在netty的官方文档中,我们可以看到大量的支持各种协议的编解码器具体实现,这为我们高效开发提供了有力支持。下面仅展示部分:
当然,你也可以编写自己的编解码器来解析自定义协议。
5、EventLoop
EventLoop 是 Netty Reactor 线程模型的核心处理引擎,其工作流程可以如下所示:
- 创建EventLoop,其可以归属于一个Event Loop组(可以理解为池化)
- channel在创建时将其绑定在一个EventLoop上
- 当有事件发生时,会将其放入Event Queue中
- EventLoop通过轮询从Event Queue中取出事件并且方法到对应的,并且将其分发给对应的pipline/future回调函数(执行异步IP时会返回future,可以通过给其添加Listner的方式来进行回调,详见第七节)
对于客户端-服务器模型,EventLoop如下工作
- 当事件出发时,会先传入第一个EventLoopGroup,其之上绑定了一个或多个channel,这个EventLoopGroup会将事件分发给对应的channel的EventLoop(右边的WorkerEventLoop内)
- 右边的EventLoop完成IO后,会将事件沿着pipline进行传播
- 当future执行完成时,右边的EventLoop同样会为其执行它的回调函数
6、ByteBuf
ByteBuf是netty内置的帮助开发者处理字节的API,与琐碎的Java NIO提供的ByteBuffer相比,拥有更方便的使用体验以及更好的性能,其优点包括:
- 易于为用户拓展
- 通过内置的复合缓冲区类型实现零拷贝
- 类似StringBuffer可以按需增长
- 读写模式间不需要切换,读写使用不同索引
- 支持链式调用
- 支持引用计数
- 支持池化
在这里我并不想对各个API进行解释,这也是没有必要的,详情可以参考官方文档
在这里更希望对ByteBuf的数据结构和使用模式进行展开
数据结构
ByteBuf在数组间维护了两个指针 readerIndex与writerIndex,分别指向了读取和写入的下一个字节的位置,如图:
当然地,在读取内容时需要保障writerIndex大于等于readerIndex,试图越界读取会报出异常。
使用模式
ByteBuffer可以主要分为以下三种使用模式
-
Heap Buffer :将数据存储在JVM的堆空间中
-
Direct Buffer:将数据存储在本地内存中,这样做的好处有
- 通过本地方法调用分配,除去了中间内存的拷贝,提高IO速度
- 本地内存不在JVM堆空间中,因此可以降低堆溢出的可能性,并且不需要GC
-
Composite Buffer:将上述二者结合起来,即有存储在堆空间的部分,也有存储在本地内存的部分,使得ByteBuf的使用更加灵活
7、ChannelFuture
Future提供了任务完成时通知引用程序的方式,就如JUC下所定义的一样。
而在netty中,如同ByteBuf之于ByteBuffer,提供了ChannelFuture进行优化。可以通过添加Listener的方式来为future提供回调函数。直接看代码或许能够更好地帮助我们理解,请注意代码中的注释。
//channel进行远程连接,在netty中这是异步的,会立即返回
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1",8888));
//为future注册一个Listener,它重写的方法将会在future执行完毕后被调用
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
System.out.println("连接成功!");
}
});
复制代码
8、引导客户端与服务器启动
在netty中,我们通过BootStrap类和ServerBootstrap引导启动。
引导启动事实上是一件相对固定的流程,同样的,使用代码加注释的方式在下方给出。
启动服务器
private void start() throws Exception{
//创建自定义的channelHandler
final EchoServerHandler serverHandler=new EchoServerHandler();
//创建EventLoop组,如果这里指定为OIO便是阻塞式,
//还可以指定如Epoll,Local,Embeded等方式
EventLoopGroup group=new NioEventLoopGroup();
try {
//创建服务器引导
ServerBootstrap b=new ServerBootstrap();
//为ServerBootstrap绑定EventLoop组来处理事件
b.group(group)
//指定是channel的实现类,如果这里指定为OIO便是阻塞式,
//还可以指定如Epoll,Local,Embeded等方式
//注意这里的传输方式需要和EventLoopGroup一致
.channel(NioServerSocketChannel.class)
//服务器需要绑定端口
.localAddress(new InetSocketAddress(port))
//添加ChannelHandler来处理网络信息
//可以直接在里面填入一个ChannelHandler
//当需要多个ChannelHandler时,需要像下方这样在initChannel方法中添加
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(serverHandler);
}
});
//绑定到端口上
ChannelFuture f=b.bind();
//添加回调函数
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
System.out.println("绑定成功");
}
});
}finally {
//在退出前要关闭group,通过sync()阻塞直到关闭成功
group.shutdownGracefully().sync();
}
}
复制代码
启动客户端
public void start() throws Exception {
//创建EventLoop组,如果这里指定为OIO便是阻塞式,
//还可以指定如Epoll,Local,Embeded等方式
EventLoopGroup group=new NioEventLoopGroup();
try {
//创建客户端引导
Bootstrap b = new Bootstrap();
//为Bootstrap绑定EventLoop组来处理事件
b.group(group)
//指定是channel的实现类,如果这里指定为OIO便是阻塞式,
//还可以指定如Epoll,Local,Embeded等方式
//注意这里的传输方式需要和EventLoopGroup一致
.channel(NioSocketChannel.class)
//这里填写服务器的socket地址
.remoteAddress(new InetSocketAddress(host, port))
//添加ChannelHandler来处理网络信息
//可以直接在里面填入一个ChannelHandler
//当需要多个ChannelHandler时,需要像下方这样在initChannel方法中添加
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
//这里当然也可以为future添加listener,只是为了演示如何主动关闭使用了下面的方式
//通过sync()方法阻塞直到连接成功
ChannelFuture f = b.connect().sync();
//通过sync()方法阻塞直到channel关闭成功
f.channel().closeFuture().sync();
}finally {
//在退出前要关闭group,通过sync()阻塞直到关闭成功
group.shutdownGracefully().sync();
}
}
复制代码
9、小结
至此,如何使用netty以及大致介绍完毕了,我们不妨再回看文章开头提到的netty优良特性,或许会有新的收获:
- 支持异步:只需要在启动时指定EventLoopGroup和channel为异步的即可,netty进行封装
- 事件驱动:netty的处理网络信息的过程本质上就是网络信息传入传出时在ChannelHandler中一步步的加工
- 快速开发:netty本身作为框架就有规范代码便于开发的功能,而其中又提供了规范的ChannelHandler,我们只需要重写其中方法即可;又提供了大量的编解码器,使得我们可以拔插式的添加协议解析
- 高性能:netty的异步通信、ByteBuf零拷贝与池化技术、Reactor事件驱动模型、异步future与回调、支持自定义协议解析、pipline中无锁的串行化设计、内部大量的并发优化如CAS等为netty提供了强大的性能。
10、其他
- 本文只是简单地介绍了netty的使用与设计,详细的解析可以参阅Norman Maurer著的【Netty实战】,同时亦附上书中提供的代码,主要参阅第2章、第12章、第13章提供的实践案例
- 本文中使用的netty版本为4.1.12.Final,maven依赖如下:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.12.Final</version>
</dependency>
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/94646.html