Netty新手引导 基于4.1.x

目录

  • Netty新手引导 基于4.1.x
    • 前言
      • 要解决的问题
      • 解决方案
    • 开始吧
      • 开始之前的准备
      • 写一个丢弃消息的服务器
        • 看透收到的数据
      • 写一个回响服务器
      • 写一个时间服务器
      • 写一个时间客户端
      • 处理流基础的传输
        • 一个关于套接字Buffer小忠告
        • 第一个解决方案
        • 第二种解决方案
      • 使用POJO来交流取代ByteBuf
      • 关闭你的程序
    • 总结

Netty新手引导 基于4.1.x

你知道吗,本网页是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();
	}
}
  1. DiscardServerHandler 继承自 ChannelInboundHandlerAdapter,它是一个CHannelInboundHandler的实现。CHannelInboundHandler提供了多种事件的处理方法你可以去覆盖。目前,你只是继承 ChannelInboundHandlerAdapter就足够了,而不是去实现所有的接口自己去。
  2. 我们在这里覆盖channelRead()事件处理方法,这个方法会在收到消息的时候调用,无论什么时候从客户端收到新的数据。这个示例中,收到的消息类型是ByteBuf
  3. 为了实现DISCARD协议,处理类忽略了收到的消息。ByteBuf是一个引用计数的对象,必须明确的通过release()方法释放。请记住,释放任何被传递给handler的引用计数的对象是这个handler的责任。通常,channelRead()会是现成下面这个样子:
@Override
public void channelRead(channelHandlerContext ctx, Object msg) {
	try {
		//Do something whit msg
	} finally {
		ReferenceCountUtil.release(msg);
	}
}
  1. 这个叫exceptionCaught()事件处理的方法当一个异常发生的时候会被Netty调用,由于I/O错误或者一个handler实现再处理事件中抛出了异常。在大多数情况,捕获的异常应该被记录并且它关联的channel应当并关闭,尽管该方法的实现根据你想要处理的异常情况而不同。比如,你想要先发送一个响应消息带一个错误码在关闭这个链接之前。
    点目前还好,我们已经实现了一半的丢弃服务器。剩下的剧场写一个main方法启动服务器它带一个DiscardServerHandler消息处理器。
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();
	}
}
  1. NioEventLoopGroup 是一个处理I/O操作的多线程事件循环。Netty为不同的传输提供了一些EventLoopGroup实现。我们在这个示例中实现了一个服务器端的程序,所以因此将要被使用两个NioEventLoopGroup。第一个,一般叫‘老板’,接受一个刚到来中的链接。第二个,我们一般叫‘工人’,用来处理一但老板接受好的并且分配给这个工人的连接的流量。使用了多少线程和怎样关联到创建的Channles取决于在EventLoopGroup咋实现甚至可能通过构造器可配置。
  2. ServerBootstrap是一个设置服务器的帮助类。你可是使用Channel直接的设置服务器。尽管如此,请注意那是一个繁琐的过程,你不需要那么做在大多数的情况下。
  3. 这里我们指定NioServerSocketChannel类它用来建立一个新的Channel来接受新来的链接。
  4. 这个指定的处理handler被新的接受的Channel执行。这个ChannelInitializer是个特殊的handler用于帮助使用者配置一个新的Channel。你可能非常想通过配置一个新Channel的ChannelPipline添加一些handler比如DiscardServerHander去实现你的网络程序。当程序变得复杂,他非常可能,你将要添加很多的handler在这个管道那么抽取这个匿名类到一个顶级类是最终的。
  5. 你也可以设置参数指定到这个Channel的实现。我们写了一个TCP/IP的服务器,所以我们可以设置选项比如tcpNodely和keepAlive等。请参考api文档中Channeloption并且指定它的实现去获得有关概述,关于支持的ChannelOptions的选择项。
  6. 你注意到了option() 和 childOption()吗?option()是设置 NioServerSocketChannel它接受来的链接。childOption()为channels来自于夫ServerChannel接受来的channel,这里它就是[NioServerSocketChannel](NioServerSocketChannel。
  7. 我们总算能开始了现在。剩下的就是绑定服务器端口并且开始服务器。这里,我们绑定端口8080到所有的NICs(网卡)在这个机器上的。你可以调用bind()方法你想要几次就几次(用不同的绑定地址)。
    恭喜你哇,你已经完成了你的第一个基于Netty的服务器。

看透收到的数据

       现在我们已经写了第一个服务,我们需要测试一下他是不是真的在干活。最简单的方式去测试他是使用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)
    }
}
  1. 这个无限的循环实际上可以简化成:System.out.println(in.toString(io.netty.util.CharsetUitl.US-ASCII))
  2. 可选方案,你也可以这么写in.release()在这里。

