步骤一:打开SocketChannel,绑定客户端本地地址(可选,默认系统会随机分配一个可用的本地地址),示例代码如下:
1
SocketChannel clientChannel = SocketChannel.open();
步骤二:设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数,示例代码如下:
1
clientChannel.configureBlocking(
false
);
2
socket.setReuseAddress(
true
);
3
socket.setReceiveBufferSize(BUFFER_SIZE);
4
socket.setSendBufferSize(BUFFER_SIZE);
步骤三:异步连接服务端,示例代码如下:
1
boolean
connected = clientChannel.connect(
new
InetSocketAddress(“ip”,port));
步骤四:判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中,如果当前没有连接成功(异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立),示例代码如下:
3
clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);
7
clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);
步骤五:向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答,示例代码如下:
1
clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);
步骤六:创建Reactor线程,创建多路复用器并启动线程,代码如下:
1
Selector selector = Selector.open();
2
New Thread(
new
ReactorTask()).start();
步骤七:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key,代码如下:
1
int
num = selector.select();
2
Set selectedKeys = selector.selectedKeys();
3
Iterator it = selectedKeys.iterator();
5
if
(key.isConnectable())
步骤九:判断连接结果,如果连接成功,注册读事件到多路复用器,示例代码如下:
1
if
(channel.finishConnect())
步骤十:注册读事件到多路复用器:
1
clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);
步骤十一:异步读客户端请求消息到缓冲区,示例代码如下:
1
int
readNumber = channel.read(receivedBuffer);
步骤十二:对ByteBuffer进行编解码,如果有半包消息接收缓冲区Reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排,示例代码如下:
01
Object message =
null
;
02
while
(buffer.hasRemain())
05
Object message = decode(byteBuffer);
11
messageList.add(message );
13
if
(!byteBuffer.hasRemain())
17
if
(messageList !=
null
& !messageList.isEmpty())
19
for
(Object messageE : messageList)
20
handlerTask(messageE);
步骤十三:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端,示例代码如下:
1
socketChannel.write(buffer);
通过序列图和关键代码的解说,相信大家对创建NIO客户端程序有了一个初步的了解,下面,就跟随着我们的脚步继续看看如果使用NIO改造之前的时间服务器客户端TimeClient。
服务端序列图
步骤一 :打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道,代码示例如下:
1
ServerSocketChannel acceptorSvr = ServerSocketChannel.open();
步骤二 :绑定监听端口,设置连接为非阻塞模式,示例代码如下:
1
acceptorSvr.socket().bind(
new
InetSocketAddress(InetAddress.getByName(“IP”), port));
2
acceptorSvr.configureBlocking(
false
);
步骤三 :创建Reactor线程,创建多路复用器并启动线程,代码如下:
1
Selector selector = Selector.open();
2
New Thread(
new
ReactorTask()).start();
步骤四 :将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件,代码如下:
1
SelectionKey key = acceptorSvr.register( selector, SelectionKey.OP_ACCEPT, ioHandler);
步骤五 :多路复用器在线程run方法的无限循环体内轮询准备就绪的Key,代码如下:
1
int
num = selector.select();
2
Set selectedKeys = selector.selectedKeys();
3
Iterator it = selectedKeys.iterator();
5
SelectionKey key = (SelectionKey)it.next();
步骤六 :多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路,代码示例如下:
1
SocketChannel channel = svrChannel.accept();
步骤七 :设置客户端链路为非阻塞模式,示例代码如下:
1
channel.configureBlocking(
false
);
2
channel.socket().setReuseAddress(
true
);
步骤八 :将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,用来读取客户端发送的网络消息,代码如下:
1
SelectionKey key = socketChannel.register( selector, SelectionKey.OP_READ, ioHandler);
步骤九 :异步读取客户端请求消息到缓冲区,示例代码如下:
1
int
readNumber = channel.read(receivedBuffer);
步骤十 :对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排,示例代码如下:
01
Object message =
null
;
02
while
(buffer.hasRemain())
05
Object message = decode(byteBuffer);
11
messageList.add(message );
13
if
(!byteBuffer.hasRemain())
17
if
(messageList !=
null
& !messageList.isEmpty())
19
for
(Object messageE : messageList)
20
handlerTask(messageE);
步骤十一:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端,示例代码如下:
1
socketChannel.write(buffer);
注意:如果发送区TCP缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入TCP缓冲区,此处不赘述,后续Netty源码分析章节会详细分析Netty的处理策略。
当我们了解创建NIO服务端的基本步骤之后,下面我们将前面的时间服务器程序通过NIO重写一遍,让大家能够学习到完整版的NIO服务端创建。
NIO服务端通信序列图
为什么选择Netty
Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架。很多其它业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。
通过对Netty的分析,我们将它的优点总结如下:
1) API使用简单,开发门槛低;
2) 功能强大,预置了多种编解码功能,支持多种主流协议;
3) 定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展;
4) 性能高,通过与其它业界主流的NIO框架对比,Netty的综合性能最优;
5) 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
6) 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入;
7) 经历了大规模的商业应用考验,质量已经得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它可以完全满足不同行业的商业应用。
正是因为这些优点,Netty逐渐成为Java NIO编程的首选框架。
Netty的入门指导
前言
问题
现如今我们使用通用的应用程序或者类库来实现系统之间地互相访问,比如我们经常使用一个HTTP客户端来从web服务器上获取信息,或者通过web service来执行一个远程的调用。
然而,有时候一个通用的协议和他的实现并没有覆盖一些场景。比如我们无法使用一个通用的HTTP服务器来处理大文件、电子邮件、近实时消息比如财务信息和多人游戏数据。我们需要一个合适的协议来处理一些特殊的场景。例如你可以实现一个优化的Ajax的聊天应用、媒体流传输或者是大文件传输的HTTP服务器,你甚至可以自己设计和实现一个新的协议来准确地实现你的需求。
另外不可避免的事情是你不得不处理这些私有协议来确保和原有系统的互通。这个例子将会展示如何快速实现一个不影响应用程序稳定性和性能的协议。
解决方案
Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
换句话说,Netty是一个NIO框架,使用它可以简单快速地开发网络应用程序,比如客户端和服务端的协议。Netty大大简化了网络程序的开发过程比如TCP和UDP的 Socket的开发。
“快速和简单”并不意味着应用程序会有难维护和性能低的问题,Netty是一个精心设计的框架,它从许多协议的实现中吸收了很多的经验比如FTP、SMTP、HTTP、许多二进制和基于文本的传统协议,Netty在不降低开发效率、性能、稳定性、灵活性情况下,成功地找到了解决方案。
有一些用户可能已经发现其他的一些网络框架也声称自己有同样的优势,所以你可能会问是Netty和它们的不同之处。答案就是Netty的哲学设计理念。Netty从第一天开始就为用户提供了用户体验最好的API以及实现设计。正是因为Netty的设计理念,才让我们得以轻松地阅读本指南并使用Netty。
入门指南
这个章节会介绍Netty核心的结构,并通过一些简单的例子来帮助你快速入门。当你读完本章节你马上就可以用Netty写出一个客户端和服务端。
如果你在学习的时候喜欢“自顶向下(top-down)”的方法,那你可能需要要从第二章《架构概述》开始,然后再回到这里。
开始之前
运行本章节中的两个例子最低要求是:Netty的最新版本(Netty5)和JDK1.6及以上。最新的Netty版本在项目下载页面可以找到。为了下载到正确的JDK版本,请到你喜欢的网站下载。
阅读本章节过程中,你可能会对相关类有疑惑,关于这些类的详细的信息请请参考API说明文档。为了方便,所有文档中涉及到的类名字都会被关联到一个在线的API说明。当然如果有任何错误信息、语法错误或者你有任何好的建议来改进文档说明,那么请联系Netty社区。
DISCARD服务(丢弃服务,指的是会忽略所有接收的数据的一种协议)
世界上最简单的协议不是”Hello,World!”,是DISCARD,他是一种丢弃了所有接受到的数据,并不做有任何的响应的协议。
为了实现DISCARD协议,你唯一需要做的就是忽略所有收到的数据。让我们从处理器的实现开始,处理器是由Netty生成用来处理I/O事件的。
01
package
io.netty.example.discard;
03
import
io.netty.buffer.ByteBuf;
05
import
io.netty.channel.ChannelHandlerContext;
06
import
io.netty.channel.ChannelHandlerAdapter;
09
* Handles a server-side channel.
11
public
class
DiscardServerHandler
extends
ChannelHandlerAdapter {
14
public
void
channelRead(ChannelHandlerContext ctx, Object msg) {
16
((ByteBuf) msg).release();
20
public
void
exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
22
cause.printStackTrace();
DisCardServerHandler 继承自 ChannelHandlerAdapter,这个类实现了ChannelHandler接口,ChannelHandler提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承ChannelHandlerAdapter类而不是你自己去实现接口方法。
这里我们覆盖了chanelRead()事件处理方法。每当从客户端收到新的数据时,这个方法会在收到消息时被调用,这个例子中,收到的消息的类型是ByteBuf
为了实现DISCARD协议,处理器不得不忽略所有接受到的消息。ByteBuf是一个引用计数对象,这个对象必须显示地调用release()方法来释放。请记住处理器的职责是释放所有传递到处理器的引用计数对象。通常,channelRead()方法的实现就像下面的这段代码:
2
public
void
channelRead(ChannelHandlerContext ctx, Object msg) {
6
ReferenceCountUtil.release(msg);
exceptionCaught()事件处理方法是当出现Throwable对象才会被调用,即当Netty由于IO错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来并且把关联的channel给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
到目前为止一切都还比较顺利,我们已经实现了DISCARD服务的一半功能,剩下的需要编写一个main()方法来启动服务端的DiscardServerHandler。
01
package
io.netty.example.discard;
03
import
io.netty.bootstrap.ServerBootstrap;
05
import
io.netty.channel.ChannelFuture;
06
import
io.netty.channel.ChannelInitializer;
07
import
io.netty.channel.ChannelOption;
08
import
io.netty.channel.EventLoopGroup;
09
import
io.netty.channel.nio.NioEventLoopGroup;
10
import
io.netty.channel.socket.SocketChannel;
11
import
io.netty.channel.socket.nio.NioServerSocketChannel;
14
* Discards any incoming data.
16
public
class
DiscardServer {
20
public
DiscardServer(
int
port) {
24
public
void
run()
throws
Exception {
25
EventLoopGroup bossGroup =
new
NioEventLoopGroup();
26
EventLoopGroup workerGroup =
new
NioEventLoopGroup();
28
ServerBootstrap b =
new
ServerBootstrap();
29
b.group(bossGroup, workerGroup)
30
.channel(NioServerSocketChannel.
class
)
31
.childHandler(
new
ChannelInitializer<SocketChannel>() {
33
public
void
initChannel(SocketChannel ch)
throws
Exception {
34
ch.pipeline().addLast(
new
DiscardServerHandler());
37
.option(ChannelOption.SO_BACKLOG,
128
)
38
.childOption(ChannelOption.SO_KEEPALIVE,
true
);
41
ChannelFuture f = b.bind(port).sync();
46
f.channel().closeFuture().sync();
48
workerGroup.shutdownGracefully();
49
bossGroup.shutdownGracefully();
53
public
static
void
main(String[] args)
throws
Exception {
55
if
(args.length >
0
) {
56
port = Integer.parseInt(args[
0
]);
60
new
DiscardServer(port).run();
NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。在这个例子中我们实现了一个服务端的应用,因此会有2个NioEventLoopGroup会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的Channels上都需要依赖于EventLoopGroup的实现,并且可以通过构造函数来配置他们的关系。
ServerBootstrap 是一个启动NIO服务的辅助启动类。你可以在这个服务中直接使用Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。
这里我们指定使用NioServerSocketChannel类来举例说明一个新的Channel如何接收进来的连接。
这里的事件处理类经常会被用来处理一个最近的已经接收的Channel。ChannelInitializer是一个特殊的处理类,他的目的是帮助使用者配置一个新的Channel。也许你想通过增加一些处理类比如DiscardServerHandle来配置一个新的Channel或者其对应的ChannelPipeline来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到pipline上,然后提取这些匿名类到最顶层的类上。
你可以设置这里指定的通道实现的配置参数。我们正在写一个TCP/IP的服务端,因此我们被允许设置socket的参数选项比如tcpNoDelay和keepAlive。请参考ChannelOption和详细的ChannelConfig实现的接口文档以此可以对ChannelOptions的有一个大概的认识。
你关注过option()和childOption()吗?option()是提供给NioServerSocketChannel用来接收进来的连接。childOption()是提供给由父管道ServerChannel接收到的连接,在这个例子中也是NioServerSocketChannel。
我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的8080端口。当然现在你可以多次调用bind()方法(基于不同绑定地址)。
恭喜!你已经完成熟练地完成了第一个基于Netty的服务端程序。
观察接收到的数据
现在我们已经编写出我们第一个服务端,我们需要测试一下他是否真的可以运行。最简单的测试方法是用telnet 命令。例如,你可以在命令行上输入telnet localhost 8080或者其他类型参数。
然而我们能说这个服务端是正常运行了吗?事实上我们也不知道因为他是一个discard服务,你根本不可能得到任何的响应。为了证明他仍然是在工作的,让我们修改服务端的程序来打印出他到底接收到了什么。
我们已经知道channelRead()方法是在数据被接收的时候调用。让我们放一些代码到DiscardServerHandler类的channelRead()方法。
02
public
void
channelRead(ChannelHandlerContext ctx, Object msg) {
03
ByteBuf in = (ByteBuf) msg;
05
while
(in.isReadable()) {
06
System.out.print((
char
) in.readByte());
10
ReferenceCountUtil.release(msg);
这个低效的循环事实上可以简化为:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
或者,你可以在这里调用in.release()。
如果你再次运行telnet命令,你将会看到服务端打印出了他所接收到的消息。
完整的discard server代码放在了io.netty.example.discard包下面。
ECHO服务(响应式协议)
到目前为止,我们虽然接收到了数据,但没有做任何的响应。然而一个服务端通常会对一个请求作出响应。让我们学习怎样在ECHO协议的实现下编写一个响应消息给客户端,这个协议针对任何接收的数据都会返回一个响应。
和discard server唯一不同的是把在此之前我们实现的channelRead()方法,返回所有的数据替代打印接收数据到控制台上的逻辑。因此,需要把channelRead()方法修改如下:
2
public
void
channelRead(ChannelHandlerContext ctx, Object msg) {
1. ChannelHandlerContext对象提供了许多操作,使你能够触发各种各样的I/O事件和操作。这里我们调用了write(Object)方法来逐字地把接受到的消息写入。请注意不同于DISCARD的例子我们并没有释放接受到的消息,这是因为当写入的时候Netty已经帮我们释放了。
2. ctx.write(Object)方法不会使消息写入到通道上,他被缓冲在了内部,你需要调用ctx.flush()方法来把缓冲区中数据强行输出。或者你可以用更简洁的cxt.writeAndFlush(msg)以达到同样的目的。
如果你再一次运行telnet命令,你会看到服务端会发回一个你已经发送的消息。
完整的echo服务的代码放在了io.netty.example.echo包下面。
TIME服务(时间协议的服务)
在这个部分被实现的协议是TIME协议。和之前的例子不同的是在不接受任何请求时他会发送一个含32位的整数的消息,并且一旦消息发送就会立即关闭连接。在这个例子中,你会学习到如何构建和发送一个消息,然后在完成时主动关闭连接。
因为我们将会忽略任何接收到的数据,而只是在连接被创建发送一个消息,所以这次我们不能使用channelRead()方法了,代替他的是,我们需要覆盖channelActive()方法,下面的就是实现的内容:
01
package
io.netty.example.time;
03
public
class
TimeServerHandler
extends
ChannelHandlerAdapter {
06
public
void
channelActive(
final
ChannelHandlerContext ctx) {
07
final
ByteBuf time = ctx.alloc().buffer(
4
);
08
time.writeInt((
int
) (System.currentTimeMillis() / 1000L + 2208988800L));
10
final
ChannelFuture f = ctx.writeAndFlush(time);
11
f.addListener(
new
ChannelFutureListener() {
13
public
void
operationComplete(ChannelFuture future) {
21
public
void
exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
22
cause.printStackTrace();
channelActive()方法将会在连接被建立并且准备进行通信时被调用。因此让我们在这个方法里完成一个代表当前时间的32位整数消息的构建工作。
为了发送一个新的消息,我们需要分配一个包含这个消息的新的缓冲。因为我们需要写入一个32位的整数,因此我们需要一个至少有4个字节的ByteBuf。通过ChannelHandlerContext.alloc()得到一个当前的ByteBufAllocator,然后分配一个新的缓冲。
和往常一样我们需要编写一个构建好的消息。但是等一等,flip在哪?难道我们使用NIO发送消息时不是调用java.nio.ByteBuffer.flip()吗?ByteBuf之所以没有这个方法因为有两个指针,一个对应读操作一个对应写操作。当你向ByteBuf里写入数据的时候写指针的索引就会增加,同时读指针的索引没有变化。读指针索引和写指针索引分别代表了消息的开始和结束。比较起来,NIO缓冲并没有提供一种简洁的方式来计算出消息内容的开始和结尾,除非你调用flip方法。当你忘记调用flip方法而引起没有数据或者错误数据被发送时,你会陷入困境。这样的一个错误不会发生在Netty上,因为我们对于不同的操作类型有不同的指针。你会发现这样的使用方法会让你过程变得更加的容易,因为你已经习惯一种没有使用flip的方式。另外一个点需要注意的是ChannelHandlerContext.write()(和writeAndFlush())方法会返回一个ChannelFuture对象,一个ChannelFuture代表了一个还没有发生的I/O操作。这意味着任何一个请求操作都不会马上被执行,因为在Netty里所有的操作都是异步的。举个例子下面的代码中在消息被发送之前可能会先关闭连接。
2
ch.writeAndFlush(message);
因此你需要在write()方法返回的ChannelFuture完成后调用close()方法,然后当他的写操作已经完成他会通知他的监听者。请注意,close()方法也可能不会立马关闭,他也会返回一个ChannelFuture。
当一个写请求已经完成是如何通知到我们?这个只需要简单地在返回的ChannelFuture上增加一个ChannelFutureListener。这里我们构建了一个匿名的ChannelFutureListener类用来在操作完成时关闭Channel。或者,你可以使用简单的预定义监听器代码:
1
f.addListener(ChannelFutureListener.CLOSE);
为了测试我们的time服务如我们期望的一样工作,你可以使用UNIX的rdate命令
Port是你在main()函数中指定的端口,host使用locahost就可以了。
Time客户端
不像DISCARD和ECHO的服务端,对于TIME协议我们需要一个客户端因为人们不能把一个32位的二进制数据翻译成一个日期或者日历。在这一部分,我们将会讨论如何确保服务端是正常工作的,并且学习怎样用Netty编写一个客户端。
在Netty中,编写服务端和客户端最大的并且唯一不同的使用了不同的BootStrap和Channel的实现。请看一下下面的代码:
01
package
io.netty.example.time;
03
public
class
TimeClient {
04
public
static
void
main(String[] args)
throws
Exception {
05
String host = args[
0
];
06
int
port = Integer.parseInt(args[
1
]);
07
EventLoopGroup workerGroup =
new
NioEventLoopGroup();
10
Bootstrap b =
new
Bootstrap();
12
b.channel(NioSocketChannel.
class
);
13
b.option(ChannelOption.SO_KEEPALIVE,
true
);
14
b.handler(
new
ChannelInitializer<SocketChannel>() {
16
public
void
initChannel(SocketChannel ch)
throws
Exception {
17
ch.pipeline().addLast(
new
TimeClientHandler());
22
ChannelFuture f = b.connect(host, port).sync();
25
f.channel().closeFuture().sync();
27
workerGroup.shutdownGracefully();
BootStrap和ServerBootstrap类似,不过他是对非服务端的channel而言,比如客户端或者无连接传输模式的channel。
如果你只指定了一个EventLoopGroup,那他就会即作为一个‘boss’线程,也会作为一个‘workder’线程,尽管客户端不需要使用到‘boss’线程。
代替NioServerSocketChannel的是NioSocketChannel,这个类在客户端channel被创建时使用。
不像在使用ServerBootstrap时需要用childOption()方法,因为客户端的SocketChannel没有父channel的概念。
我们用connect()方法代替了bind()方法。
正如你看到的,他和服务端的代码是不一样的。ChannelHandler是如何实现的?他应该从服务端接受一个32位的整数消息,把他翻译成人们能读懂的格式,并打印翻译好的时间,最后关闭连接:
01
package
io.netty.example.time;
03
import
java.util.Date;
05
public
class
TimeClientHandler
extends
ChannelHandlerAdapter {
07
public
void
channelRead(ChannelHandlerContext ctx, Object msg) {
08
ByteBuf m = (ByteBuf) msg;
10
long
currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
11
System.out.println(
new
Date(currentTimeMillis));
19
public
void
exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
20
cause.printStackTrace();
在TCP/IP中,NETTY会把读到的数据放到ByteBuf的数据结构中。
这样看起来非常简单,并且和服务端的那个例子的代码也相差不多。然而,处理器有时候会因为抛出IndexOutOfBoundsException而拒绝工作。在下个部分我们会讨论为什么会发生这种情况。
流数据的传输处理
一个小的Socket Buffer问题
在基于流的传输里比如TCP/IP,接收到的数据会先被存储到一个socket接收缓冲里。不幸的是,基于流的传输并不是一个数据包队列,而是一个字节队列。即使你发送了2个独立的数据包,操作系统也不会作为2个消息处理而仅仅是作为一连串的字节而言。因此这是不能保证你远程写入的数据就会准确地读取。举个例子,让我们假设操作系统的TCP/TP协议栈已经接收了3个数据包:
由于基于流传输的协议的这种普通的性质,在你的应用程序里读取数据的时候会有很高的可能性被分成下面的片段。
因此,一个接收方不管他是客户端还是服务端,都应该把接收到的数据整理成一个或者多个更有意思并且能够让程序的业务逻辑更好理解的数据。在上面的例子中,接收到的数据应该被构造成下面的格式:
第一个解决方案
现在让我们回到TIME客户端的例子上。这里我们遇到了同样的问题,一个32字节数据是非常小的数据量,他并不见得会被经常拆分到到不同的数据段内。然而,问题是他确实可能会被拆分到不同的数据段内,并且拆分的可能性会随着通信量的增加而增加。
最简单的方案是构造一个内部的可积累的缓冲,直到4个字节全部接收到了内部缓冲。下面的代码修改了TimeClientHandler的实现类修复了这个问题
01
package
io.netty.example.time;
03
import
java.util.Date;
05
public
class
TimeClientHandler
extends
ChannelHandlerAdapter {
09
public
void
handlerAdded(ChannelHandlerContext ctx) {
10
buf = ctx.alloc().buffer(
4
);
14
public
void
handlerRemoved(ChannelHandlerContext ctx) {
20
public
void
channelRead(ChannelHandlerContext ctx, Object msg) {
21
ByteBuf m = (ByteBuf) msg;
25
if
(buf.readableBytes() >=
4
) {
26
long
currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L;
27
System.out.println(
new
Date(currentTimeMillis));
33
public
void
exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
34
cause.printStackTrace();
ChannelHandler有2个生命周期的监听方法:handlerAdded()和handlerRemoved()。你可以完成任意初始化任务只要他不会被阻塞很长的时间。
首先,所有接收的数据都应该被累积在buf变量里。
然后,处理器必须检查buf变量是否有足够的数据,在这个例子中是4个字节,然后处理实际的业务逻辑。否则,Netty会重复调用channelRead()当有更多数据到达直到4个字节的数据被积累。
第二个解决方案
尽管第一个解决方案已经解决了Time客户端的问题了,但是修改后的处理器看起来不那么的简洁,想象一下如果由多个字段比如可变长度的字段组成的更为复杂的协议时,你的ChannelHandler的实现将很快地变得难以维护。
正如你所知的,你可以增加多个ChannelHandler到ChannelPipeline ,因此你可以把一整个ChannelHandler拆分成多个模块以减少应用的复杂程度,比如你可以把TimeClientHandler拆分成2个处理器:
TimeDecoder处理数据拆分的问题
TimeClientHandler原始版本的实现
幸运地是,Netty提供了一个可扩展的类,帮你完成TimeDecoder的开发。
01
package
io.netty.example.time;
03
public
class
TimeDecoder
extends
ByteToMessageDecoder {
05
protected
void
decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
06
if
(in.readableBytes() <
4
) {
10
out.add(in.readBytes(
4
));
ByteToMessageDecoder是ChannelHandler的一个实现类,他可以在处理数据拆分的问题上变得很简单。
每当有新数据接收的时候,ByteToMessageDecoder都会调用decode()方法来处理内部的那个累积缓冲。
Decode()方法可以决定当累积缓冲里没有足够数据时可以往out对象里放任意数据。当有更多的数据被接收了ByteToMessageDecoder会再一次调用decode()方法。
如果在decode()方法里增加了一个对象到out对象里,这意味着解码器解码消息成功。ByteToMessageDecoder将会丢弃在累积缓冲里已经被读过的数据。请记得你不需要对多条消息调用decode(),ByteToMessageDecoder会持续调用decode()直到不放任何数据到out里。
现在我们有另外一个处理器插入到ChannelPipeline里,我们应该在TimeClient里修改ChannelInitializer 的实现:
1
b.handler(
new
ChannelInitializer<SocketChannel>() {
3
public
void
initChannel(SocketChannel ch)
throws
Exception {
4
ch.pipeline().addLast(
new
TimeDecoder(),
new
TimeClientHandler());
如果你是一个大胆的人,你可能会尝试使用更简单的解码类ReplayingDecoder。不过你还是需要参考一下API文档来获取更多的信息。
1
public
class
TimeDecoder
extends
ReplayingDecoder {
4
ChannelHandlerContext ctx, ByteBuf in, List"300"
height=
"150"
>out) {out.add(in.readBytes(
4
));}}
此外,Netty还提供了更多可以直接拿来用的解码器使你可以更简单地实现更多的协议,帮助你避免开发一个难以维护的处理器实现。请参考下面的包以获取更多更详细的例子:
对于二进制协议请看io.netty.example.factorial
对于基于文本协议请看io.netty.example.telnet
用POJO代替ByteBuf
我们已经讨论了所有的例子,到目前为止一个消息的消息都是使用ByteBuf作为一个基本的数据结构。在这一部分,我们会改进TIME协议的客户端和服务端的例子,用POJO替代ByteBuf。在你的ChannelHandlerS中使用POJO优势是比较明显的。通过从ChannelHandler中提取出ByteBuf的代码,将会使ChannelHandler的实现变得更加可维护和可重用。在TIME客户端和服务端的例子中,我们读取的仅仅是一个32位的整形数据,直接使用ByteBuf不会是一个主要的问题。然后,你会发现当你需要实现一个真实的协议,分离代码变得非常的必要。首先,让我们定义一个新的类型叫做UnixTime。
01
package
io.netty.example.time;
03
import
java.util.Date;
05
public
class
UnixTime {
07
private
final
int
value;
10
this
((
int
) (System.currentTimeMillis() / 1000L + 2208988800L));
13
public
UnixTime(
int
value) {
22
public
String toString() {
23
return
new
Date((value() - 2208988800L) * 1000L).toString();
现在我们可以修改下TimeDecoder类,返回一个UnixTime,以替代ByteBuf
2
protected
void
decode(ChannelHandlerContext ctx, ByteBuf in, List out) {
3
if
(in.readableBytes() <
4
) {
7
out.add(
new
UnixTime(in.readInt()));
下面是修改后的解码器,TimeClientHandler不再有任何的ByteBuf代码了。