当我们的应用程序需要接受比预期多很多的并发连接的时候,我们需要从阻塞传输切换到非阻塞传输上去,如果是我们的网络编程是基于jdk提供的API进行开发地的话,这种传输模式的切换几乎要我们重构整个网络传输相关的代码,然而,Netty为它所有的传输实现了一个通用的API,这使得我们能更加简单的从阻塞传输切换到非阻塞传输的编程方式上去。

1.传输迁移

首先,我们来看一下一个使用netty去实现阻塞传输的例子,这个应用程序简单的接受连接,向客户端写hi,然后再关闭连接。代码如下:

 public class NettyOioServer {
public void server(int port) throws Exception{
final ByteBuf buf = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("hi\r\n", Charset.forName("UTF-8"))
);
//创建EventLoopGroup
EventLoopGroup eventLoopGroup = new OioEventLoopGroup();
try {
//创建ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventLoopGroup)
//指定nio传输channel
.channel(OioServerSocketChannel.class)
//指定端口
.localAddress(new InetSocketAddress(port))
//添加一个echoServerHandler到子channel的channelPipeLine
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
//异步的绑定服务器,调用sync阻塞到绑定完成
ChannelFuture channelFuture = serverBootstrap.bind().sync();
//获取channel的closeFuture,并阻塞到其完成
channelFuture.channel().closeFuture().sync();
}finally {
//释放资源
eventLoopGroup.shutdownGracefully().sync();
}
}
}

当我们需要改为飞阻塞版本的代码的时候,就只需要将上面代码中高亮的两行分别改为如下:

 EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
serverBootstrap.group(eventLoopGroup)
//指定nio传输channel
.channel(NioServerSocketChannel.class).。。。。

因为netty对每种传输都暴露了相同的接口,所以我们的代码几乎不用修改。在所有的情况下,传输的实现都依赖于interface,channel,channelPipeline和channelHandler。

2.传输API

传输的API核心是channel,它被用于所有的I/O操作,channel类的层次结构如下图:

如上图所示,每一个channel都会被分配一个channelPipeLine和channelConfig,channelConfig包含了该channel的所有的配置,并且支持热更新。

channelPipeLine持有所有应用于入站和出站数据以及事件的channelHandler实例(channelHandler的用途在这里就不说了)。

我们也需要了解一下channel的其他方法:

 public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable<Channel> {

     EventLoop eventLoop();              //返回分配给channel的eventloop

     void write();                       //将数据写到远程节点,这个数据将被传递给channelpipeline,并排队直到它被冲刷

     boolean isActive();

     SocketAddress localAddress();

     SocketAddress remoteAddress();

     ChannelPipeline pipeline();         //返回分配给channel的pipeline

     Channel flush();                    //将之前写的数据冲刷到底层进行传输,如一个socket

     ...
}

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的程序的大致结构:

 interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//读,写或者连接
} //IO线程主循环:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//选择就绪的事件和对应的连接,如果没有I/O事件产生,我们的程序就会阻塞在select处,而不是空转!!!!
if(channel.event==OP_ACCEPT){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
}
if(channel.event==OP_WRITE){
getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
}
if(channel.event==OP_READ){
getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
}

在这里,我们介绍一下零拷贝的概念,我们知道“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。

在非零拷贝方式中,我们要将磁盘上的文件通过网络发送至网卡,需要经历以下几个过程:

可以看到非零拷贝的过程如下:

  1. read() 调用导致一次从user mode到kernel mode的上下文切换。在内部调用了sys_read() 来从文件中读取data。第一次copy由DMA (direct memory access)完成,将文件内容从disk读出,存储在kernel的buffer中。
  2. 然后请求的数据被copy到user buffer中,此时read()成功返回。调用的返回触发了第二次context switch: 从kernel到user。至此,数据存储在user的buffer中。
  3. send() Socket call 带来了第三次context switch,这次是从user mode到kernel mode。同时,也发生了第三次copy:把data放到了kernel adress space中。当然,这次的kernel buffer和第一步的buffer是不同的buffer。
  4. 最终 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,则需要在代码中这样写:

 ........
