Netty中文技术文档.docx
《Netty中文技术文档.docx》由会员分享,可在线阅读,更多相关《Netty中文技术文档.docx(36页珍藏版)》请在冰点文库上搜索。
Netty中文技术文档
本指南对Netty进行了介绍并指出其意义所在。
1.问题
现在,我们使用适合一般用途的应用或组件来和彼此通信。
例如,我们常常使用一个HTTP客户端从远程服务器获取信息或者通过web
services进行远程方法的调用。
然而,一个适合普通目的的协议或其实现并不具备其规模上的扩展性。
例如,我们无法使用一个普通的HTTP服务器进行大型文件,电邮信息的交互,或者
处理金融信息和多人游戏数据那种要求准实时消息传递的应用场景。
因此,这些都要求使用一个适用于特殊目的并经过高度优化的协议实现。
例如,你可能想要实现
一个对基于AJAX的聊天应用,媒体流或大文件传输进行过特殊优化的HTTP服务器。
你甚至可能想去设计和实现一个全新的,特定于你的需求的通信协议。
另一种无法避免的场景是你可能不得不使用一种专有的协议和原有系统交互。
在这种情况下,你需要考虑的是如何能够快速的开发出这个协议的实现并且同时
还没有牺牲最终应用的性能和稳定性。
2.方案
Netty是一个异步的,事件驱动的网络编程框架和工具,使用Netty
可以快速开发出可维护的,高性能、高扩展能力的协议服务及其客户端应用。
也就是说,Netty是一个基于NIO的客户,服务器端编程框架,使用Netty
可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。
Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP
和UDP的socket服务开发。
"快速"和"简单"并不意味着会让你的最终应用产生维护性或性能上的问题。
Netty
是一个吸收了多种协议的实现经验,这些协议包括FTP,SMPT,HTTP,各种二进制,文本协议,并经过相当精心设计的项目,最终,Netty
成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。
一些用户可能找到了某些同样声称具有这些特性的编程框架,因此你们可能想问Netty又有什么不一样的地方。
这个问题的答案是Netty
项目的设计哲学。
从创立之初,无论是在API还是在其实现上Netty
都致力于为你提供最为舒适的使用体验。
虽然这并不是显而易见的,但你终将会认识到这种设计哲学将令你在阅读本指南和使用Netty
时变得更加得轻松和容易。
第一章.开始
这一章节将围绕Netty的核心结构展开,同时通过一些简单的例子可以让你更快的了解Netty的使用。
当你读完本章,你将有能力使用Netty完
成客户端和服务端的开发。
如果你更喜欢自上而下式的学习方式,你可以首先完成第二章:
架构总览的学习,然后再回到这里。
1.1.开始之前
运行本章示例程序的两个最低要求是:
最新版本的Netty程序以及JDK1.5或更高版本。
最新版本的Netty程序可在项目下载页
下载。
下载正确版本的JDK,请到你偏好的JDK站点下载。
这就已经足够了吗?
实际上你会发现,这两个条件已经足够你完成任何协议的开发了。
如果不是这样,请联系Netty项目社区
,让我们知道还缺少了什么。
最终但不是至少,当你想了解本章所介绍的类的更多信息时请参考API手册。
为方便你的使用,这篇文档中所有的类名均连接至在线API手册。
此外,如
果本篇文档中有任何错误信息,无论是语法错误,还是打印排版错误或者你有更好的建议,请不要顾虑,立即联系Netty项目社区。
1.2.抛弃协议服务
在这个世界上最简化的协议不是"Hello,world!
"而是抛弃协议。
这是一种丢弃接收到的任何数据并不做任何回应的协议。
实现抛弃协议(DISCARD
protocol),你仅需要忽略接受到的任何数据即可。
让我们直接从处理器(handler)实现开始,这个处理器处理Netty的所有I/O事件。
packagety.example.discard;
@ChannelPipelineCoverage("all")1
publicclassDiscardServerHandlerextendsSimpleChannelHandler{2
@Override
publicvoidmessageReceived(ChannelHandlerContextctx,
MessageEvente){3
}
@Override
publicvoidexceptionCaught(ChannelHandlerContextctx,
ExceptionEvente){4
e.getCause().printStackTrace();
Channelch=e.getChannel();
ch.close();
}
@Override
publicvoidexceptionCaught(ChannelHandlerContextctx,
ExceptionEvente){4
e.getCause().printStackTrace();
Channelch=e.getChannel();
ch.close();
}
}
代码说明
1)ChannelPipelineCoverage注解了一种处理器类型,这个注解标示了一个处理器是否可被多个Channel通道共享(同时关
联着ChannelPipeline)。
DiscardServerHandler没有处理任何有状态的信息,因此这里的注解是"all"。
2)DiscardServerHandler继承了SimpleChannelHandler,这也是一个ChannelHandler
的实现。
SimpleChannelHandler提供了多种你可以重写的事件处理方法。
目前直接继承SimpleChannelHandler已经足够
了,并不需要你完成一个自己的处理器接口。
3)我们这里重写了messageReceived事件处理方法。
这个方法由一个接收了客户端传送数据的MessageEvent事件调用。
在这个
例子中,我们忽略接收到的任何数据,并以此来实现一个抛弃协议(DISCARDprotocol)。
4)exceptionCaught
事件处理方法由一个ExceptionEvent异常事件调用,这个异常事件起因于Netty的I/O异常或一个处理器实现的内部异常。
多数情况下,捕捉
到的异常应当被记录下来,并在这个方法中关闭这个channel通道。
当然处理这种异常情况的方法实现可能因你的实际需求而有所不同,例如,在关闭这个连
接之前你可能会发送一个包含了错误码的响应消息。
目前进展不错,我们已经完成了抛弃协议服务器的一半开发工作。
下面要做的是完成一个可以启动这个包含DiscardServerHandler处理
器服务的主方法。
packagety.example.discard;
import.InetSocketAddress;
importjava.util.concurrent.Executors;
publicclassDiscardServer{
publicstaticvoidmain(String[]args)throwsException{
ChannelFactoryfactory=
newNioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ServerBootstrapbootstrap=newServerBootstrap(factory);
DiscardServerHandlerhandler=newDiscardServerHandler();
ChannelPipelinepipeline=bootstrap.getPipeline();
pipeline.addLast("handler",handler);
bootstrap.setOption("child.tcpNoDelay",true);
bootstrap.setOption("child.keepAlive",true);
bootstrap.bind(newInetSocketAddress(8080));
}
}
代码说明
1)ChannelFactory是一个创建和管理Channel通道及其相关资源的工厂接口,它处理所有的I/O请求并产生相应的I/O
ChannelEvent通道事件。
Netty提供了多种ChannelFactory
实现。
这里我们需要实现一个服务端的例子,因此我们使用NioServerSocketChannelFactory实现。
另一件需要注意的事情是这个工
厂并自己不负责创建I/O线程。
你应当在其构造器中指定该工厂使用的线程池,这样做的好处是你获得了更高的控制力来管理你的应用环境中使用的线程,例如一
个包含了安全管理的应用服务。
2)ServerBootstrap
是一个设置服务的帮助类。
你甚至可以在这个服务中直接设置一个Channel通道。
然而请注意,这是一个繁琐的过程,大多数情况下并不需要这样做。
3)这里,我们将DiscardServerHandler处理器添加至默认的ChannelPipeline通道。
任何时候当服务器接收到一个新
的连接,一个新的ChannelPipeline管道对象将被创建,并且所有在这里添加的ChannelHandler对象将被添加至这个新的
ChannelPipeline管道对象。
这很像是一种浅拷贝操作(ashallow-copy
operation);所有的Channel通道以及其对应的ChannelPipeline实例将分享相同的DiscardServerHandler
实例。
4)你也可以设置我们在这里指定的这个通道实现的配置参数。
我们正在写的是一个TCP/IP服务,因此我们运行设定一些socket选项,例如
tcpNoDelay和keepAlive。
请注意我们在配置选项里添加的"child."前缀。
这意味着这个配置项仅适用于我们接收到的通道实例,而不
是ServerSocketChannel实例。
因此,你可以这样给一个ServerSocketChannel设定参数:
bootstrap.setOption("reuseAddress",true);
5)我们继续。
剩下要做的是绑定这个服务使用的端口并且启动这个服务。
这里,我们绑定本机所有网卡(NICs,networkinterface
cards)上的8080端口。
当然,你现在也可以对应不同的绑定地址多次调用绑定操作。
大功告成!
现在你已经完成你的第一个基于Netty的服务端程序。
1.3.查看接收到的数据
现在你已经完成了你的第一个服务端程序,我们需要测试它是否可以真正的工作。
最简单的方法是使用telnet
命令。
例如,你可以在命令行中输入"telnetlocalhost8080"或其他类型参数。
然而,我们可以认为服务器在正常工作吗?
由于这是一个丢球协议服务,所以实际上我们无法真正的知道。
你最终将收不到任何回应。
为了证明它在真正的工
作,让我们修改代码打印其接收到的数据。
我们已经知道当完成数据的接收后将产生MessageEvent消息事件,并且也会触发messageReceived处理方法。
所以让我在
DiscardServerHandler处理器的messageReceived方法内增加一些代码。
@Override
publicvoidmessageReceived(ChannelHandlerContextctx,MessageEvent
e){
ChannelBufferbuf=(ChannelBuffer)e.getMessage();
while(buf.readable()){
System.out.println((char)buf.readByte());
}
}
代码说明
1)
基本上我们可以假定在socket的传输中消息类型总是ChannelBuffer。
ChannelBuffer是Netty的一个基本数据结构,这个数
据结构存储了一个字节序列。
ChannelBuffer类似于NIO的ByteBuffer,但是前者却更加的灵活和易于使用。
例如,Netty允许你创
建一个由多个ChannelBuffer构建的复合ChannelBuffer类型,这样就可以减少不必要的内存拷贝次数。
2)
虽然ChannelBuffer有些类似于NIO的ByteBuffer,但强烈建议你参考Netty的API手册。
学会如何正确的使用
ChannelBuffer是无障碍使用Netty的关键一步。
如果你再次运行telnet命令,你将会看到你所接收到的数据。
抛弃协议服务的所有源代码均存放在在分发版的ty.example.discard包下。
1.4.响应协议服务
目前,我们虽然使用了数据,但最终却未作任何回应。
然而一般情况下,一个服务都需要回应一个请求。
让我们实现ECHO协议
来学习如何完成一个客户请求的回应消息,ECHO协议规定要返回任何接收到的数据。
与我们上一节实现的抛弃协议服务唯一不同的地方是,这里需要返回所有的接收数据而不是仅仅打印在控制台之上。
因此我们再次修改
messageReceived方法就足够了。
@Override
publicvoidmessageReceived(ChannelHandlerContextctx,MessageEvent
e){
Channelch=e.getChannel();
ch.write(e.getMessage());
}
代码说明
1)一个ChannelEvent通道事件对象自身存有一个和其关联的Channel对象引用。
这个返回的Channel通道对象代表了这个接收
MessageEvent消息事件的连接(connection)。
因此,我们可以通过调用这个Channel通道对象的write方法向远程节点写入返
回数据。
现在如果你再次运行telnet命令,你将会看到服务器返回的你所发送的任何数据。
相应服务的所有源代码存放在分发版的ty.example.echo包下。
1.5.时间协议服务
这一节需要实现的协议是TIME协议
。
这是一个与先前所介绍的不同的例子。
这个例子里,服务端返回一个32位的整数消息,我们不接受请求中包含的任何数据并且当消息返回完毕后立即关闭连接。
通过这个例子你将学会如何构建和发送消息,以及当完成处理后如何主动关闭连接。
因为我们会忽略接收到的任何数据而只是返回消息,这应当在建立连接后就立即开始。
因此这次我们不再使用messageReceived方法,取而代
之的是使用channelConnected方法。
下面是具体的实现:
packagety.example.time;
@ChannelPipelineCoverage("all")
publicclassTimeServerHandlerextendsSimpleChannelHandler{
@Override
publicvoidchannelConnected(ChannelHandlerContextctx,
ChannelStateEvente){
Channelch=e.getChannel();
ChannelBuffertime=ChannelBuffers.buffer(4);
time.writeInt(System.currentTimeMillis()/1000);
ChannelFuturef=ch.write(time);
f.addListener(newChannelFutureListener(){
publicvoidoperationComplete(ChannelFuturefuture){
Channelch=future.getChannel();
ch.close();
}
});
}
@Override
publicvoidexceptionCaught(ChannelHandlerContextctx,
ExceptionEvente){
e.getCause().printStackTrace();
e.getChannel().close();
}
}
代码说明
1)
正如我们解释过的,channelConnected方法将在一个连接建立后立即触发。
因此让我们在这个方法里完成一个代表当前时间(秒)的32位整数消
息的构建工作。
2)为了发送一个消息,我们需要分配一个包含了这个消息的buffer缓冲。
因为我们将要写入一个32位的整数,因此我们需要一个4字节的
ChannelBuffer。
ChannelBuffers是一个可以创建buffer缓冲的帮助类。
除了这个buffer方
法,ChannelBuffers还提供了很多和ChannelBuffer相关的实用方法。
更多信息请参考API手册。
另外,一个很不错的方法是使用静态的导入方式:
importstaticty.buffer.ChannelBuffers.*;
...
ChannelBufferdynamicBuf=dynamicBuffer(256);
ChannelBufferordinaryBuf=buffer(1024);
3)像通常一样,我们需要自己构造消息。
但是打住,flip在哪?
过去我们在使用NIO发送消息时不是常常需要调用
ByteBuffer.flip()方法吗?
实际上ChannelBuffer之所以不需要这个方法是因为
ChannelBuffer有两个指针;一个对应读操作,一个对应写操作。
当你向一个
ChannelBuffer写入数据的时候写指针的索引值便会增加,但与此同时读指针的索引值不会有任何变化。
读写指针的索引值分别代表了这个消息的开
始、结束位置。
与之相应的是,NIO的buffer缓冲没有为我们提供如此简洁的一种方法,除非你调用它的flip方法。
因此,当你忘记调用flip方法而引起发
送错误时,你便会陷入困境。
这样的错误不会再Netty中发生,因为我们对应不同的操作类型有不同的指针。
你会发现就像你已习惯的这样过程变得更加容易—
一种没有flippling的体验!
另一点需要注意的是这个写方法返回了一个ChannelFuture对象。
一个ChannelFuture
对象代表了一个尚未发生的I/O操作。
这意味着,任何已请求的操作都可能是没有被立即执行的,因为在Netty内部所有的操作都是异步的。
例如,下面的代
码可能会关闭一个连接,这个操作甚至会发生在消息发送之前:
Channelch=...;
ch.write(message);
ch.close();
因此,你需要这个write方法返回的ChannelFuture对象,close方法需要等待写操作异步完成之后的ChannelFuture通
知/监听触发。
需要注意的是,关闭方法仍旧不是立即关闭一个连接,它同样也是返回了一个ChannelFuture对象。
4)
在写操作完成之后我们又如何得到通知?
这个只需要简单的为这个返回的ChannelFuture对象增加一个ChannelFutureListener
即可。
在这里我们创建了一个匿名ChannelFutureListener对象,在这个ChannelFutureListener对象内部我们处理了
异步操作完成之后的关闭操作。
另外,你也可以通过使用一个预定义的监听类来简化代码。
f.addListener(ChannelFutureListener.CLOSE);
1.6.时间协议服务客户端
不同于DISCARD和ECHO协议服务,我们需要一个时间协议服务的客户端,因为人们无法直接将一个32位的二进制数据转换一个日历时间。
在这一
节我们将学习如何确保服务器端工作正常,以及如何使用Netty完成客户端的开发。
使用Netty开发服务器端和客户端代码最大的不同是要求使用不同的Bootstrap及ChannelFactory。
请参照以下的代码:
packagety.example.time;
import.InetSocketAddress;
importjava.util.concurrent.Executors;
publicclassTimeClient{
publicstaticvoidmain(String[]args)throwsException{
Stringhost=args[0];
intport=Integer.parseInt(args[1]);
ChannelFactoryfactory=
newNioClientSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ClientBootstrapbootstrap=newClientBootstrap(factory);
TimeClientHandlerhandler=newTimeClientHandler();
bootstrap.getPipeline().addLast("handler",handler);
bootstrap.setOption("tcpNoDelay",true);
bootstrap.setOption("keepAlive",true);
bootstrap.connect(newInetSocketAddress(host,port));
}
}
代码说明
1)
使用NioClientSocketChannelFactory而不是NioServerSocketChannelFactory来创建客户端的
Channel通道对象。
2)客户端的ClientBootstrap对应ServerBootstrap。
3)请注意,这里不存在使用"child."前缀的配置项,客户端的SocketChannel实例不存在父级Channel对象。
4)我们应当调用connect连接方法,而不是之前的bind绑定方法。
正如你所看到的,这与服务端的启动过程是完全不一样的。
ChannelHandler又该如何实现呢?
它应当负责接收一个32位的整数,将其转换为
可读的格式后,打印输出时间,并关闭这个连接。
packageorg.j