第 2 章 开始
最后更新于:2022-04-01 03:07:57
# 开始
[TOC=2,2]
本章围绕 Netty 的核心架构,通过简单的示例带你快速入门。当你读完本章节,你马上就可以用 Netty 写出一个客户端和服务器。
如果你在学习的时候喜欢“top-down(自顶向下)”,那你可能需要要从第二章《[Architectural Overview (架构总览)](#)》开始,然后再回到这里。
## 开始之前
在运行本章示例之前,需要准备:最新版的 Netty 以及 JDK 1.6 或以上版本。最新版的 Netty 在这[下载](http://netty.io/downloads.html)。自行下载 JDK。
阅读本章节过程中,你可能会对相关类有疑惑,关于这些类的详细的信息请请参考 API 说明文档。为了方便,所有文档中涉及到的类名字都会被关联到一个在线的 API 说明。当然,如果有任何错误信息、语法错误或者你有任何好的建议来改进文档说明,那么请[联系Netty社区](http://netty.io/community.html)。
*译者注:对本翻译有任何疑问,在[https://github.com/waylau/netty-4-user-guide/issues](https://github.com/waylau/netty-4-user-guide/issues)提问*
## 写个丢弃服务器
世上最简单的协议不是'Hello, World!' 而是 [DISCARD(丢弃)](http://tools.ietf.org/html/rfc863)。这个协议将会丢掉任何收到的数据,而不响应。
为了实现 DISCARD 协议,你只需忽略所有收到的数据。让我们从 handler (处理器)的实现开始,handler 是由 Netty 生成用来处理 I/O 事件的。
~~~
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* 处理服务端 channel.
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
// 默默地丢弃收到的数据
((ByteBuf) msg).release(); // (3)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
}
~~~
1.DiscardServerHandler 继承自 [ChannelInboundHandlerAdapter](http://netty.io/4.0/api/io/netty/channel/ChannelInboundHandlerAdapter.html),这个类实现了 [ChannelInboundHandler](http://netty.io/4.0/api/io/netty/channel/ChannelInboundHandler.html)接口,ChannelInboundHandler 提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承 ChannelInboundHandlerAdapter 类而不是你自己去实现接口方法。
2.这里我们覆盖了 chanelRead() 事件处理方法。每当从客户端收到新的数据时,这个方法会在收到消息时被调用,这个例子中,收到的消息的类型是 [ByteBuf](http://netty.io/4.0/api/io/netty/buffer/ByteBuf.html)
3.为了实现 DISCARD 协议,处理器不得不忽略所有接受到的消息。ByteBuf 是一个引用计数对象,这个对象必须显示地调用 release() 方法来释放。请记住处理器的职责是释放所有传递到处理器的引用计数对象。通常,channelRead() 方法的实现就像下面的这段代码:
~~~
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}
~~~
4.exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
目前为止一切都还不错,我们已经实现了 DISCARD 服务器的一半功能,剩下的需要编写一个 main() 方法来启动服务端的 DiscardServerHandler。
~~~
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;
/**
* 丢弃任何进入的数据
*/
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)
// 绑定端口,开始接收进来的连接
ChannelFuture f = b.bind(port).sync(); // (7)
// 等待服务器 socket 关闭 。
// 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
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](http://netty.io/4.0/api/io/netty/channel/nio/NioEventLoopGroup.html) 是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 [EventLoopGroup](http://netty.io/4.0/api/io/netty/channel/EventLoopGroup.html) 的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的 [Channel](http://netty.io/4.0/api/io/netty/channel/Channel.html)上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。
2.[ServerBootstrap](http://netty.io/4.0/api/io/netty/bootstrap/ServerBootstrap.html) 是一个启动 NIO 服务的辅助启动类。你可以在这个服务中直接使用 Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。
3.这里我们指定使用 [NioServerSocketChannel](http://netty.io/4.0/api/io/netty/channel/socket/nio/NioServerSocketChannel.html) 类来举例说明一个新的 Channel 如何接收进来的连接。
4.这里的事件处理类经常会被用来处理一个最近的已经接收的 Channel。[ChannelInitializer](http://netty.io/4.0/api/io/netty/channel/ChannelInitializer.html) 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel。也许你想通过增加一些处理类比如DiscardServerHandler 来配置一个新的 Channel 或者其对应的[ChannelPipeline](http://netty.io/4.0/api/io/netty/channel/ChannelPipeline.html) 来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。
5.你可以设置这里指定的 Channel 实现的配置参数。我们正在写一个TCP/IP 的服务端,因此我们被允许设置 socket 的参数选项比如tcpNoDelay 和 keepAlive。请参考 [ChannelOption](http://netty.io/4.0/api/io/netty/channel/ChannelOption.html) 和详细的 [ChannelConfig](http://netty.io/4.0/api/io/netty/channel/ChannelConfig.html) 实现的接口文档以此可以对ChannelOption 的有一个大概的认识。
6.你关注过 option() 和 childOption() 吗?option() 是提供给[NioServerSocketChannel](http://netty.io/4.0/api/io/netty/channel/socket/nio/NioServerSocketChannel.html) 用来接收进来的连接。childOption() 是提供给由父管道 [ServerChannel](http://netty.io/4.0/api/io/netty/channel/ServerChannel.html) 接收到的连接,在这个例子中也是 NioServerSocketChannel。
7.我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的 8080 端口。当然现在你可以多次调用 bind() 方法(基于不同绑定地址)。
恭喜!你已经熟练地完成了第一个基于 Netty 的服务端程序。
## 查看收到的数据
现在我们已经编写出我们第一个服务端,我们需要测试一下他是否真的可以运行。最简单的测试方法是用 telnet 命令。例如,你可以在命令行上输入`telnet localhost 8080`或者其他类型参数。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-10_55f179c3537aa.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-10_55f179c362395.jpg)
然而我们能说这个服务端是正常运行了吗?事实上我们也不知道,因为他是一个 discard 服务,你根本不可能得到任何的响应。为了证明他仍然是在正常工作的,让我们修改服务端的程序来打印出他到底接收到了什么。
我们已经知道 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 命令,你将会看到服务端打印出了他所接收到的消息。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-10_55f179c36c598.jpg)
完整的discard server代码放在了[io.netty.example.discard](http://netty.io/4.0/xref/io/netty/example/discard/package-summary.html)包下面。
*译者注:翻译版本的项目源码见 [https://github.com/waylau/netty-4-user-guide-demos](https://github.com/waylau/netty-4-user-guide-demos) 中的`com.waylau.netty.demo.discard` 包下*
## 写个应答服务器
到目前为止,我们虽然接收到了数据,但没有做任何的响应。然而一个服务端通常会对一个请求作出响应。让我们学习怎样在 [ECHO](http://tools.ietf.org/html/rfc862) 协议的实现下编写一个响应消息给客户端,这个协议针对任何接收的数据都会返回一个响应。
和 discard server 唯一不同的是把在此之前我们实现的 channelRead() 方法,返回所有的数据替代打印接收数据到控制台上的逻辑。因此,需要把 channelRead() 方法修改如下:
~~~
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
~~~
1. [ChannelHandlerContext](http://netty.io/4.0/api/io/netty/channel/ChannelHandlerContext.html) 对象提供了许多操作,使你能够触发各种各样的 I/O 事件和操作。这里我们调用了 write(Object) 方法来逐字地把接受到的消息写入。请注意不同于 DISCARD 的例子我们并没有释放接受到的消息,这是因为当写入的时候 Netty 已经帮我们释放了。
1. ctx.write(Object) 方法不会使消息写入到通道上,他被缓冲在了内部,你需要调用 ctx.flush() 方法来把缓冲区中数据强行输出。或者你可以用更简洁的 cxt.writeAndFlush(msg) 以达到同样的目的。
如果你再一次运行 telnet 命令,你会看到服务端会发回一个你已经发送的消息。
完整的echo服务的代码放在了 [io.netty.example.echo](http://netty.io/4.0/xref/io/netty/example/echo/package-summary.html)包下面。
*译者注:翻译版本的项目源码见 [https://github.com/waylau/netty-4-user-guide-demos](https://github.com/waylau/netty-4-user-guide-demos) 中的`com.waylau.netty.demo.echo` 包下*
## 写个时间服务器
在这个部分被实现的协议是 [TIME](http://tools.ietf.org/html/rfc868) 协议。和之前的例子不同的是在不接受任何请求时他会发送一个含32位的整数的消息,并且一旦消息发送就会立即关闭连接。在这个例子中,你会学习到如何构建和发送一个消息,然后在完成时关闭连接。
因为我们将会忽略任何接收到的数据,而只是在连接被创建发送一个消息,所以这次我们不能使用 channelRead() 方法了,代替他的是,我们需要覆盖 channelActive() 方法,下面的就是实现的内容:
~~~
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();
}
}
~~~
1.channelActive() 方法将会在连接被建立并且准备进行通信时被调用。因此让我们在这个方法里完成一个代表当前时间的32位整数消息的构建工作。
2.为了发送一个新的消息,我们需要分配一个包含这个消息的新的缓冲。因为我们需要写入一个32位的整数,因此我们需要一个至少有4个字节的 [ByteBuf](http://netty.io/4.0/api/io/netty/buffer/ByteBuf.html)。通过 ChannelHandlerContext.alloc() 得到一个当前的[ByteBufAllocator](http://netty.io/4.0/api/io/netty/buffer/ByteBufAllocator.html),然后分配一个新的缓冲。
3.和往常一样我们需要编写一个构建好的消息。但是等一等,flip 在哪?难道我们使用 NIO 发送消息时不是调用 java.nio.ByteBuffer.flip() 吗?ByteBuf 之所以没有这个方法因为有两个指针,一个对应读操作一个对应写操作。当你向 ByteBuf 里写入数据的时候写指针的索引就会增加,同时读指针的索引没有变化。读指针索引和写指针索引分别代表了消息的开始和结束。
比较起来,NIO 缓冲并没有提供一种简洁的方式来计算出消息内容的开始和结尾,除非你调用 flip 方法。当你忘记调用 flip 方法而引起没有数据或者错误数据被发送时,你会陷入困境。这样的一个错误不会发生在 Netty 上,因为我们对于不同的操作类型有不同的指针。你会发现这样的使用方法会让你过程变得更加的容易,因为你已经习惯一种没有使用 flip 的方式。
另外一个点需要注意的是 ChannelHandlerContext.write() (和 writeAndFlush() )方法会返回一个 [ChannelFuture](http://netty.io/4.0/api/io/netty/channel/ChannelFuture.html) 对象,一个 ChannelFuture 代表了一个还没有发生的 I/O 操作。这意味着任何一个请求操作都不会马上被执行,因为在 Netty 里所有的操作都是异步的。举个例子下面的代码中在消息被发送之前可能会先关闭连接。
~~~
Channel ch = ...;
ch.writeAndFlush(message);
ch.close();
~~~
因此你需要在 write() 方法返回的 ChannelFuture 完成后调用 close() 方法,然后当他的写操作已经完成他会通知他的监听者。请注意,close() 方法也可能不会立马关闭,他也会返回一个ChannelFuture。
4.当一个写请求已经完成是如何通知到我们?这个只需要简单地在返回的 ChannelFuture 上增加一个[ChannelFutureListener](http://netty.io/4.0/api/io/netty/channel/ChannelFutureListener.html)。这里我们构建了一个匿名的 ChannelFutureListener 类用来在操作完成时关闭 Channel。
或者,你可以使用简单的预定义监听器代码:
~~~
f.addListener(ChannelFutureListener.CLOSE);
~~~
为了测试我们的time服务如我们期望的一样工作,你可以使用 UNIX 的 rdate 命令
~~~
$ rdate -o <port> -p <host>
~~~
Port 是你在main()函数中指定的端口,host 使用 locahost 就可以了。
## 写个时间客户端
不像 DISCARD 和 ECHO 的服务端,对于 TIME 协议我们需要一个客户端,因为人们不能把一个32位的二进制数据翻译成一个日期或者日历。在这一部分,我们将会讨论如何确保服务端是正常工作的,并且学习怎样用Netty 编写一个客户端。
在 Netty 中,编写服务端和客户端最大的并且唯一不同的使用了不同的[BootStrap](http://netty.io/4.0/api/io/netty/bootstrap/Bootstrap.html) 和 [Channel](http://netty.io/4.0/api/io/netty/channel/Channel.html)的实现。请看一下下面的代码:
~~~
public class TimeClient {
public static void main(String[] args) throws Exception {
String host = args[0];
int port = Integer.parseInt(args[1]);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); // (1)
b.group(workerGroup); // (2)
b.channel(NioSocketChannel.class); // (3)
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
// 启动客户端
ChannelFuture f = b.connect(host, port).sync(); // (5)
// 等待连接关闭
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
~~~
1.BootStrap 和 [ServerBootstrap](http://netty.io/4.0/api/io/netty/bootstrap/ServerBootstrap.html) 类似,不过他是对非服务端的 channel 而言,比如客户端或者无连接传输模式的 channel。
2.如果你只指定了一个 [EventLoopGroup](http://netty.io/4.0/api/io/netty/channel/EventLoopGroup.html),那他就会即作为一个 boss group ,也会作为一个 workder group,尽管客户端不需要使用到 boss worker 。
3.代替[NioServerSocketChannel](http://netty.io/4.0/api/io/netty/channel/socket/nio/NioServerSocketChannel.html)的是[NioSocketChannel](http://netty.io/4.0/api/io/netty/channel/socket/nio/NioSocketChannel.html),这个类在客户端channel 被创建时使用。
4.不像在使用 ServerBootstrap 时需要用 childOption() 方法,因为客户端的 [SocketChannel](http://netty.io/4.0/api/io/netty/channel/socket/SocketChannel.html) 没有父亲。
5.我们用 connect() 方法代替了 bind() 方法。
正如你看到的,他和服务端的代码是不一样的。[ChannelHandler](http://netty.io/4.0/api/io/netty/channel/ChannelHandler.html) 是如何实现的?他应该从服务端接受一个32位的整数消息,把他翻译成人们能读懂的格式,并打印翻译好的时间,最后关闭连接:
~~~
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg; // (1)
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
} finally {
m.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
~~~
1.在TCP/IP中,Netty 会把读到的数据放到 ByteBuf 的数据结构中。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-10_55f179c377d55.jpg)
这样看起来非常简单,并且和服务端的那个例子的代码也相差不多。然而,处理器有时候会因为抛出 IndexOutOfBoundsException 而拒绝工作。在下个部分我们会讨论为什么会发生这种情况。
## 处理一个基于流的传输
### 关于 Socket Buffer的一个小警告
基于流的传输比如 TCP/IP, 接收到数据是存在 socket 接收的 buffer 中。不幸的是,基于流的传输并不是一个数据包队列,而是一个字节队列。意味着,即使你发送了2个独立的数据包,操作系统也不会作为2个消息处理而仅仅是作为一连串的字节而言。因此这是不能保证你远程写入的数据就会准确地读取。举个例子,让我们假设操作系统的 TCP/TP 协议栈已经接收了3个数据包:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-10_55f179c381a74.png)
由于基于流传输的协议的这种普通的性质,在你的应用程序里读取数据的时候会有很高的可能性被分成下面的片段
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-10_55f179c38c307.png)
因此,一个接收方不管他是客户端还是服务端,都应该把接收到的数据整理成一个或者多个更有意思并且能够让程序的业务逻辑更好理解的数据。在上面的例子中,接收到的数据应该被构造成下面的格式:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-10_55f179c3963b7.png)
### The First Solution 办法一
回到 TIME 客户端例子。同样也有类似的问题。一个32位整型是非常小的数据,他并不见得会被经常拆分到到不同的数据段内。然而,问题是他确实可能会被拆分到不同的数据段内,并且拆分的可能性会随着通信量的增加而增加。
最简单的方案是构造一个内部的可积累的缓冲,直到4个字节全部接收到了内部缓冲。下面的代码修改了 TimeClientHandler 的实现类修复了这个问题
~~~
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
buf = ctx.alloc().buffer(4); // (1)
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
buf.release(); // (1)
buf = null;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg;
buf.writeBytes(m); // (2)
m.release();
if (buf.readableBytes() >= 4) { // (3)
long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
~~~
1.[ChannelHandler](http://netty.io/4.0/api/io/netty/channel/ChannelHandler.html) 有2个生命周期的监听方法:handlerAdded()和 handlerRemoved()。你可以完成任意初始化任务只要他不会被阻塞很长的时间。
2.首先,所有接收的数据都应该被累积在 buf 变量里。
3.然后,处理器必须检查 buf 变量是否有足够的数据,在这个例子中是4个字节,然后处理实际的业务逻辑。否则,Netty 会重复调用channelRead() 当有更多数据到达直到4个字节的数据被积累。
### The Second Solution 方法二
尽管第一个解决方案已经解决了 TIME 客户端的问题了,但是修改后的处理器看起来不那么的简洁,想象一下如果由多个字段比如可变长度的字段组成的更为复杂的协议时,你的 [ChannelInboundHandler](http://netty.io/4.0/api/io/netty/channel/ChannelInboundHandler.html) 的实现将很快地变得难以维护。
正如你所知的,你可以增加多个 [ChannelHandler](http://netty.io/4.0/api/io/netty/channel/ChannelHandler.html) 到[ChannelPipeline](http://netty.io/4.0/api/io/netty/channel/ChannelPipeline.html) ,因此你可以把一整个ChannelHandler 拆分成多个模块以减少应用的复杂程度,比如你可以把TimeClientHandler 拆分成2个处理器:
- TimeDecoder 处理数据拆分的问题
- TimeClientHandler 原始版本的实现
幸运地是,Netty 提供了一个可扩展的类,帮你完成 TimeDecoder 的开发。
~~~
public class TimeDecoder extends ByteToMessageDecoder { // (1)
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
if (in.readableBytes() < 4) {
return; // (3)
}
out.add(in.readBytes(4)); // (4)
}
}
~~~
1.[ByteToMessageDecoder](http://netty.io/4.0/api/io/netty/handler/codec/ByteToMessageDecoder.html) 是 [ChannelInboundHandler](http://netty.io/4.0/api/io/netty/channel/ChannelInboundHandler.html) 的一个实现类,他可以在处理数据拆分的问题上变得很简单。
2.每当有新数据接收的时候,ByteToMessageDecoder 都会调用 decode() 方法来处理内部的那个累积缓冲。
3.Decode() 方法可以决定当累积缓冲里没有足够数据时可以往 out 对象里放任意数据。当有更多的数据被接收了 ByteToMessageDecoder 会再一次调用 decode() 方法。
4.如果在 decode() 方法里增加了一个对象到 out 对象里,这意味着解码器解码消息成功。ByteToMessageDecoder 将会丢弃在累积缓冲里已经被读过的数据。请记得你不需要对多条消息调用 decode(),ByteToMessageDecoder 会持续调用 decode() 直到不放任何数据到 out 里。
现在我们有另外一个处理器插入到 [ChannelPipeline](http://netty.io/4.0/api/io/netty/channel/ChannelPipeline.html) 里,我们应该在 TimeClient 里修改 ChannelInitializer 的实现:
~~~
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
}
});
~~~
如果你是一个大胆的人,你可能会尝试使用更简单的解码类[ReplayingDecoder](http://netty.io/4.0/api/io/netty/handler/codec/ReplayingDecoder.html)。不过你还是需要参考一下 API 文档来获取更多的信息。
~~~
public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(
ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(in.readBytes(4));
}
}
~~~
此外,Netty还提供了更多开箱即用的解码器使你可以更简单地实现更多的协议,帮助你避免开发一个难以维护的处理器实现。请参考下面的包以获取更多更详细的例子:
- 对于二进制协议请看 [io.netty.example.factorial](http://netty.io/4.0/xref/io/netty/example/factorial/package-summary.html)
- 对于基于文本协议请看 [io.netty.example.telnet](http://netty.io/4.0/xref/io/netty/example/telnet/package-summary.html)
*译者注:翻译版本的项目源码见 [https://github.com/waylau/netty-4-user-guide-demos](https://github.com/waylau/netty-4-user-guide-demos) 中的`com.waylau.netty.demo.factorial` 和 `com.waylau.netty.demo.telnet` 包下*
## 用POJO代替ByteBuf
我们回顾了迄今为止的所有例子使用 [ByteBuf](http://netty.io/4.0/api/io/netty/buffer/ByteBuf.html) 作为协议消息的主要数据结构。在本节中,我们将改善的 TIME 协议客户端和服务器例子,使用 POJO 代替 ByteBuf。
在 [ChannelHandler](http://netty.io/4.0/api/io/netty/channel/ChannelHandler.html) 使用 POIO 的好处很明显:通过从ChannelHandler 中提取出 ByteBuf 的代码,将会使 ChannelHandler的实现变得更加可维护和可重用。在 TIME 客户端和服务器的例子中,我们读取的仅仅是一个32位的整形数据,直接使用 ByteBuf 不会是一个主要的问题。然而,你会发现当你需要实现一个真实的协议,分离代码变得非常的必要。
首先,让我们定义一个新的类型叫做 UnixTime。
~~~
public class UnixTime {
private final long value;
public UnixTime() {
this(System.currentTimeMillis() / 1000L + 2208988800L);
}
public UnixTime(long value) {
this.value = value;
}
public long value() {
return value;
}
@Override
public String toString() {
return new Date((value() - 2208988800L) * 1000L).toString();
}
}
~~~
现在我们可以修改下 TimeDecoder 类,返回一个 UnixTime,以替代ByteBuf
~~~
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}
out.add(new UnixTime(in.readUnsignedInt()));
}
~~~
下面是修改后的解码器,TimeClientHandler 不再任何的 ByteBuf 代码了。
~~~
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
UnixTime m = (UnixTime) msg;
System.out.println(m);
ctx.close();
}
~~~
是不是变得更加简单和优雅了?相同的技术可以被运用到服务端。让我们修改一下 TimeServerHandler 的代码。
~~~
@Override
public void channelActive(ChannelHandlerContext ctx) {
ChannelFuture f = ctx.writeAndFlush(new UnixTime());
f.addListener(ChannelFutureListener.CLOSE);
}
~~~
现在,唯一缺少的功能是一个编码器,是[ChannelOutboundHandler](http://netty.io/4.0/api/io/netty/channel/ChannelOutboundHandler.html)的实现,用来将 UnixTime 对象重新转化为一个 ByteBuf。这是比编写一个解码器简单得多,因为没有需要处理的数据包编码消息时拆分和组装。
~~~
public class TimeEncoder extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
UnixTime m = (UnixTime) msg;
ByteBuf encoded = ctx.alloc().buffer(4);
encoded.writeInt((int)m.value());
ctx.write(encoded, promise); // (1)
}
}
~~~
1.在这几行代码里还有几个重要的事情。第一,通过 [ChannelPromise](http://netty.io/4.0/api/io/netty/channel/ChannelPromise.html),当编码后的数据被写到了通道上 Netty 可以通过这个对象标记是成功还是失败。第二, 我们不需要调用 cxt.flush()。因为处理器已经单独分离出了一个方法 void flush(ChannelHandlerContext cxt),如果像自己实现 flush() 方法内容可以自行覆盖这个方法。
进一步简化操作,你可以使用 [MessageToByteEncode](http://netty.io/4.0/api/io/netty/handler/codec/MessageToByteEncoder.html):
~~~
public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
@Override
protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
out.writeInt((int)msg.value());
}
}
~~~
最后的任务就是在 TimeServerHandler 之前把 TimeEncoder 插入到ChannelPipeline。 但这是不那么重要的工作。
## 关闭你的应用
关闭一个 Netty 应用往往只需要简单地通过 shutdownGracefully() 方法来关闭你构建的所有的 [EventLoopGroup](http://netty.io/4.0/api/io/netty/channel/EventLoopGroup.html)。当EventLoopGroup 被完全地终止,并且对应的所有 [channel](http://netty.io/4.0/api/io/netty/channel/Channel.html) 都已经被关闭时,Netty 会返回一个[Future](http://netty.io/4.0/api/io/netty/util/concurrent/Future.html)对象来通知你。
## 总结
在这一章节中,我们快速地回顾下如果在熟练掌握 Netty 的情况下编写出一个健壮能运行的网络应用程序。在 Netty 接下去的章节中还会有更多更相信的信息。我们也鼓励你去重新复习下在 [io.netty.example](https://github.com/netty/netty/tree/4.0/example/src/main/java/io/netty/example) 包下的例子。请注意[社区](http://netty.io/community.html)一直在等待你的问题和想法以帮助 Netty 的持续改进,Netty 的文档也是基于你们的快速反馈上。
*译者注:翻译版本的项目源码见 [https://github.com/waylau/netty-4-user-guide-demos](https://github.com/waylau/netty-4-user-guide-demos)。如对本翻译有任何建议,可以在[https://github.com/waylau/netty-4-user-guide/issues](https://github.com/waylau/netty-4-user-guide/issues)留言*