你知道吗,本网页是Github Wiki 页面自动生成的,你可以自主的改它进在这里。
如今我们使用通用软件或者库来互相通讯。比如,我们经常使用HTTP客户端库来接收信息从一个web服务器和调用远程过程通过一个web服务。尽管如此,一个通用协议或者它的实现了有时没有很好的伸缩性。他就像为啥为什么我们不用一个通用的HTTP服务来交换大量文件、e-mail信息、和接近实时的消息例如金融信息和多人游戏数据。他们需要的是高度优化的协议实现,为了特殊的目的。比如,你需要实现一个为AJAX-based聊天程序,媒体流,或大文件传输而优化过的HTTP服务器。你可能甚至想要设置和实现一整个的新协议为了你的需要精确定做。另一个不可避免的情况是你需要处理一个遗留的专有协议确保他可以可老系统互相操作。最主要的在这种情况下是如何快捷实现协议同时不牺牲稳定性和性能对这个结果程序。
Netty 项目努力提供了一异步的,事件驱动的网络应用工具和框架,为快速开发可维护、高性能、高伸缩协议服务器和客户端。
换句话说,Netty 是一个NIO客户端服务器框架使快速和简单的开发网络协议服务器和客户端程序成为可能。它极大的简化和流水线化网络程序例如TCP和UPD套接字服务开发。
’快和容易‘不意味着结果程序会遭受维护和性能问题。Netty被设计地小心,从好多协议的实现比如FTP,SMTP,HTTP,和一些可变二进制、文本为基础的遗留协议中学习了经验。结果就是,Netty成功的找到一条不妥协的路做到简单开发,性能,稳定,灵活。
一些使用者可能已经发现其他的一些声称有同样益处的框架,and你也许想要问是什么让Netty与他们不同。答案就是Netty的基于的哲学。Netty从第一天就被设计的给你同时在API和实现上更舒服的体验。他不是有形的,但是你将要意识到这个哲学将使你生活变得更容易当你读这个文档和玩Netty时。
这一章节的教程围绕核心构造用简单的例子让你快速的开始。你将要能够立即地正确使用Netty写一个客户的短和服务器在你看见这章之后。
如果你更倾向于从上而下的方式学习东西,你也许需要开始从第二章结构概述开始然后再回来。
最低要求的对于运行这一章的例子只有两个:最新版本的Netty和JDK1.6以上。最近版本的Netty在工程下载页面获取。为了下载正确的jdk版本,请参考你喜欢的JDK提供页面。
当你阅读的时候,在本章节中你可能会有很多的问题关于介绍到的类,请你参考API参考资料,不管何时你想要知道更多关于它们的时候。所有的类名在文档中都被链接到在线API参考中方便你查询。尽管如此,请不要犹豫联系Netty项目社区,让我们知道如果哪有不正确的信息,拼写语法错误,以及你有更好的主意帮助我们提升这个文档资料。
最简单的协议在这个世界上不是"Hello, World!",而是丢弃.这个协议会丢弃任何的收到的数据同时没有任何的回复。
为了实现这个丢弃协议,唯一需要做的事情就是忽略收到的数据,让我们直接从handler的实现开始,它处理由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)
// 关闭链接当异常发生的时候
cause.printStackTrace();
ctx.close();
}
}
@Override
public void channelRead(channelHandlerContext ctx, Object msg) {
try {
//Do something whit msg
} finally {
ReferenceCountUtil.release(msg);
}
}
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;
/**
* Discard any incomming 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)
.childHander(new ChannelInitializer<SocketChannel>() {//(4)
@Override
public vodi 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)
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
boosGroup.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命令。打个比方,我么可以输入telnet localhost 8080在命令行中,让后输入点东西。
尽管如此,你也不能说它工作的很好,因为我么不能真实的知道因为他是一个丢弃服务嘛。你不会得到任何的响应根本。让我们少错修改打印出它收到了啥。
我们已经知道了这个channelRead()方法无论何时只要数据来了就会被调用。让我们放一些代码在channelRead()方法当中去在DiscardServerHandler类中。
@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)
}
}
如果你再次运行telnet,你将要看见服务器打印出收到的东西。
这里全部的代码你将在发布包的位于io.netty.example.discard包下找到。
到现在为止,我们已经写了一个消费数据但是根本没有返回任何响应的服务器。一个服务,不管怎样,一般要要给请求一个回复。让我们学习怎么写一个相应消息到客户端,通过实现这个EHCO协议,不管收到啥都把他发回去。
这里与丢弃服务唯一的不同是我们需要实现之前的片段吧收到的数据发回去,替代刚才的打印到控制台。因此,再次修改channelRead()方法就足够了。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
在这个片段中,我么将要实现【TIME】协议,他和之前的例子不同的地方在于他会发送一个消息,里面包含32-位的整数,不需要收到任何的请求,当消息发出后就关闭当前的链接。在这个例子中,你将会学会发么构造和发送消息,当完成的时候关闭该链接。
因为我们将要忽略任何收到的数据但是会写出一个message消息当它一建立链接。我们不能使用channelRead()方法这一次,我们应该覆盖channelActive()方法。下面就是这个的实现:
package io.netty.example.time;
public class TimeServerHandler extends ChannelInboundHandlerAdapt {
@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();
}
}
Channel ch= ...;
ch.writeAndFlush(message);
ch.close();
因此,你需要调用close方法要在,write方法返回的ChannelFuture完成并且它通知监听者写的操作已经完成。请注意这些,close也是一个异步方法,也可能没有立即的关闭这个链接,并且它也会返回一个ChannelFuture。
4. 那我们怎么才能够当请求写入完成时得到一个通知呢?只要向返回的ChannelFuture添加(addListener方法)一个ChannelFutureListener就这么简单,现在,我们建立一个新的匿名ChannelFutureListener当关闭这个Channel的以后这个listener中的operationComplete()方法就会执行,在这个方法中你就可以实现你的逻辑(关闭这个链接)了。
作为可选的,你可以简化这个代码使用已经预定义好的listener。这个listener的被通知时会关闭链接:
Channel ch= ...;
ChannelFuture f = ch.writeAndFlush(message);
//ch.close(); not this time!
f.addListener(ChannelFutureListener.CLOSE);
为了测试这个服务器是否工作我们可以使用UNIX的rdate命令:
$ rdate -o <port> -p <host>
这里的 端口 是你传给main方法的那个端口,host通常本机的话通常使用localhost
不同于丢弃服务和回响服务,我么需要实现TIME协议的客户端,因为人类不能转换一个32位的二进制数据在日历上转换成日期。在这个片段,我们讨论如何确认这个服务器正确工作并且学习怎么去写一个客户端通过Netty。
这里最大的不同于服务器,一个客户在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 worker = 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
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
// 启动这个客户端
ChannelFuture f = b.connect(host, port).sync();
// 等待直到链接关闭
f.channel().cloase
} finally {
workerGroup.shuddownGracefully();
}
}
}
就像你看到的那样,它与服务器端的代码没有特别的不同。ChanelHandler是怎么实现的呢?它将收到一个32位的整型数从服务器端,翻译成人类能够读懂的格式,打印翻译出来的时间,然后关闭这个链接。
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctc, Object msg) {
ByteBuf m = (ByteBuf)msg;//1
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) *
1000l;
System.out.println(currenntTimeMills)
ctx.close();
} finally {
m.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throw cause) {
cause.printStackTrace():
ctx.close();
}
它看起来非常的简单,与服务器端的例子基本没有任何区别。尽管如此,这个消息接受handler有时会拒绝工作产生一个IndexOutOfBonusException。我们将要讨论这种情况为什么会发生在下个部分。
在一个类似TCP/IP这样流为基础的传输中,收到的数据会存贮到一个套接字接受缓冲中(socket receive buffer)。不幸的是,这个流为基础的缓冲不是一个数据包的队列,而是字节的队列。它意味着,即使你发送了两个消息在各自独立的包中,操作系统不会按照两个消息来处理它,而是只是按照一捆字节来处理。因此这里不能保证你读取的精确的是你的远端发的。比如,让我们假定一个操作系统中的TCP/IP栈收到了如下三个包:
ABC | DEF | GHI |
---|
因为他是流基础的协议的大概属性,它很有可能会在你的程序里面读成下面的分片格式:
AB | CDEFG | H | I |
---|
因此,收到的部分,不论服务器端还是客户端,应当把收到的数据整理成一个或者多个有意义的架构这样可以被应用程序逻辑容易的理解。在这个上面的例子中,收到的数据应该被弄成像下面这样的帧:
ABC | DEF | GHI |
---|
现在让我们回到TIME客户端这个例子。我们也有同样的问题在这里。一个32为的整数是一个非常小数量的数据。它一般不容易被分片,问题是它可以被分片发生,并且被分片的可能性将随着流量的增加二增大。
一个简化的解决方式是建立一个中间的累积的buffer然后等待,直到4个字节被收到到这个中间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) {
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();
}
}
虽然第一种解决方式解决了时间客户端的这个问题,但是这个修改的handler看起来不够干净。试想,你有一个更复杂的协议,由国歌可变长度的字段组成。你的ChannelInbonundHandler实现将会很快变的不可维护。
你也可能注意到,你可以添加多个ChannelHandler到一个ChannelPipline中,因此,你可以分解一个大的集成ChannelHandler到多个模块化的中,减少你的程序的复杂度。比如,你可以分解TimeClientHander到两个handler中去。
package io.netty.example.time;
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)
}
}
b.handler(new CHannelInitializer<SocketChannel>() {
@Override
public vid inithannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
}
});
如果你是一个喜欢冒险的人,你可能要想试一下ReplayingDecoder,它简化了上面的解码。你将可以通过API提及更多的信息。
public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protecte void decode(ChannelHandlerConttext ctx, ByteBuf in, LIst<Object> out){
out.add(in.readBytes(4));
}
}
另外,Netty体验提供了开箱即用的解码器,它们能够让你非常轻松地实现更多的解码器,并且帮助你避免大量的不可维护的handlr实现。请关注下面的包,里面有个更多的详细的例子。
之前我们讨论的所以的例子都是用ByteBuf来作为消息协议的基础的数据结构。在这个片节中,我们将要改进TIME协议的客户端和服务器使用POJO代替ByteBuf。
使用POJO在你的CHannelHandler中的好处明显的;你的handler会变的更加的可维护可重用,因为分抽取信息从ByteBuf到你的handler中。在TIME客户端中,我们只是读了一个32位的整数,直接使用ButerBuf还不是很大的问题。可是,你会发现在现实世界的消息中,把他分解是很有必要的。
首先,我们先定义一个显得类型,叫做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();
}
}
我们现在重新修订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()));
}
更新decoder之后,TimeCLientHandler就不再需要一个ByteBuf了。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
UnixTime m = (UnixTime) msg;
System.out.println(m);
ctx.close();
}
非常的简单和优雅,是吧?同样的技术可以实施到服务器端。让我们更新一下TimeServerHandler首先这次:
@Overide
public void channelActive(ChannelHandlerContext ctx) {
ChannelFuture f = ctx.writeAndFlush(new UnixTime());
f.addListener(CHannnelFutureListener.CLOSE);
}
现在只差一点就是编码器encoder了,他实现了ChannelOutboundHandler接口,他UnixTime转换回一个ByteBuf。他非常的简单对比写一个解码器decoder,因为它不需要处理包分片和重新打包的问题,当编码一个消息的时候。
package io.netty.example.time;
public class TimeEncoder extends ChannelOutboundlerAdapter {
@Override
public void write(ChannelHandlerContext cxt, Object msg, ChannelPromis promis) {
UnixTime m = (UnixTime) msg;
ByteBuf encoded = ctx.alloc().buffer(4);
encoded.writeInt(m.value());
ctx.write(encoded, promis); //(1)
}
}
public class TimeEncoder extends MessageToByterEncoder<UnixTime> {
@Override
public void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
out.write((int)msg.value());
}
}
剩下的任务是把TimeEncoder加入到服务端ChannelPipline的TimeServerHandler之前,这里留下当作一个简单的练习吧。
关闭一个Netty程序一般和关闭所有的你创建的EventLoopGroups调用shutdownGracefully一样的简单。他会返回一个Future,他会通知你当EventLoopGroup完全终止和所有属于这个group的Channel被关闭的时候。
在这个章节中,我们得到了一个使用怎样使用Netty构建一个完整工作的带样例的快速课程。
有更多的关于Netty的详细的信息在即将来临的章节。我们也鼓励你查阅Netty例子在io.netty.example包中。
请注意,Netty社区也会一直等待你的问题和好主意在你反馈的基础上持续的提升Netty和它的文档。