如果你再次运行telnet,你将要看见服务器打印出收到的东西。
这里全部的代码你将在发布包的位于io.netty.example.discard包下找到。

写一个回响服务器

到现在为止,我们已经写了一个消费数据但是根本没有返回任何响应的服务器。一个服务,不管怎样,一般要要给请求一个回复。让我们学习怎么写一个相应消息到客户端,通过实现这个EHCO协议,不管收到啥都把他发回去。
这里与丢弃服务唯一的不同是我们需要实现之前的片段吧收到的数据发回去,替代刚才的打印到控制台。因此,再次修改channelRead()方法就足够了。

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg); // (1)
        ctx.flush(); // (2)
    }
  1. 一个ChannelHandlerContext对象提供了各种操作,使你能够发起各种的I/O事件和操作。这里,我们调用write()方法去把收到的消息一个字不差地写出去。请注意我么没有像之前丢弃服务器那样释放收到的消息,那是因为Netty释放了它当它写出到网络的时候。
  2. xct.write(Object) 不会把消息写到线路。它会缓冲在内部当刷新出去到线路通过ctx.fiush()方法。可以选择的是,你可以调用ctx.writeAndFlush(msg)简洁的实现。
    当你运行telnet命令行再次,你会看到服务器无论你发送什么他都会给你发送回来。
    这个全部的回信服务器代码在发行包中位置【io.netty.example.echo】包下面。

写一个时间服务器

在这个片段中,我么将要实现【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();
	}
}
  1. 像之前解释过的,channelActive()方法会在当一个链接建立并且准备产生流量的时候。让我们在这个方法中写一个32位的整形数字表示当前的时间。
  2. 为了发送一个新的消息,我们需要分配一个新的缓冲buffer保存来的写消息。我们将要写一个32位的整形数,因此我么需要一个最少容量是4的ByteBuf。通过调用ChannelHandlerContext.alloc()获取当前的ByteBufAllocator然后分配了一那样个新的buffer。
  3. 像通常一样,我们写出构造好的消息。
    但是等等,为啥没用flip?在NIO中我们原来不是通过调用java.nio.ByteBuffer.flip()方法,在发送消息之前吗?ByteBuf不需要靠这个操作,因为他有两个指针:一个用为了读操作,一个为了写操作。写指针读的索引增长当你往里面写东西的到这个ByteBuf的时候,这个时候读指针不会变化。这个读索引和写索引分别代表这个消息读开始和结束。
    对比来了说,NIO的buffer没有提供一个清楚的方式去指出哪里是消息开始那里是消息结束,除非调用flip方法。你将要遇到麻烦,当你忘记flip这个buffer,没有或者错误的数据将会被发送出去。这种错误不会出现在Netty上,因为我们对于不同的操作类型有不同的指针。你会发现你的生活更加容易了当你使用它的时候,一个没有讨厌(flipping)的生活。
    另外一点需要注意的是,CHannelHandlerContext.write(和writeAndflush())方法会返回一个ChannelFuture。一个ChannelFuture代表一个还没有发生的I/O操作。它的意思是,任何请求坑没有被执行完毕,因为所有的操作都是异步的在netty里面。比如,下面的代码可能会在消息发送之前被关闭掉。
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();
		}
	}
}
  1. Bootstrap是和ServerBootstrap类似的,区别是它是为了非服务器提供的channels,比如客户端和无链接的channel。
  2. 如果你指定了只有一个EventLoopGroup,它将要同时当作老板组和工人组使用。老板组在客户端不再需要尽管。
  3. NioSocketChannel 取代NioServerSocketChannel,被用来建立一个客户端的Channel。
  4. 注意我们这里没有像我们作ServerBootstrap那样使用childOption()方法,因为客户端的SocketChanel没有双亲。
  5. 我们应该调用connect方法替代bind方法。

