关注微信公众号(瓠悠笑软件部落),大家一起学习,一起摸鱼。
本章将概括
- OIO —— blocking transport 阻塞式传输
- NIO —— asynchronous transport 异步式传输
- Local transport —— asynchronous communications within a JVM
(JVM中的异步通信) - Embedded transport —— testing your ChannelHandlers
流经网络的数据始终具有相同的类型:字节(bytes)。怎么这些移动字节主要取决于我们称之为网络传输的内容,这一概念有助于我们抽象出数据的基础机制传递。用户不关心细节;他们只是想确定他们的字节可靠地发送和接收。
如果您有使用Java进行网络编程的经验,那么您可能在某些时候发现需要支持比预期更多的并发连接。如果您随后尝试从阻塞传输切换到非阻塞传输,则可能会遇到问题,因为两个网络API完全不同。
然而,Netty在其所有传输实现上层叠了一个通用API,使这种转换比直接使用JDK更简单。生成的代码将不受实现细节的影响,您也不会需要对整个代码库进行广泛的重构。总之,你可以把时间花在做有成效的事情上。
在本章中,我们将研究这个通用API,将其与JDK进行对比,以展示其更加易用的易用性。我们将解释与Netty捆绑在一起的传输实现以及适合每个实例的用例。有了这些信息,您应该可以直接为您的应用选择最佳选项。
本章的唯一先决条件是Java编程语言的知识。有网络框架或网络编程的经验是一个优点,但不是必需的。我们首先要看看运输在现实世界中的运作方式。
4.1 Case study: transport migration
我们将开始使用一个只接受连接的应用程序来研究传输,将“Hi!”写入客户端,然后关闭连接。
4.1.1 Using OIO and NIO without Netty
我们将介绍仅使用JDK API的应用程序的阻塞(OIO)和异步(NIO)版本。下一个清单显示了阻止实现。如果您曾经历过使用JDK进行网络编程的乐趣,那么这段代码将唤起您愉快的回忆。
package com.huxing.study.oio;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class PlainOioServer {
public void serve(int port) throws IOException {
// Binds the server to the specified port
final ServerSocket socket = new ServerSocket(port);
for(;;) {
// Accepts a connection
final Socket clientSocket = socket.accept();
System.out.println("Accepted connection form " + clientSocket);
// Creates a new thread to handle the connection
new Thread(new Runnable() {
public void run() {
try {
OutputStream out = clientSocket.getOutputStream();
// Writes message to the connected client
String message = "Hi!\r\n";
out.write(message.getBytes("UTF-8"));
out.flush();
// closes the connection
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException ex) {
// ignore on close
}
}
}
}).start(); // start the thread
}
}
}
此代码可以充分处理中等数量的并发客户端。但随着应用程序变得流行,您会注意到它不能很好地扩展到成千上万的并发传入连接。您决定转换为异步网络,但很快发现异步API完全不同,所以现在你必须重写你的应用程序。
非阻塞版本显示在下面的清单中。
package com.huxing.study.oio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class PlainNioServer {
public void serve(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
ServerSocket serverSocket = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
// binds the server to the selected port
serverSocket.bind(address);
// opens the selector for handling channels
Selector selector = Selector.open();
// registers the ServerSocket with the Selector to accept connections
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
String message = "Hi!\r\n";
final ByteBuffer msg = ByteBuffer.wrap(message.getBytes());
for(;;) {
try {
// waits for new events to process; blocks until the next incoming event
selector.select();
} catch (IOException ex) {
ex.printStackTrace();
// handle exception
break;
}
// obtains all SelectionKey instances that received events
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readyKeys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
// checks if the event is a new connection ready to be accepted
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
// accepts client and registers it with the selector
client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, msg.duplicate());
System.out.println("Accepted connection from " + client);
}
// checks if the socket is ready for writing data
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
while (buffer.hasRemaining()) {
// writes data to the connected client
if(client.write(buffer) == 0) {
break;
}
}
client.close();
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException exception) {
// ignore on close.
}
}
}
}
}
}
正如您所看到的,尽管此代码与前一版本完全相同,但它完全不同。如果为非阻塞I / O重新实现这个简单的应用程序需要完全重写,考虑移植真正复杂的东西所需的工作量。考虑到这一点,让我们看看使用Netty实现应用程序时的外观。
4.1.2 Using OIO and NIO with Netty
我们首先编写应用程序的另一个阻止版本,这次使用Netty框架,如下面的清单所示。
package com.huxing.study.oio;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.oio.OioServerSocketChannel;
public class NettyOioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi!\r\n".getBytes("UTF-8")));
EventLoopGroup group = new OioEventLoopGroup();
try {
// creates a ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group)
// uses OioEventLoopGroup to allow blocking mode(old I/O)
.channel(OioServerSocketChannel.class)
// specifies ChannelInitializer that will be called for each accepted connection
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
// adds a ChannelInboundHandlerAdapter to intercept and handle events
new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// writes message to client and adds ChannelFutureListener to close connection once message is written
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
// binds server to accept connections
ChannelFuture channelFuture = serverBootstrap.bind().sync();
channelFuture.channel().closeFuture().sync();
} finally {
// releases all resources
group.shutdownGracefully().sync();
}
}
}
接下来,我们将使用Netty实现与非阻塞 I / O 相同的逻辑。
4.1.3 Non-blocking Netty version
除了两个突出显示的行之外,下一个列表几乎与列表4.3相同。这就是从阻塞(OIO)切换到非阻塞(NIO)传输所需的全部内容。
package com.huxing.study.oio;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyNioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi!\r\n".getBytes("UTF-8")));
EventLoopGroup group = new NioEventLoopGroup();
try {
// creates a ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group)
// uses NioEventLoopGroup for non-blocking mode
.channel(NioServerSocketChannel.class)
// specifies ChannelInitializer that will be called for each accepted connection
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
// adds a ChannelInboundHandlerAdapter to intercept and handle events
new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// writes message to client and adds ChannelFutureListener to close connection once message is written
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
// binds server to accept connections
ChannelFuture channelFuture = serverBootstrap.bind().sync();
channelFuture.channel().closeFuture().sync();
} finally {
// releases all resources
group.shutdownGracefully().sync();
}
}
}
由于Netty为每个传输实现公开了相同的API,无论您选择哪种,您的代码几乎不受影响。在所有情况下,实施都是根据接口Channel,ChannelPipeline和ChannelHandler定义。看过使用基于Netty的传输的一些好处之后,让我们仔细看看传输API本身。
4.2 Transport API
传输API的核心是接口通道,它用于所有I / O操作。 Channel类层次结构如图4.1所示。
该图显示Channel具有ChannelPipeline和ChannelConfig 分配给它。 ChannelConfig保存Channel的所有配置设置并支持热更改。由于特定传输可能具有唯一设置,因此可以实现ChannelConfig的子类型。 (有关ChannelConfig实现,请参阅Javadocs。)
由于Channel是唯一的,因此将Channel声明为 java.lang.Comparable的子接口旨在保证有序性。因此,如果两个不同的Channel实例返回相同的哈希码,则AbstractChannel中的 compareTo() 会抛出一个 Error。
ChannelPipeline保存将应用于入站和出站数据和事件的所有ChannelHandler实例。 这些ChannelHandler实现了应用程序的逻辑,用于处理状态更改和数据处理。
ChannelHandler的典型用途包括:
- 将数据从一个格式转换称另外一个格式
- 遇到异常的时候提供 notification
- 当一个 Channel 被激活(active)或者失活(inactive)的时候提供 notification
- 当一个Channel 从一个 EventLoop里面注册或者注销时。提供 notification
- 为用户自定义的时间提供 notification
intercepting filter ChannelPipeline实现了通用设计
模式,拦截过滤器。 UNIX管道是另一个熟悉的例子:命令链接在一起,一个命令的输出连接到排在下一行的输入。
您还可以根据需要通过添加或删除 ChannelHandler 实例来动态修改 ChannelPipeline 。 Netty 的这种功能可以被利用来构建高度灵活的应用程序。 例如,只要在请求协议时向ChannelPipeline 添加适当的 ChannelHandler(SslHandler),就可以按需支持STARTTLS 协议。 除了访问指定的ChannelPipeline和ChannelConfig之外,您还可以使用Channel方法,其中最重要的方法在表4.1中列出
稍后我们将详细讨论所有这些功能的用法。 现在,请记住,Netty提供的广泛功能依赖于少量接口。 这意味着您可以对应用程序逻辑进行重大修改,而无需对代码库进行大量重构。考虑编写数据并将其刷新到远程对等方的常见任务。 以下清单说明了Channel.writeAndFlush() 用于此目的。
Netty的Channel实现是线程安全的,因此您可以存储对Channel的引用,并在需要向远程对等方写入内容时使用它,即使在使用多个线程时也是如此。 以下清单显示了使用多个线程编写的简单示例。 请注意,保证按顺序发送消息。
4.3 Included transports
Netty捆绑了几个可以使用的传输。 因为并非所有协议都支持所有协议,所以必须选择与应用程序使用的协议兼容的传输。 在本节中,我们将讨论这些关系。
表4.2列出了Netty提供的所有传输。
我们将在下一节中更详细地讨论这些传输。
4.3.1 NIO —— non-blocking I/O
NIO提供所有I / O操作的完全异步实现。 它利用了自JDK 1.4中引入NIO子系统以来可用的基于选择器的API。选择器背后的基本概念是作为注册表,您要求在通道状态发生变化时收到通知。
可能的状态变化是:
- 一个新 Channel 被接受并准备好了。
- 一个 Channel 链接建立好了
- 一个 Channel 的 data 已经准备好了,可以读取了。
- 一个 Channel 已经可以写数据了。
在应用程序对状态更改做出反应之后,重置选择器并重复该过程,在检查更改的线程上运行并相应地响应它们。 表4.3中显示的常量表示由类 java.nio.channels.SelectionKey 定义的位模式。 组合这些模式以指定应用程序请求通知的状态更改集。
NIO的这些内部细节被所有Netty的传输实现共同的用户级API隐藏。 图4.2显示了流程。
Zero-copy
零拷贝是目前仅适用于NIO和Epoll传输的功能。 它允许您快速有效地将数据从文件系统移动到网络,而无需从内核空间复制到用户空间,这可以显着提高FTP或HTTP等协议的性能。 并非所有操作系统都支持此功能。具体而言,它不适用于实现数据加密或压缩的文件系统 - 只能传输文件的原始内容。 相反,传输已加密的文件不是问题。
4.3.2 Epoll —— native non-blocking transport for Linux
正如我们之前解释的那样,Netty的NIO传输基于Java提供的异步/非阻塞网络的通用抽象。 虽然这确保了Netty的非阻塞API可以在任何平台上使用,但它也带来了限制,因为JDK必须做出妥协才能在所有系统上提供相同的功能。
Linux作为高性能网络平台的重要性日益增加,已经开发出许多高级功能,包括epoll,一种高度可扩展的I / O事件通知功能。 这个API自Linux内核版本2.5.44(2002)开始提供,它提供了比旧版POSIX选择和更好的性能轮询系统调用,现在是Linux上非阻塞网络的事实标准。 Linux JDK NIO API使用这些epoll调用。
Netty为Linux提供了一个NIO API,它使用epoll的方式与自己的设计更加一致,并且使用中断的方式成本更低。 如果您的应用程序适用于Linux,请考虑使用此版本; 你会发现重负载下的性能优于JDK的NIO实现。
这种传输的语义与图4.2中所示的相同,并且它的使用很简单。 有关示例,请参阅清单4.4。 要在该列表中将epoll替换为NIO,请将EpioEventLoopGroup替换为NioEventLoopGroup,使用EpollServerSocketChannel.class替换NioServerSocketChannel.class。
4.3.3 OIO - old blockingI/O
Netty的OIO传输实现代表了一种妥协:它通过公共传输API访问,但由于它是基于java.net的阻塞实现构建的,因此它不是异步的。 但它非常适合某些用途。
例如,您可能需要移植使用进行阻塞调用的库的遗留代码(例如JDBC),将逻辑转换为非阻塞可能不切实际。 相反,您可以在短期内使用Netty的OIO传输,并稍后将代码移植到纯异步传输之一。 让我们看看它是如何工作的。
在java.net API中,通常有一个线程接受到达侦听ServerSocket的新连接。 为与对等体的交互创建新套接字,并分配新线程来处理流量。 这是必需的,因为特定套接字上的任何I / O操作都可以随时阻止。 使用单个线程处理多个套接字很容易导致一个套接字上的阻塞操作也会占用所有其他套接字。
鉴于此,您可能想知道 Netty 如何使用与异步传输相同的API来支持NIO。 答案是 Netty 使用 SO_TIMEOUT 套接字标志,该标志指定等待 I / O 操作完成的最大毫秒数。 如果操作未能在指定的时间间隔内完成,则抛出 SocketTimeoutException。 Netty 捕获此异常并继续处理循环。 在下一个 EventLoop 运行时,它将再次尝试。 这是像 Netty 这样的异步框架可以支持OIO的唯一方法。 图4.3说明了这种逻辑。
4.3.4 Local transport for communication within a JVM
Netty 为在同一JVM中运行的客户端和服务器之间的异步通信提供本地传输。 同样,此传输支持所有Netty传输实现通用的API。
在此传输中,与服务器通道关联的 SocketAddress 未绑定到物理网络地址; 相反,只要服务器正在运行,它就存储在注册表中,并在 Channel 关闭时取消注册。 由于传输不接受实际网络流量,因此无法与其他传输实施互操作。 因此,希望连接到使用此传输的服务器(在同一JVM中)的客户端也必须使用它。 除此限制外,其使用方法与其他运输方式相同。
4.3.5 Embedded transport
Netty提供了一个额外的传输,允许您将 ChannelHandler 作为辅助类嵌入其他ChannelHandler 中。 通过这种方式,您可以扩展 ChannelHandler 的功能,而无需修改其内部代码。 这种嵌入式传输的关键是一个具体的 Channel 实现,毫不奇怪,它被称为EmbeddedChannel。 在第9章中,我们将详细讨论如何使用此类为 ChannelHandler 实现创建单元测试用例。
4.4 Transport use cases
现在我们已经详细研究了所有传输,让我们考虑选择特定用途协议的因素。 如前所述,并非所有传输都支持所有核心协议,这可能会限制您的选择。 表4.4显示了矩阵
出版时支持的运输和协议。
请参阅RFC 2960中的流控制传输协议(SCTP)的说明
Enabling SCTP on Linux
请参阅RFC 2960中的流控制传输协议(SCTP)的说明.
例如,对于Ubuntu,您将使用以下命令:sudo apt-get install libsctp1
对于Fedora,你会使用yum:
sudo yum install kernel-modules-extra.x86_64 lksctp-tools.x86_64
有关如何启用SCTP的更多信息,请参阅Linux发行版的文档。
虽然只有SCTP具有这些特定要求,但其他传输可能有自己的配置选项需要考虑。 此外,服务器平台可能需要与客户端配置不同,如果仅支持更多数量的并发连接。
以下是您可能遇到的用例。
- Non-blocking code base —— 如果您的代码库中没有阻塞调用,或者
你可以限制它们 - 在Linux上使用NIO或epoll总是一个好主意。虽然NIO / epoll旨在处理许多并发连接,但它在较小的数量下也能很好地工作,特别是考虑到它在连接之间共享线程的方式。 - Blocking code base —— 正如我们已经说过的那样,如果您的代码库依赖很多
阻塞I / O和你的应用程序有相应的设计,如果你试图直接转换为Netty的NIO传输,你可能会遇到阻塞操作的问题。 考虑分阶段迁移,而不是重写您的代码,考虑分阶段迁移:一旦修改了代码,就开始使用OIO并转移到NIO(如果您使用的是Linux,则转移到epoll)。 - Communications within the same JVM—— 同一JVM内的通信不需要通过网络公开服务,这是本地传输的完美用例。 这将消除实际网络操作的所有开销,同时仍然使用您的Netty代码库。 如果需要通过网络公开服务,您只需使用NIO或OIO替换传输。
- Testing your ChannelHandler implementations —— 同一 JVM 内的通信不需要通过网络公开服务,这是本地传输的完美用例。 这将消除实际网络操作的所有开销,同时仍然使用您的Netty代码库。 如果需要通过网络公开服务,您只需使用NIO或OIO替换传输。…
4.5 Summary
在本章中,我们研究了传输,它们的实现和使用,以及Netty如何将它们呈现给开发人员。
我们浏览了Netty附带的运输工具并解释了他们的行为。
我们还查看了它们的最低要求,因为并非所有传输都使用相同的Java版本,有些只能在特定的操作系统上使用。 最后,我们讨论了如何将传输与特定用例的要求相匹配。
在下一章中,我们将重点介绍Netty的数据容器ByteBuf和ByteBufHolder。 我们将展示如何使用它们以及如何从中获得最佳性能。