一起来读Netty In Action之传输(三)
当我们的应用程序需要接受比预期多很多的并发连接的时候,我们需要从阻塞传输切换到非阻塞传输上去,如果是我们的网络编程是基于jdk提供的API进行开发地的话,这种传输模式的切换几乎要我们重构整个网络传输相关的代码,然而,Netty为它所有的传输实现了一个通用的API,这使得我们能更加简单的从阻塞传输切换到非阻塞传输的编程方式上去。
1.传输迁移
首先,我们来看一下一个使用netty去实现阻塞传输的例子,这个应用程序简单的接受连接,向客户端写hi,然后再关闭连接。代码如下:
1 public class NettyOioServer { 2 public void server(int port) throws Exception{ 3 final ByteBuf buf = Unpooled.unreleasableBuffer( 4 Unpooled.copiedBuffer("hi\r\n", Charset.forName("UTF-8")) 5 ); 6 //创建EventLoopGroup 7 EventLoopGroup eventLoopGroup = new OioEventLoopGroup(); 8 try { 9 //创建ServerBootstrap 10 ServerBootstrap serverBootstrap = new ServerBootstrap(); 11 serverBootstrap.group(eventLoopGroup) 12 //指定nio传输channel 13 .channel(OioServerSocketChannel.class) 14 //指定端口 15 .localAddress(new InetSocketAddress(port)) 16 //添加一个echoServerHandler到子channel的channelPipeLine 17 .childHandler(new ChannelInitializer<SocketChannel>() { 18 protected void initChannel(SocketChannel socketChannel) throws Exception { 19 socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){ 20 @Override 21 public void channelActive(ChannelHandlerContext ctx) throws Exception { 22 ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE); 23 } 24 }); 25 } 26 }); 27 //异步的绑定服务器,调用sync阻塞到绑定完成 28 ChannelFuture channelFuture = serverBootstrap.bind().sync(); 29 //获取channel的closeFuture,并阻塞到其完成 30 channelFuture.channel().closeFuture().sync(); 31 }finally { 32 //释放资源 33 eventLoopGroup.shutdownGracefully().sync(); 34 } 35 } 36 }
当我们需要改为飞阻塞版本的代码的时候,就只需要将上面代码中高亮的两行分别改为如下:
1 EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); 2 serverBootstrap.group(eventLoopGroup) 3 //指定nio传输channel 4 .channel(NioServerSocketChannel.class).。。。。
因为netty对每种传输都暴露了相同的接口,所以我们的代码几乎不用修改。在所有的情况下,传输的实现都依赖于interface,channel,channelPipeline和channelHandler。
2.传输API
传输的API核心是channel,它被用于所有的I/O操作,channel类的层次结构如下图:
如上图所示,每一个channel都会被分配一个channelPipeLine和channelConfig,channelConfig包含了该channel的所有的配置,并且支持热更新。
channelPipeLine持有所有应用于入站和出站数据以及事件的channelHandler实例(channelHandler的用途在这里就不说了)。
我们也需要了解一下channel的其他方法:
1 public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable<Channel> { 2 3 EventLoop eventLoop(); //返回分配给channel的eventloop 4 5 void write(); //将数据写到远程节点,这个数据将被传递给channelpipeline,并排队直到它被冲刷 6 7 boolean isActive(); 8 9 SocketAddress localAddress(); 10 11 SocketAddress remoteAddress(); 12 13 ChannelPipeline pipeline(); //返回分配给channel的pipeline 14 15 Channel flush(); //将之前写的数据冲刷到底层进行传输,如一个socket 16 17 ... 18 }
Netty的channel都是线程安全的,因此我们可以保有一个channel的引用,当需要向远程冲刷数据的时候,即使有许多的线程都在使用它,我们也可以冲刷 本次数据,并且,消息会被保证按顺序发送。
3.内置的传输
Netty内置了一些开箱即用的传输,因为并不是所有的传输都支持每一种传输协议,所以我们必须选择一个和我们的应用程序使用的协议相容的传输。我们来看一下netty提供了哪些传输
名称 | 包 | 描述 |
---|---|---|
NIO | io.netty.channel.socket.nio | 基于java.nio.channels(基于selector的途径)包 |
Epoll | io.netty.channel.epoll | 使用JNI的epoll()和非阻塞IO,这个传输服务支持的一些特性在Linux上才有效,如SO_REUSEPORT。且比NIO传输服务和完全非阻塞都要快 |
OIO | io.netty.channel.socket.oio | 基于java.net包,使用阻塞流 |
Local | io.netty.channel.local | 一本地传输服务能用来通过pipe在VM中通信 |
Embedded | io.netty.channel.embedded | 一个嵌入式(embedded)传输服务,允许在没有真正基于网络传输服务的情况下使用ChannelHandler,这对于你测试ChannelHandler的实现很有用 |
3.1.NIO-非阻塞I/O
nio提供了一个所有的I/O操作的全异步的实现,它利用了基于选择器的API。选择器的基本概念是充当一个注册表,在那里你将可以在channel的状态发生改变时得到通知,可能的状态变化有:
(1)新的channel已被接受并且就绪。
(2)channel连接已经完成。
(3)channel有已经可以读取的数据。
(4)channel可用于写数据。
选择器运行在一个检查状态变化并对其做出响应的线程上,在应用程序对状态的改变做出响应之后,选择器将被重置,并重复这个过程。选择操作有如下四种类型
(1)OP_ACCEPT 请求在接受新连接并且创建channel时获得通知
(2)OP_CONNECT请求在建立一个连接时获得通知
(3)OP_READ 数据就绪可以从channel中读取时获得通知
(4)OP_WRITE请求可以向channel写入数据时获得通知
Netty对所有的传输都提供了共有的用户级别的API,并完全隐藏了NIO的内部实现细节,下图展示了该流程:
根据上面的流程,我们不难猜想基于NIO的程序的大致结构:
1 interface ChannelHandler{ 2 void channelReadable(Channel channel); 3 void channelWritable(Channel channel); 4 } 5 class Channel{ 6 Socket socket; 7 Event event;//读,写或者连接 8 } 9 10 //IO线程主循环: 11 class IoThread extends Thread{ 12 public void run(){ 13 Channel channel; 14 while(channel=Selector.select()){//选择就绪的事件和对应的连接,如果没有I/O事件产生,我们的程序就会阻塞在select处,而不是空转!!!! 15 if(channel.event==OP_ACCEPT){ 16 registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器 17 } 18 if(channel.event==OP_WRITE){ 19 getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件 20 } 21 if(channel.event==OP_READ){ 22 getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件 23 } 24 } 25 } 26 Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器 27 }
在这里,我们介绍一下零拷贝的概念,我们知道“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。
在非零拷贝方式中,我们要将磁盘上的文件通过网络发送至网卡,需要经历以下几个过程:
可以看到非零拷贝的过程如下:
- read() 调用导致一次从user mode到kernel mode的上下文切换。在内部调用了sys_read() 来从文件中读取data。第一次copy由DMA (direct memory access)完成,将文件内容从disk读出,存储在kernel的buffer中。
- 然后请求的数据被copy到user buffer中,此时read()成功返回。调用的返回触发了第二次context switch: 从kernel到user。至此,数据存储在user的buffer中。
- send() Socket call 带来了第三次context switch,这次是从user mode到kernel mode。同时,也发生了第三次copy:把data放到了kernel adress space中。当然,这次的kernel buffer和第一步的buffer是不同的buffer。
- 最终 send() system call 返回了,同时也造成了第四次context switch。同时第四次copy发生,DMA egine将data从kernel buffer拷贝到protocol engine中。第四次copy是独立而且异步的。
明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)
1. 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
2. 接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上面的两次操作都不需要CPU参与,所以就达到了零拷贝,过程如下图:
3.2.Epoll-用于linux的本地非阻塞传输
Netty的Nio基于java提供的异步/非阻塞网络编程的通用抽象,这虽然保证了netty的非阻塞API在任何平台上的使用,但是也有相应的限制,因为JDK为了在所有的系统上提供相同的功能,做出了妥协。Linux作为高性能的网络编程平台,有着大量的先进特性的开发,其中包括epoll-一个高度可扩展的I/O事件通知特性。
在上面的selector模型中,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。我们有O(n)的无差别轮询复杂度,同时处理的流越多,没一次无差别轮询时间就越长。 而epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数)。限于篇幅,关于epoll这里只讲解一下原理性的东西。
Netty为linux提供了一组NIO的API,其以一种和它本身的设计更加一致的方式去使用epoll,如果我们需要在代码中使用epoll,则需要在代码中这样写:
1 ........ 2 //创建EventLoopGroup 3 EventLoopGroup eventLoopGroup = new EpollEventLoopGroup(); 4 try { 5 //创建ServerBootstrap 6 ServerBootstrap serverBootstrap = new ServerBootstrap(); 7 serverBootstrap.group(eventLoopGroup) 8 //指定nio传输channel 9 .channel(EpollServerSocketChannel.class) 10 ......
3.3. OIO-阻塞型I/O
Netty的OIO是一种妥协方案,其使用JAVA中原生态的旧的API,其是同步阻塞的。在java.net的API中,通常使用一个线程接受来自指定端口的请求,当创建一个套接字时,就会创建一个新的线程来进行处理。
3.4. 在JVM内进行通信的本地传输
Netty提供了在同一个JVM中的客户端与服务端之间的异步通信,在此传输中,与服务器通道相关联的SocketAddress不绑定到物理网络地址,相反,当服务器运行时,其存储在注册表中,关闭时取消注册。因为传输不能接受真正的网络流量,所以它不能与其他传输实现互操作。
3.5.嵌入式传输
Netty还提供了一个额外的传输,其可以将ChannelHandler作为帮助类嵌入其他的ChannelHandler中,以这种方式,你无需修改其内部代码便能扩展ChannelHandler的功能。
3.6.传输的用例
以下表格展示了应用场景下的最佳传输:
应用程序的需求 | 推荐的传输方式 |
---|---|
非阻塞代码库或者一个常规的起点 | NIO(或者在linux上使用epoll) |
阻塞代码库 | OIO |
在同一个jvm内部的通信 | Local |
测试channelHandler的实现 | 嵌入式传输 |
3.7.小结
本节我们讨论了Netty预置的传输。展示了使用netty进行传输相比较jdk进行网络传输的好处,netty为不同传输都暴露了近乎相同的API,我们还介绍了一些重要的I/O模型,并概要的解释了模型的工作原理,最后总结了这些传输的使用场景。
参考资料:
http://www.cnblogs.com/leesf456/p/6895145.html
https://www.zhihu.com/question/20122137
http://blog.onlycatch.com/post/Netty%E4%B8%AD%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D
https://tech.meituan.com/nio.html?utm_source=tool.lu