就像你看到的那样,它与服务器端的代码没有特别的不同。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();
}
  1. 在TCP/IP,Netty读取从另一端发了的所有数据到一个ByteBuf。

它看起来非常的简单,与服务器端的例子基本没有任何区别。尽管如此,这个消息接受handler有时会拒绝工作产生一个IndexOutOfBonusException。我们将要讨论这种情况为什么会发生在下个部分。

处理流基础的传输

一个关于套接字Buffer小忠告

在一个类似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();
	}
}
  1. 一个ChannelHandler有两个生命周期的监听方法:handlerAdded()和handlerRemoved().你可以任意执行一些初始化反初始化任务只要他不会阻塞太长的时间。
  2. 首先,所有的收到的数据会累积到buf中去。
  3. 那时,handler必须检查buf是不是满了,这个例子是4个字节,然后处理真是的业务逻辑。否则,Netty将会等再一次调用channelRead()方法当更多数据到来的时候,然后最终所有的4个字节都被累积。

第二种解决方案

虽然第一种解决方式解决了时间客户端的这个问题,但是这个修改的handler看起来不够干净。试想,你有一个更复杂的协议,由国歌可变长度的字段组成。你的ChannelInbonundHandler实现将会很快变的不可维护。
你也可能注意到,你可以添加多个ChannelHandler到一个ChannelPipline中,因此,你可以分解一个大的集成ChannelHandler到多个模块化的中,减少你的程序的复杂度。比如,你可以分解TimeClientHander到两个handler中去。

  • TimeDecoder 用来处理分片问题
  • 最初的简单版本的TimeClientHandler.
    幸运的是,Netty提供了一个可扩展的类帮助我们写一个开箱即用的:
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)
	}
}
  1. ByteToMessageDecoder是一个ChannelInboundHandler的实现,它让分片问题更容易处理。
  2. 无论何时,数据来了的时候ByteToMessageDecoder调用有一个中间维护的一个累积buffer的decode()方法。
  3. decode()方法可以决定,当累积buffer没有足够的数据来的时候不再out中添加任何对象。ByteToMessageDecoder将会在下一次来了更多数据的时候再次调用decode()方法。
  4. 如果decode()方法添加了一个对象到out中,那么意味着decoder成功解码一个消息。ByteToMessageDecoder将会丢弃积累buffer中已经读取过的部分。请记住,你不需要解码多条消息。ByteToMessageDecoder会继续调用decode()方法直到没有任何类被加入到out中。
    现在,我们需要将上面的handler插入到ChannelPipeline中,我们需要修改ChannelInitializer的实现中,在TimeClient代码中:
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实现。请关注下面的包,里面有个更多的详细的例子。

  • io.netty.example.factorial 实现二进制协议,和
  • io.netty.example.telnet 实现了一个文本行为基础的协议。

使用POJO来交流取代ByteBuf

之前我们讨论的所以的例子都是用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)
		}
	}
  1. 这一行有一些重要的东西。
    第一,我们传了一个原始的ChannelPromise所以Netty能够标记它成功或者失败,在被编码的数据真正的被写出到线路的时候。
    第二,我们没有调用ctx.flush()方法。这是一个分开的handler,方法flush(ChannelHandlerContext ctx)计划被覆盖了flush()操作。(因为上层的TimeServerHandler已经ctx.writeAndFlush(UnixTime obj)所以fluhs方法会自动的调用);
    为了简化你的代码,你可以使用MessageToByteEncoder:
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和它的文档。

你可能感兴趣的:(Java)