传输API的核心是interface Channnel,它被用于所有的I/O操作。
每个Channel都将会被分配一个ChannelPipeline和ChannelConfig。ChannelConfig包含了该Channel的所有配置设置,并且支持热更新。由于支持热更新。由于特定的传输可能具有独特的设置,所以它可能会实现一个ChannelConfig的子类型。
由于Channel是独一无二的,所以为了保证顺序将Channel声明为java.lang.Comparable的一个子接口。因此,如果两个不同的Channel实例都返回了相同的散列码,那么AbstractChannel中的comparaTo()方法的实现将会抛出一个Error。
ChannelPipeline持有所有将应用于入站和出站数据以及事件的ChannelHandler实例,这些ChannelHandler实现了应用程序用于处理状态变化以及数据处理的逻辑。
ChannelHandler的典型用途包括:
(1)将数据从一种格式转换为另一种格式;
(2)提供异常的通知;
(3)提供Channel变为活动的或者非活动的通知;
(4)提供当Channel注册到EventLoop或者EventLoop注销时的通知;
(5)提供有关用户自定义事件的通知。
拦截过滤器 ChannelPipeline实现了一种常见的设计模式——拦截过滤器。UNIX管道是另外一个熟悉的例子:多个命令被链接在一起,其中一个命令的输出端将连接到命令行中下一个命令的输入端。
你也可以根据需要通过添加或者移除ChannelHandler实例来修改ChannelPipeline。通过利用Netty的这项能力可以构建出高度灵活的应用程序。
除了访问所分配的ChannelPipeline和ChannelConfig之外,也可以利用Channel的其它方法,其中最重要的列举如下:
方法名 | 描述 |
eventLoop | 返回分配给Channel的EventLoop |
pipeline | 返回分配给Channel的ChannelPipeline |
isActive | 如果Channel是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket传 输一旦连接到了远程节点便是活动的,而一个Datagram传输一旦被打开便是活动的 |
localAddress | 返回本地的SocketAddress |
remoteAddress | 返回远程的SocketAddress |
write | 讲数据写到远程节点。这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷 |
flush | 将之前已写的数据冲刷到底层传输,如一个Socket |
writeAndFlush | 一个简便的方法,等同于调用write()并接着调用flush() |
写数据并将其冲刷到远程节点:
Channel channel = ...;
//创建持有要写数据的ByteBuf
ByteBuf buf = Unpooled.copiedBuffer("your data", CharsetUtil.UTF_8);
ChannelFuture future = channel.writeAndFlush(buf);
//添加CHannelFutureListener以便在写操作完成后接收通知
future.addListener(new ChannelFutureListener(){
@Override
public void operationComplete (ChannelFuture future){
//写操作完成,并且没有错误发生
if(future.isSuccess()){
System.out.println("Write successful");
} else {
//记录错误
System.out.println("Write error");
future.cause().printStackTrace();
}
}
});
Netty的Channel实现是线程安全的,因此你可以存储一个到Channel的引用,并且每当你需要像远程节点写数据时,都可以使用它,即使当时许多线程都在使用它。以下代码展示了一个多线程写数据的简单例子:
final Channel channel = ...;
//创建持有要写数据的ByteBuf
final ByteBuf buf = Unpooled.copiedBuffer("your data", CharsetUtil.UTF_8).retain();
//创建将数据写到Channel的Runnable
Runnable writer = new Runnable() {
@Override
public void run() {
channel.writeAndFlush(buf.duplicate());
}
};
//获取到线程池Executor的引用
Executor executor = Executors.newCachedThreadPool();
//递交写任务给线程池以便在某个线程中执行
//write in one thread
executor.execute(writer);
//递交另一个写任务以便在另一个线程中执行
//write in another thread
executor.execute(writer);
Netty内置了一些可开箱即用的传输。因为并不是它们所有的传输都支持每一种协议,所以你必须选择一个和你的应用程序所使用的协议相容的传输。
下表显示了所有Netty提供的传输。
名称 | 包 | 描述 |
NIO | io.netty.channel.socket.nio | 使用java.nio.channels包作为基础——基于选择器的方式 |
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 | 可以在VM内部通过管道进行通信的本地传输 |
Embeded | io.netty.channel.embeded | Embeded传输,允许使用ChannelHandler而又不需要一个真正的基于网络的传输。这在测试 你的ChannelHandler实现时非常有用 |
NIO提供了一个所有I/O操作的全异步实现。它利用了自NIO子系统被引入JDK1.4时便可用的基于选择器的API。
选择器背后的基本概念是充当一个注册表,在那里你将可以请求在Channel的状态发生变化时得到通知,可能变化的变化有:
(1)新的Channel已被接受并且就绪;
(2)Channel连接已经完成;
(3)Channel有已经就绪的可供读取的数据;
(4)Channel可用于写数据。
选择器运行在一个检查状态变化并对其做出相应响应的线程上,在应用程序对状态的改变做出响应之后,选择器将会被重置,并将重复这个过程。
下表中的常量值代表了由class java.nio.channels.SelectionKey定义的位模式。这些位模式可以组合起来定义一组应用程序正在请求通知的状态变化集。
名称 | 描述 |
OP_ACCEPT | 请求在接受新连接并创建Channel时获得通知 |
OP_CONNECT | 请求在建立一个连接时获得通知 |
OP_READ | 请求当数据已经就绪,可以从Channel中读取时获得通知 |
OP_WRITE | 请求当可以向Channel中写更多的数据时获得通知。这处理了套接字缓冲区被完全填满时的情况,这种情况 通常发生在数据的发送速度比远程节点可处理的速度更快的时候 |
零拷贝
零拷贝(zero-copy)是一种只有在使用NIO和Epoll传输时才可使用的特性。它使你可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核
空间复制到用户空间,其在像FTP或者HTTP这样的协议中可以显著地提升性能。但是,并不是所有的操作系统都支持这一特性。特别地,它对于实现了数据加密或者
压缩的文件系统是不可用的——只能传输文件的原始内容。反过来说,传输已被加密的文件则不是问题。
Netty的NIO传输基于Java提供的异步/非阻塞网络编程的通用抽象。虽然这保证了Netty的非阻塞API可以在任何平台上使用,但它也包含了相应的限制,因为JDK为了在所有系统上提供相同的功能,必须做出妥协。
Linux作为高性能网络编程的平台,其重要性与日俱增,这催生了大量先进特性的开发,其中包括epoll——一个高度可扩展的I/O事件通知特性。这个API自Linux内核版本2.5.44被引入,提供了比旧的POSIX select和poll系统调用更好地性能,同时现在也是Linux上非阻塞网络编程的事实标准。
Epoll使用简单,只需要将代码中的NioEventLoopGroup替换为EpollEventLoopGroup,并且将NioServerSocketChannel.class替换为EpollServerSocketChannel.class即可。
Netty的OIO传输实现表达了一种折中:它可以通过常规的传输API使用,但是由于它是建立在java.net包的阻塞实现之上的,所以它不是异步的。
在java.net API中,通常会有一个用来接受到达正在监听的ServerSocket的新连接的线程。会创建一个新的和远程节点进行交互的套接字,并且会分配一个新的用于处理相应通信流量的线程。这是必须的,因为某个指定套接字上的任何I/O操作在任意的时间点上都可能会阻塞。使用单个线程来处理多个套接字,很容易导致一个套接字上的阻塞操作也捆绑了所有其他的套接字。
Netty是如何能够使用和用于异步传输相同的API来支持OIO的呢?
Netty利用了SO_TIMEOUT这个Socket标志,它指定了等待一个I/O操作完成的最大毫秒数。如果操作在指定的时间间隔内没有完成,则将会抛出一个SocketTimeout Exception。Netty将捕获这个异常并继续处理循环。在EventLoop下一次运行时,它将再次尝试。这实际上也是类似于Netty这样的异步框架能够支持OIO的唯一方式。
Netty提供了一个Local传输,用于在同一个JVM中运行的客户端和服务器之间的异步通信。这样,这个传输也支持对于所有Netty传输实现都共同的API。
在这个传输中,和服务器Channel相关联的SocketAddress并没有绑定物理网络地址;相反,只要服务器还在运行,它就会被存储在注册表里,并在Channel关闭时注销。因为这个传输并不接受真正的网络流量,所以它并不能够和其他传输实现进行互操作。因此,客户端希望连接到(在同一个JVM中)使用了这个传输的服务器端时也必须使用它。除了这个限制,它的使用方式和其他的传输一模一样。
Netty提供了一种额外的传输,使得你可以将一组ChannelHandler作为帮助器类嵌入到其他的ChannelHandler内部。通过这种方式,你将可以扩展一个ChannelHandler的功能,而又不需要修改其内部代码。
不足为奇的是,Embedded纯属的关键是一个被称为EmbeddedChannel的具体的Channel实现。
传输 | TCP | UDP | SCTP | UDT |
NIO | 支持 | 支持 | 支持 | 支持 |
Epoll(仅Linux) | 支持 | 支持 | 不支持 | 不支持 |
OIO | 支持 | 支持 | 支持 | 支持 |
在Linux上启用SCTP
SCTP需要内核的支持,并且需要安装用户库。
例如,对于Ubuntu,可以使用下面的命令:
#sudo apt-get install libsctpl
对于Fedora,可以使用yum:
#sudo yum install kernel-modules-extra.x86_64 lksctp-tools.x86_64
应用程序的需求 | 推荐的传输 |
非阻塞代码库或者一个常规的起点 | NIO(或者在Linux上使用epoll) |
阻塞代码库 | OIO |
在同一个JVM内部的通信 | Local |
测试ChannelHandler的实现 | Embedded |