Netty是一款NIO客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。 它极大地简化并简化了TCP和UDP套接字服务器等网络编程。
'快速和简单'并不意味着由此产生的应用程序将受到可维护性或性能问题的困扰。 Netty的设计经验非常丰富,包括FTP,SMTP,HTTP以及各种基于二进制和文本的传统协议等。 因此,Netty成功地找到了一种方法来实现轻松的开发,性能,稳定性和灵活性,而无需妥协。
Features
Design
用于各种传输类型的统一API - 阻塞和非阻塞套接字
基于灵活和可扩展的事件模型,可以清晰地分离问题
高度可定制的线程模型 - 单线程,一个或多个线程池,如SEDA
真正的无连接数据报套接字支持(自3.1以来)
Ease of use
没有额外的依赖关系,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了
注意:某些组件(如HTTP / 2)可能有更多要求。 请参阅需求页面了解更多信息
Performance
Better throughput, lower latency(吞吐量更好,延迟更低)
Less resource consumption(更少的资源消耗)
Minimized unnecessary memory copy(最小化不必要的内存拷贝)
Security
Complete SSL/TLS and StartTLS support(完整的SSL / TLS和StartTLS支持)
---------------------------------------------------------------------------------
netty 4.x
Preface
The Problem
现在我们使用通用应用程序或库来相互通信。例如,我们经常使用HTTP客户端库从Web服务器检索信息并通过Web服务调用远程过程调用。但是,通用协议或其实现有时不能很好地扩展。这就好比我们不使用通用HTTP服务器来交换巨大的文件,电子邮件和接近实时的消息,例如财务信息和多人游戏数据。所需要的是高度优化的专用于特殊用途的协议实现。例如,您可能希望实施针对基于AJAX的聊天应用程序,媒体流或大型文件传输进行优化的HTTP服务器。另一个不可避免的情况是,您必须处理传统专有协议以确保与旧系统的互操作性。在这种情况下重要的是我们可以多快地实现该协议,同时不会牺牲最终应用程序的稳定性和性能。The Solution
Netty项目旨在为快速开发可维护的高性能·高可扩展性协议服务器和客户端提供异步事件驱动的网络应用程序框架和工具。“快速而简单”并不意味着由此产生的应用程序将受到可维护性或性能问题的困扰。 Netty的设计经验非常丰富,包括FTP,SMTP,HTTP以及各种基于二进制和文本的传统协议等。 因此,Netty成功地找到了一种方法来实现轻松的开发,性能,稳定性和灵活性,而无需妥协。
有些用户可能已经找到了其他宣称具有相同优势的网络应用程序框架,并且您可能想问一下Netty与他们有何不同之处。 答案是基于netty的哲学。 Netty旨在为您提供在第一天的API和实施方面最舒适的体验。 这不是有形的,但你会意识到,当你阅读本指南并与Netty一起玩时,这一哲学将使你的生活变得更加轻松。
Getting Started
本章将以简单的例子来介绍Netty的核心结构,以便您快速入门。 当你在本章结尾时,你将能够在Netty之上立即编写客户端和服务器。
Before Getting Started
运行本章中示例的最低要求只有两个; 最新版本的Netty和JDK 1.6或更高版本。
Writing a Discard Server
世界上最简单的协议不是'Hello, World!' 而是DISCARD。 这是一个协议,丢弃任何收到的数据没有任何回应。
要实施DISCARD协议,您只需要忽略所有收到的数据。 让我们从处理器实现开始,它处理由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、为了实现DISCARD协议,处理程序必须忽略收到的消息。 ByteBuf是一个引用计数的对象,必须通过release()方法明确释放。 请记住,处理程序的职责是释放传递给处理程序的任何引用计数的对象。 通常,channelRead()处理程序方法的实现如下所示:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}
4、当由于I / O错误或由于在处理事件时抛出异常而导致的处理程序实现导致Netty引发异常时,会使用Throwable调用exceptionCaught()事件处理程序方法。 在大多数情况下,应该记录捕获到的异常并关闭其相关通道,尽管此方法的实现可能因您想要处理异常情况而需要做的不同而有所不同。 例如,您可能希望在关闭连接之前发送包含错误代码的响应消息。
到现在为止还挺好。 我们已经实施了DISCARD服务器的前半部分。 现在剩下的就是编写用DiscardServerHandler启动服务器的main()方法。
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<SocketChannel>() { // (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;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new DiscardServer(port).run();
}
}
1、NioEventLoopGroup是一个处理I / O操作的多线程事件循环。 Netty为不同类型的传输提供了各种EventLoopGroup实现。 在这个例子中,我们正在实现一个服务器端应用程序,因此将使用两个NioEventLoopGroup。 第一个通常称为“boss”,接受传入的连接。 第二个通常称为“worker”,一旦老板接受连接并将接受的连接注册给工作人员,则处理接受连接的流量。 使用多少个线程以及它们如何映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
2、ServerBootstrap是设置服务器的辅助类。 您可以直接使用Channel设置服务器。 但是,请注意,这是一个乏味的过程,在大多数情况下您不需要这样做。
3、在这里,我们指定使用NioServerSocketChannel类来实例化一个新的Channel来接受传入的连接。
4、此处指定的处理程序将始终由新接受的通道评估。 ChannelInitializer是一个特殊的处理程序,旨在帮助用户配置新的通道。 您很可能希望通过添加一些处理程序(如DiscardServerHandler)来配置新通道的ChannelPipeline,以实现您的网络应用程序。 随着应用程序变得复杂,您可能会向流水线添加更多处理程序,并最终将此匿名类提取到顶级类中。
5、您还可以设置特定于Channel实现的参数。 我们正在编写一个TCP / IP服务器,因此我们可以设置套接字选项,如tcpNoDelay和keepAlive。 请参阅ChannelOption的apidocs和特定的ChannelConfig实现,以获得关于支持的ChannelOptions的概述。
6、你有没有注意到option()和childOption()? option()用于接受传入连接的NioServerSocketChannel。 childOption()适用于父ServerChannel接受的通道,在这种情况下是NioServerSocketChannel。
7、我们现在准备好了。 剩下的就是绑定到端口并启动服务器。 在这里,我们绑定到机器中所有NIC(网络接口卡)的端口8080。 您现在可以根据需要多次调用bind()方法(使用不同的绑定地址)。
恭喜! 你刚刚在Netty之上完成了你的第一台服务器。
Looking into the Received Data
现在我们已经编写了我们的第一台服务器,我们需要测试它是否真的有效。 测试它的最简单方法是使用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.netty.util.CharsetUtil.US_ASCII))
2、或者,你可以在这里做in.release()。
如果您再次运行telnet命令,您将看到服务器打印收到的内容。
Writing an Echo Server
到目前为止,我们一直在消费数据而没有做出任何回应。 然而,服务器通常应该对请求做出响应。 让我们学习如何通过实现ECHO协议向客户端写入响应消息,其中接收到的数据将被发回。
与我们在前面部分中实现的丢弃服务器唯一的区别在于,它将接收到的数据发送回来,而不是将接收到的数据打印到控制台。 因此,重新修改channelRead()方法就足够了:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
1、一个ChannelHandlerContext对象提供了各种操作,使您能够触发各种I / O事件和操作。 在这里,我们调用write(Object)逐字写入收到的消息。 请注意,我们没有发布收到的消息,与我们在DISCARD示例中所做的不同。 这是因为当Netty写出来时,Netty会为你发布它。
2、ctx.write(Object)不会将消息写出来。 它在内部缓冲,然后通过ctx.flush()刷新到线路。 另外,为简洁起见,你可以调用ctx.writeAndFlush(msg)。
如果你再次运行telnet命令,你会看到服务器发回你发送给它的任何东西。
Writing a Time Server
在本节中实现的协议是TIME协议。 它与前面的例子不同之处在于,它发送的消息包含一个32位整数,但未收到任何请求,并在发送消息后关闭连接。 在本例中,您将学习如何构建和发送消息,并在完成时关闭连接。
因为我们将忽略任何收到的数据,但只要连接建立就发送消息,我们不能使用此次的channelRead()方法。 相反,我们应该重写channelActive()方法。 以下是实现:
package io.netty.example.time;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(final ChannelHandlerContext ctx) { // (1)
final ByteBuf time = ctx.alloc().buffer(4); // (2)
time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
final ChannelFuture f = ctx.writeAndFlush(time); // (3)
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
}); // (4)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}