https://netty.io/wiki/user-guide-for-4.x.html
https://github.com/netty/netty/wiki/User-guide-for-4.x
问题
如今我们大概需要一些应用或者链接库来和人互相实时交流,比如:我们总是使用HTTP协议库通过Web服务器唤醒一个远程处理服务(线程)交流信息。然而,一些常见的协议或者用那些协议来实现的一些工具不太那么好用。就像为什么我们不会使用HTTP服务器去处理巨大的文件,email邮件消息和实时性要求比较高动消息比如金融信息和多人在线游戏数据。这就需要一个高度定制化的协议实现来实现特殊的目标。比如:你也许需要实现一个HTTP服务器,使用基于AJAX的聊天应用,媒体流或者大型文件的传输。你也许想设计和实现一整套的新协议用来很好的满足你自己的需求。另一个不可避免的因素,当你必须处理一个遗留的私有协议去确保和一个旧系统的协作。重点就是我们如何快速的实现一个协议不会威胁到旧应用的稳定性和变现性能
解决方案
Netty致力于提供一个异步的事件驱动的网络应用框架,快速开发,可控,高性能的服务器和客户端。Netty是一个NIO的客户端-服务端框架,能够快速并且轻松的开发网络c-s模式的应用,很好的适用于简单化和流化的TCP和UDP 套接字socket网络程序。
‘简单并且快速’不意味着这个应用会遭遇不可操作或者性能问题。Netty架构有学习很多其他协议FTP、SMTP、HTTP和很多基于二进制、文本的遗留协议的经验,设计得很小心。所以,Netty已经很成功的发现了一些方式实现了:简单开发,高性能,稳定性,灵活性而不需要妥协,666!
一些用户也许已经发现有其他网络框架声称也是同样的高级先进,也许想要问Netty跟其他有何不同,答案是,哲学自有他的意义。Netty设计用于给你最舒适的体验,兼顾API易用和可覆盖重写。这不是一些容易感受到的,但是你在选择来Netty并且读了这篇概览之后,将会意识到那种哲学让你的生活更容易了!
开始吧
这个章节导游围绕着Netty核心架构,一个简单的案例让你快速开始。你最好跟着写一个客户端和一个服务端,最好情况是,你在读完这个章节后就能够正确的使用Netty。
如果你更偏爱top-down从上到下的方式学习,你就从第二章节开始,来一个整体的概览之后再回到这里来。
开始之前
这个章节有两个最小需求;最新的Netty和最低JDK1.6或以上。最新的Netty下载。下载一个正确Netty版本和对应的JDK,可以访问你偏爱的JDK发行网站下载。
在这个章节,你也许有更多的关于class介绍的问题。当你想要了解更多class你可以访问API文档。为了方便,这个文档中所有的class名字都会链接到在线的API文档。当然,在使用开发中,请不要犹豫的联系Netty组织,提供遇到的错误信息bug,或者是一些好的建议可以优化文档。
写一个DISCARD(丢弃,无响应)服务端
世界上最简单的并不是‘Hello, World!’,笑,是DISCARD,这个DISCARD接收到数据而不会回复任何消息。
实现这个DISCARD,你只需要忽略所有收到的数据。让我们直接实现Netty生成提供的Handler,用来处理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.
* 服务端的事件消息处理器Handler
* 1.继承ChannelInboundHandlerAdapter 实现了 ChannelInboundHandler的所有方法,可以复写需要的部分方法就够了,足够使用,或者可以直接实现ChannelInboundHandler的所有方法
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
/**
* channelRead当收到scoket消息时会被调用 回调函数,这个案例中收到的消息是bytebuf形式
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
// Discard the received data silently.
/**
* 这个demo中 收到的消息都不做处理,Byteuf这个对象必须被主动释放,请注意handler的责任之一就是释放一些经过了这个handler的reference-counted(引用计数法的jre内存释放规则?)的对象
* 通常,该方法具体实现类似这样:
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
*/
((ByteBuf) msg).release(); // (3)
}
/**
* 在Netty I/O异常或者handler实现处理事件异常时被调用。大多时候,这个捕获的异常应该被记录日志,并且异常的ctx 会话连接在这里应该被关闭,尽管具体要看你业务处理的场景。比如,你也许在关闭会话连接之前想发送一个响应信息error code
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
至今我们已经实现了一半,接下来编写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 {
/**
NioEventLoopGroup是多线程的事件循环,处理I/O操作。Netty提供多种具体实现来适配不同的需求。这个案例中,我们实现服务端应用,将使用两个 NioEventLoopGroup,第一个,通常称为‘boss’,负责接收即将到来的socket连接。第二个,称为‘worker’,负责处理那些已经通过boss接入并已经注册过了的socket连接。具体需要用多少个线程,线程如何映射到已经创建的Channels依赖于 在构造的时候配置的 具体的 EventLoopGroup实现。
*/
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
/**
设置服务端的帮助类。你能够直接设置Channel。然而,请注意这是一个冗长的配置,大多时候你不需要这样子配置。
*/
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
/**
这里我们推荐使用 NioServerSocketChannel ,用于实现一个新的Channle来接收即将到来的socket连接
*/
.channel(NioServerSocketChannel.class) // (3)
/**
配置在这里的handler将总会被新接入的Channel调用。 ChannelInitializer是一个特殊的handler用于帮助用户配置新的Channel。就像你想通过添加一些Handler(DiscardServerHandler)来配置 新Channle的ChannelPipeline,用来实现你的网络应用。当这个应用完成之后,可能你还会添加更多handler到pipeline中,可能把重要的class放到最优先级。
*/
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
/**
你也能设置参数,用来定制Channel的实现。我们编写TCP/IP的服务端,所以我们可以设置socket参数,比如tcpNodelay、keepAlive,不延迟,保活。可以看文档 ChannelOption、ChannelConfig 具体支持哪些配置。
*/
.option(ChannelOption.SO_BACKLOG, 128) // (5)
/**
注意option和childOption,option是为了NioServerSocketChannel 接入socket连接。childOption是为了父Channel接入。
*/
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
// Bind and start to accept incoming connections.
/**
我们准备好了。剩下就是绑定端口启动服务端。这台机器,我们绑定8080(网络接口名片)。你可以多次调用bind方法(不同端口)。
*/
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();
}
}
查看接收数据
现在我们已经编写好了服务端,我们需要测试是否能用。最简单的方式是使用telnet命令。比如,你可以使用终端/cmd输入 telnet localhost 8080,接下来输入一些数据回车。
然而,我们能够说这个服务端正常工作吗?我们不知道,因为这是一个无响应的服务端。你需要获取一些回复。去证明服务端正常工作,让我们修改服务端,把收到的数据记录日志。
我们已经知道channelRead方法会在收到数据的时候被调用。让我们添加一点代码:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
/**
这个低效率的循环能够简单有效的替换为:
System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
*/
while (in.isReadable()) { // (1)
System.out.print((char) in.readByte());
System.out.flush();
}
} finally {
/**
修改,可以替换为
in.release();
*/
ReferenceCountUtil.release(msg); // (2)
}
}
如果你运行telnet命令,就可以看到服务端打印出收到的数据。
这个案例完整的源代码位于发行版的 io.netty.example.discard 包下面
写一个回响服务端
至今,我们消费数据而没有回复。一个服务端,往往是用来回复一个结果。让我们学习如何回写一个消息给客户端,实现一个回响ECHO功能,用来回复收到的消息。
唯一的区别DISCARD在于handler处理的时候回写数据替代打印日志到控制台。因此,只需要修改channelead方法:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
/**
ChannelHandlerContext 这个对象提供了多样的操作,可以让你触发多样的I/O事件和动作。这里,我们调用write方法,写入刚收到的消息。请注意,对比以前DISCARD,我们没有释放刚收到的消息。这是因为Netty会在你把数据写入网络设备后自动释放!
*/
ctx.write(msg); // (1)
/**
ctx.write方法不会让消息写入网络设备。它只写入交换缓冲区buffer,使用flush方法,推送并清除缓冲区。或者,你可以简易的使用
ctx.writeAndFlush(msg);
*/
ctx.flush(); // (2)
}
写一个时间服务端
这个功能是实现TIME。不同于先前的案例,这个在没有收到任何请求并且在发送一个32位整数的消息后立即关闭socket连接。在这个案例中,你可以学习如何构建和发送一条消息,如何完整的关闭socket连接。
因为我们将忽视一些收到的消息,而且在建立socket连接的时候尽快的发送消息,我们这次不使用channelRead方法!我们应该重写channelActive方法。下面是实现的样子:
package io.netty.example.time;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
/**
channelActive方法在socket连接建立并且准备产生交易(活跃)的时候被调用。写入一个代表当前时间的32位整数。
*/
@Override
public void channelActive(final ChannelHandlerContext ctx) { // (1)
/**
为了发送一个新消息,我们需要分配一个新的buffer缓冲区用来包含消息。我们将写入32位整数,因此我们需要一个至少拥有4字节的Byteuf。通过ChannelHandler.alloc()获取当前的ByteBufAllocator分配器并且分配一个新的buffer缓冲区。
*/
final ByteBuf time = ctx.alloc().buffer(4); // (2)
time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
/**
通常,我们写入结构化的消息!
等等,没有flip?难道我们不应该在NIO发送完消息之前调用Byteuffer.flip()?ByteBuf没有这样一个方法,因为它有两个指针游标;一个用来读操作,另一个用来写操作。写入游标在你写入一些数据的时候增长,同时读游标不会改变。读游标和写游标代表了这个消息的开始和结束位置。
对比,NIO buffer缓冲区没有提供flip方法,所以没有一个清理的方式来证实这个消息内容的开始和结束。你将在你忘记调用flip来清理 buffer缓冲区而没有发送任何消息或者发送正确数据时遇到麻烦(就是说不用担心使用其他buffer时忘记flip而导致的一些列麻烦)。这样的错误不会在使用Netty时发生,因为我们针对不同的操作类型拥有不同的指针。你将发现当你习惯用这种东西会让你的生活更容易!-- 一个不会flipping out(越界)的生活,666!
另外一个指针需要注意的是,ctx.write(ctx.writeAndFlush)方法返回了一个ChannelFuture(当执行完毕的期望),这个代表还没发生的I/O操作的结果。这意味着一些请求操作可能还没处理完毕,因为所有的Netty操作都是异步的。例如下面的代码操作可能在发送消息成功之前就关闭socket连接而异常:
Channel ch = ...;
ch.writeAndFlush(message);
ch.close();
因此,你需要在ChannelFuture完成的时候调用close方法,这将在write方法线程操作完毕之后返回,并且唤醒调用监听器。请注意close方法也不会立即关闭socket连接,同样返回ChannelFuture。
*/
final ChannelFuture f = ctx.writeAndFlush(time); // (3)
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
}); // (4)
/**
当请求处理完成后如何通知到我们?这有一个简单的方式,添加监听器,我们创建了一个新的匿名内部类ChannelFutureListener用来关闭Channel,当操作完成。
可替换的,你也可以简化使用一个已经定义好的监听器:
f.addListener(ChannlFutureListener.CLOSE);
*/
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
测试是否我们的TIME服务端是否预期的工作,你能使用UNIX命令
rdate -o <8081> -p <127.0.0.1>
port替换为你启动的main函数里面的端口 服务端的ip
写一个TIME客户端
不像DISCARD和ECHO服务端,我们需要一个客户端适配TIME服务端,因为人类不能转换32位二进制数据为一个日期数据。这个模块,我们讨论如何确保服务端工作正常,并且学习如何编写Netty客户端。
和服务端最大并且不同的是使用的Bootstrap 和 Channel实现。看一下下面的代码:
package io.netty.example.time;
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和服务端的ServerBootstrap差异在于它是个无服务端的channel。
*/
Bootstrap b = new Bootstrap(); // (1)
/**
如果你想定制EentLoopGroup,将用于boss和worker组。客户端不需要用boss组
*/
b.group(workerGroup); // (2)
/**
客户端使用NioSocketChannle而服务端使用NioServerSocketChannel
*/
b.channel(NioSocketChannel.class); // (3)
/**
注意!对比服务端我们没有使用childOpthion,因为客户端的SocketChannel没有父Channel
*/
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());
}
});
/**
我们调用connect而不是bind(服务端)方法
*/
// Start the client.
ChannelFuture f = b.connect(host, port).sync(); // (5)
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
你能够看到,这和服务端也不是完全不同。如何实现ChannelHandler?它应该从服务端接收一个32位整数,转换为一个人类可读的格式,输出转换的结果,关闭socket连接:
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
/**
在TCP/IP,Netty读取发送过来的ByteBuf数据
*/
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();
}
}
这看起来很简单,也和服务端的没有什么不同。然而,这个handler有时候也会发生IndexOutOfBoundsException异常而拒绝工作。我们下节讨论一下这个问题为什么会发生。
处理基于流的传输
一个SocketBuffer的小告诫
在一个基于流的传输,比如TCP/IP,接收到的数据存储在socket连接的接收缓冲区。不幸的是,这个基于流的传输的buffer缓冲区不是一个数据包的队列,而是数据byte的队列。这意味着,尽管你发送来两条消息作为两个无关的数据包packet,操作系统却不会把这看作两消息而一个消息的两批数据(网络设备 分包 1501byte mtu)。因此,没有保证你一次读取的数据就是你服务端一次写入的数据。例如,让我们假设一个操作系统的TCP/IP栈已经收到了三个数据包packet:
ABC DEF GHI
因为这个基于流的协议的特性,高低位读取,分包发送(mtu)等问题,你的应用读取到的消息包可能如下:
AB CDEFG H I
因此,一个接收部分,不管是服务端还是客户端,应该把接收到的碎片化的数据包,整理为一个或者多个的有意义的能够被应用逻辑理解和处理的数据包。上述案例,接收到的数据应该被格式化解析为如下:
ABC DEF GHI
第一个解决方案
现在回到TIME的客户端案例。我们有同样的问题。一个32位整数是很小量的数据,也不像常见的会被碎片化。然而,问题是:这个可以碎片化,碎片化的可能性会随着交易增长而增长。
简单的解决方案是(为socket连接)创建一个的累积的buffer缓冲区,等待直到所有的4字节(32位)的数据被收到并放入buffer缓冲区。下面的是修改过后的 TimeClientHandler实现,用来解决这个问题:
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
/**
ChannelHandler有两个循环监听方法:handlerAdded和handlerRemoved。你可以强硬的初始化一个任务,尽可能长而不太长的阻塞太久(不会阻塞太久?)。
*/
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
*/
buf.writeBytes(m); // (2)
m.release();
/**
接着,这个handler处理器必须检查缓冲区buf是否已经有了足够的数据,这个案例中4个字节,处理具体的业务逻辑。否则,Netty将在更多消息到来时一次次的再调用channelRead方法,4个字节总会被累积到足够。
*/
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();
}
}
第二个解决方案
尽管第一个方案已经解决了TIME客户端的问题,这个修改handler看起来不怎么干净简洁。想象一个更复杂的功能,用来组建多域的数据报文,比如多样的变长的消息。你的ChannelInboundHandler实现将难以很快的维持。
你也许已经发现,你能够添加配置不止一个 ChannelHandler到ChannelPipeline中,因此,你能分离一个庞大的 ChannelHandler为多个模块,去减少你得应用的复杂度。比如,你能分离TimeClientHandler为两个handler:
TimeDecoder用来处理碎片化问题
TimeClientHandler照旧的简单版本
幸运的是,Netty提供一个可扩展的class用来帮助你写入第一个脱离盒子(报文编码):
package io.netty.example.time;
/**
ByteToMessageDecoder 实现了ChannelInboundHandler,用来更容易的处理碎片化问题。
*/
public class TimeDecoder extends ByteToMessageDecoder { // (1)
/**
ByteToMessageDecoder 在收到新数据的时候用累积的维持的内部buffer缓冲的数据来调用decode方法(解码)
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
if (in.readableBytes() < 4) {
/**
decode方法能决定在累积buffer缓冲区没有足够数据时什么都不写入out输出。 ByteToMessageDecoder会在收到更多的数据时再次调用decode方法。
*/
return; // (3)
}
/**
如果decode方法添加了一个对象到out输出,这意味着decoder成功的解码出来一个消息。 ByteToMessageDecoder将会忽略掉累积buffer缓冲区中已经读出出来的部分。请记住你不需要去解码多样的消息。 ByteToMessageDecoder将保持调用decode方法直到累积buffer缓冲区没有新来的数据且没有新的可读取的数据。
*/
out.add(in.readBytes(4)); // (4)
}
}
现在,我们有另一个handler需要插入ChannelPipeline,我们应该修改 TimeClient客户端的ChannelInitializer的实现:
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
}
});
如果你热爱冒险,你也许想尝试使用 ReplayingDecoder 解码器,可以简化decoder解码器。你将需要查阅API文档拿到更多信息。
public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode( ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(in.readBytes(4));
}
}
额外的,Netty提供了out-of-the-box解码器用来让你很容易的实现绝大部分功能,帮助你避免开发出整体的不可维护的handler实现。可以看这些package包了解更多细节:
io.netty.example.factorial 二进制协议
io.netty.example.telnet 基于文本的协议
使用POJO简单基本类型而不是ByteBuf
至今为止,所有的案例我们已经预览过的都是使用ByteBuf作为消息的基本的数据结构。这个小节,我们将使用POJO类替换ByteBuf来提升TIME客户端和服务端案例。
在ChannelHandler中使用POJO的优点是很明显的;你的handler变得更可维持的和可复用的,依靠从 ByteBuf分离额外信息的代码。在TIME客户端和服务端案例中,我们只读取一个32为整数,这不是一个去使用ByteBuf主要的直接的问题。然而,你将发现你的全功能协议分离实现是必要的。
首先,我们定义一个新的类型UnixTime。
package io.netty.example.time;
import java.util.Date;
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();
}
}
我们能翻转Timeecoder去生产一个UnixTime而不是ByteBuf。
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}
out.add(new UnixTime(in.readUnsignedInt()));
}
更新之后的decoder, 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);
}
现在,唯一缺少的一片是encoder编码器,是ChannelOutboundHandler的实现,用来转换UnixTime编码为ByteBuf。这比编写一个decoder解码器更简单,因为编码消息的时候不需要处理packet包碎片,和装配。
package io.netty.example.time;
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());
/**
这一行有一些重要的事。
首先,我们跳过了起源的的ChannelPromise--用于Netty标记编码数据实际上是否写入网络设备成功。
第二,我们没有调用ctx.flush()。这是分离的handler方法flush,支持重写flush此操作。
*/
ctx.write(encoded, promise); // (1)
}
}
为了更多的简化事件,使用MessageToByteEncoder:
public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
@Override
protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
out.writeInt((int)msg.value());
}
}
最后一个任务是插入一个TimeEncoder到服务端的ChannelPipeline,要在TimeServerHandler之前,剩下的就是微不足道的练习。
关闭你的应用
关闭Netty应用通常只是简单的关闭你创建的所有的EventLoopGroups组,通过shutdownGracefully()。这返回一个Future,用来当完整的关闭的时候通知你,同时所有的归属于该组的Channel也都会被关闭。
总结
这个章节,我们快速的预览了Netty,一个如何使用Netty编写完整工作可用的网络应用的示范。
在下面的章节中有更多的关于Netty的消息。我们也鼓励你去看一下io.netty.example package包中的Netty案例。
也请关注Netty组织总是在等待你的问题和建议idea并帮助你,同时基于你的反馈来保持完善Netty和文档。
检索于2019-03-22