//创建EventLoopGroup
EventLoopGroup eventLoopGroup = new EpollEventLoopGroup();
try {
//创建ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventLoopGroup)
//指定nio传输channel
.channel(EpollServerSocketChannel.class)
......

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

一起来读Netty In Action之传输(三)的更多相关文章

  1. 一起来读Netty In Action(一)

    Netty是一款异步事件驱动的网络应用程序框架,支持快速的开发可维护的高性能的面向协议的服务器和客户端.在网络编程中,阻塞.非阻塞.同步.异步经常被提到.同步(synchronous) IO和异步(a ...

  2. 一起来读Netty In Action之netty的组件和设计(二)

    在上一篇博客中,我们给出了java高性能网络编程的技术基础,也简单的介绍了netty的核心构件,在这一篇博客中,我们将更加详细的研究netty的各个组件,并且密切关注它们是如何通过协作来支撑这些体系结 ...

  3. Netty In Action中文版 - 第三章:Netty核心概念

            在这一章我们将讨论Netty的10个核心类.清楚了解他们的结构对使用Netty非常实用.可能有一些不会再工作中用到.可是也有一些非经常常使用也非常核心,你会遇到. Bootstrap ...

  4. Netty In Action中文版 - 第四章:Transports(传输)

    本章内容 Transports(传输) NIO(non-blocking IO,New IO), OIO(Old IO,blocking IO), Local(本地), Embedded(嵌入式) U ...

  5. Netty 系列二(传输).

    一.前言 上一篇文章我们提到 Netty 的核心组件是 Channel.回调.Future.ChannelHandler.EventLoop,这篇文章主要是对 Channel (Netty传入和传出数 ...

  6. 《Netty in action》 读书笔记

    声明:这篇文章是记录读书过程中的知识点,并加以归纳总结,成文.文中图片.代码出自<Netty in action>. 1. 为什么用Netty? 每个框架的流行,都一定有它出众的地方.Ne ...

  7. Netty In Action中文版 - 第七章:编解码器Codec

    http://blog.csdn.net/abc_key/article/details/38041143 本章介绍 Codec,编解码器 Decoder,解码器 Encoder,编码器 Netty提 ...

  8. Netty In Action中文版 - 第五章:Buffers(缓冲)

    本章介绍 ByteBuf ByteBufHolder ByteBufAllocator 使用这些接口分配缓冲和运行操作 每当你须要数据传输时,它必须包括一个缓冲区.Java NIO API自带的缓冲区 ...

  9. Netty In Action中国版 - 第二章:第一Netty程序

    本章介绍 获得Netty4最新的版本号 设置执行环境,以构建和执行netty程序 创建一个基于Netty的server和client 拦截和处理异常 编制和执行Nettyserver和client 本 ...

随机推荐

  1. Unity场景和代码合并以及UnityYAMLMerge的使用

    1.首先是.gitignore的配置. # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ ...

  2. Flink 源码解析 —— 如何获取 ExecutionGraph ?

    https://t.zsxq.com/UnA2jIi 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1学习 -- Mac 上搭建 Flink 1.6. ...

  3. 【KakaJSON手册】04_JSON转Model_04_值过滤

    在KakaJSON手册的第2篇文章中提过:由于JSON格式能表达的数据类型是比较有限的,所以服务器返回的JSON数据有时无法自动转换成客户端想要的数据类型 比如客户端想要的是Date类型,服务器返回的 ...

  4. 读取某个目录下的所有图片并显示到pictureBox

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  5. 【Node/JavaScript】论一个低配版Web实时通信库是如何实现的( WebSocket篇)

    引论 simple-socket是我写的一个"低配版"的Web实时通信工具(相对于Socket.io),在参考了相关源码和资料的基础上,实现了前后端实时互通的基本功能 选用了Web ...

  6. Entity Framework 6.0 入门系列 第一篇

    Entity Framework 6.0 入门系列 第一篇 好几年前接触过一些ef感觉不是很好用,废弃.但是 Entity Framework 6.0是经过几个版本优化过的产物,性能和功能不断完善,开 ...

  7. Code signing is required for product type 'Unit Test Bundle' in SDK 'iOS 11.0.1'

    Code signing is required for product type 'Unit Test Bundle' in SDK 'iOS 11.0.1' 进入 projects and lis ...

  8. Yarn上常驻Spark-Streaming程序调优

    对于长时间运行的Spark Streaming作业,一旦提交到YARN群集便需要永久运行,直到有意停止.任何中断都会引起严重的处理延迟,并可能导致数据丢失或重复.YARN和Apache Spark都不 ...

  9. 史上最全面的SignalR系列教程-6、SignalR 实现聊天室

    1.概述 通过前面几篇文章对SignalR的详细介绍.我们知道Asp.net SignalR是微软为实现实时通信的一个类库.一般情况下,SignalR会使用JavaScript的长轮询(long po ...

  10. 通过 Channel 实现 Goroutine Pool

    最近用到了 Go 从 Excel 导数据到服务器内部 用的是 http 请求 但是发现一个问题 从文件读取之后 新开 Goroutine 会无限制新增 导致全部卡在初始化请求 于是乎就卡死了 问题模拟 ...