本文由笔者翻译于Netty官方文档,如有任何翻译上的错误,请联系笔者,感谢!
前言
问题
现在我们使用通用应用程序或库来彼此通信。例如,我们经常使用HTTP客户端库从web服务器检索信息,并通过web服务调用远程过程调用。然而,一个通用协议或它的实现有时并不能很好地扩展。这就像我们没有使用通用的HTTP服务器来交换巨大的文件、电子邮件消息和接近实时的消息(如财务信息和多人游戏数据)。需要的是一个高度优化的协议实现,专门用于特定的目的。例如,您可能想实现一个针对基于ajax的聊天应用程序、媒体流或大型文件传输进行优化的HTTP服务器。您甚至可能想设计和实现一个全新的协议,它可以根据您的需要进行精确定制。另一种不可避免的情况是,您必须处理遗留的专有协议,以确保与旧系统的互操作性。在这种情况下,重要的是在不牺牲应用程序的稳定性和性能的情况下,我们可以多快地实现该协议。
解决方案
Netty项目致力于提供一个异步事件驱动的网络应用框架和工具,用于快速开发可维护的高性能和高可伸缩性协议服务器和客户端。
换句话说,Netty是一个NIO客户端服务器框架,它支持快速而简单地开发网络应用程序,如协议服务器和客户端。它极大地简化和简化了网络编程,如TCP和UDP套接字服务器开发。“快速和简单”并不意味着结果应用程序将受到可维护性或性能问题的影响。
Netty是经过精心设计的,从许多协议(如FTP、SMTP、HTTP和各种基于二进制和文本的遗留协议)的实现中吸取了经验。因此,Netty成功地找到了一种方法,可以在不妥协的情况下实现开发的易用性、性能、稳定性和灵活性。
有些用户可能已经发现了其他声称具有相同优势的网络应用程序框架,您可能想知道是什么使Netty与它们如此不同。答案是它所建立的哲学。
Netty从一开始就为您提供API和实现方面最舒适的体验。这不是一些有形的东西,但你会意识到,这种哲学将使你的生活更容易,当你阅读本指南和玩Netty。
开始
本章通过简单的例子介绍了Netty的核心构造,让你快速入门。当你在本章结束的时候,你将能够写一个客户端和一个服务器在Netty之上。如果您喜欢使用自顶向下的方法来学习,您可能希望从第2章“架构概述”开始,然后再回到这里。
开始之前
运行本章示例的最低要求只有两个;最新版本的Netty和JDK 1.6或以上。最新版本的Netty可以在项目下载页面中找到。要下载正确版本的JDK,请参考您首选的JDK供应商的网站。
在阅读的过程中,你可能会对本章介绍的课程有更多的疑问。如果您想了解更多有关它们的信息,请参阅API参考。为了您的方便,本文档中的所有类名都链接到在线API参考。此外,如果有任何不正确的信息、语法错误或打字错误,请不要犹豫联系Netty项目社区,并让我们知道,如果您有任何好的想法来帮助改进文档。
写一个丢弃服务
世界上最简单的协议不是“你好,世界!”但丢弃。它是一种丢弃任何接收到的数据而不进行任何响应的协议。
要实现丢弃协议,您需要做的唯一一件事就是忽略所有接收到的数据。让我们直接从处理程序实现开始,它处理Netty生成的I/O事件。
package io.netty.example.discard;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* Handles a server-side channel.
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
// Discard the received data silently.
((ByteBuf) msg).release(); // (3)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
1.DiscardServerHandler扩展了ChannelInboundHandlerAdapter,它是ChannelInboundHandler的实现。ChannelInboundHandler提供了各种可以覆盖的事件处理程序方法。现在,扩展ChannelInboundHandlerAdapter就足够了,而不需要您自己实现处理程序接口。
2.我们在这里重写channelRead()事件处理程序方法。每当从客户端接收到新数据时,就会随着接收到的消息调用此方法。在本例中,接收到的消息的类型是ByteBuf。
3.要实现丢弃协议,处理程序必须忽略接收到的消息。ByteBuf是一个引用计数对象,必须通过release()方法显式地释放它。请记住,释放传递给处理程序的引用计数对象是处理程序的责任。通常,channelRead()处理程序方法的实现如下所示:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}
4.当Netty由于I/O错误或处理程序实现由于处理事件时抛出异常而引发异常时,使用Throwable调用exceptionCaught()事件处理程序方法。在大多数情况下,应该记录捕获的异常,并在这里关闭与之关联的通道,尽管此方法的实现可能因您想要处理异常情况而有所不同。例如,您可能希望在关闭连接之前发送带有错误代码的响应消息。
到目前为止一切顺利。我们已经实现了丢弃服务器的前半部分。现在剩下的工作是编写main()方法,该方法使用DiscardServerHandler启动服务器。
package io.netty.example.discard;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* Discards any incoming data.
*/
public class DiscardServer {
private int port;
public DiscardServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)
.childHandler(new ChannelInitializer() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128) // (5)
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync(); // (7)
// Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
new DiscardServer(port).run();
}
}
1.NioEventLoopGroup是一个处理I/O操作的多线程事件循环。Netty为不同类型的传输提供了各种EventLoopGroup实现。在本例中,我们将实现一个服务器端应用程序,因此将使用两个NioEventLoopGroup。第一个,通常称为“boss”,接受一个传入连接。第二个,通常称为“worker”,在老板接受连接并向worker注册已接受的连接后,处理已接受连接的流量。使用多少线程以及如何将它们映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
2.ServerBootstrap是一个设置服务器的助手类。您可以直接使用通道设置服务器。但是,请注意,这是一个乏味的过程,而且在大多数情况下不需要这样做。
3.这里,我们指定使用NioServerSocketChannel类,该类用于实例化一个新通道以接受传入连接
4.这里指定的处理程序将始终由新接受的通道进行评估。ChannelInitializer是一个特殊的处理程序,用于帮助用户配置新通道。您很可能希望通过添加一些处理程序(如DiscardServerHandler)来配置新通道的ChannelPipeline,以实现网络应用程序。随着应用程序变得越来越复杂,您可能会向管道中添加更多的处理程序,并最终将这个匿名类提取到一个顶级类中。
5.您还可以设置特定于通道实现的参数。我们正在编写一个TCP/IP服务器,因此允许设置套接字选项,如tcpNoDelay和keepAlive。请参考ChannelOption的apidocs和特定的ChannelConfig实现,以获得有关受支持的ChannelOptions的概述。
6.您注意到option()和childOption()了吗?option()用于接受传入连接的NioServerSocketChannel。childOption()用于父服务器通道(在本例中为NioServerSocketChannel)接受的通道。
7.我们现在准备走了。剩下的工作就是绑定到端口并启动服务器。在这里,我们绑定到机器中所有nic(网络接口卡)的端口8080。现在可以任意多次调用bind()方法(使用不同的绑定地址)。
恭喜你!您刚刚完成了在Netty上的第一个服务器。
查看收到的数据
现在我们已经编写了我们的第一个服务器,我们需要测试它是否真的工作。最简单的测试方法是使用telnet命令。例如,您可以在命令行中输入telnet localhost 8080并键入一些内容。
但是,我们能说服务器工作正常吗?我们不能真正知道这一点,因为它是一个废弃服务器。你不会得到任何回应。为了证明它确实在工作,让我们修改服务器来打印它收到的内容。
我们已经知道,只要接收到数据,就会调用channelRead()方法。让我们在DiscardServerHandler的channelRead()方法中放入一些代码:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
while (in.isReadable()) { // (1)
System.out.print((char) in.readByte());
System.out.flush();
}
} finally {
ReferenceCountUtil.release(msg); // (2)
}
}
1.这个低效循环实际上可以简化为:System.out.println(in.toString(io.net . uti.charsetutil.us_ascii))
2.您也可以在这里使用in.release()。
如果您再次运行telnet命令,您将看到服务器打印它接收到的内容。
丢弃服务器的完整源代码位于该发行版的io.net .example.丢弃包中。
写一个应答服务器
到目前为止,我们一直在使用数据而没有响应。然而,服务器通常应该响应请求。让我们学习如何通过实现ECHO协议向客户端编写响应消息,接收到的任何数据都会被发回。
与我们在前几节中实现的丢弃服务器的惟一区别是,它将发送回接收到的数据,而不是将接收到的数据打印到控制台。因此,修改channelRead()方法就足够了
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
1.ChannelHandlerContext对象提供各种操作,使您能够触发各种I/O事件和操作。在这里,我们调用write(Object)来逐字写入接收到的消息。请注意,我们没有像在丢弃示例中那样释放接收到的消息。这是因为Netty在将其写入网络时为您释放了它。write(Object)不会将消息写入网络。它在内部进行缓冲,然后通过ctx.flush()将其刷新到连接上。或者,为了简洁,您可以调用ctx.writeAndFlush(msg)。
2.write(Object)不会将消息写入网络。它在内部进行缓冲,然后通过ctx.flush()将其刷新到连接上。或者,为了简洁,您可以调用ctx.writeAndFlush(msg)。
如果您再次运行telnet命令,您将看到服务器发回您发送给它的任何内容。
echo服务器的完整源代码位于该发行版的io.net .example.echo包中。
写一个时间服务器
未完,待续!
最近更新于2020.11.04