我们继续昨天的点,学习Netty。
写一个时间客户端
与DISCARD和ECHO服务器不同,我们需要TIME协议的客户端,因为人类无法将32位二进制数据转换为日历上的日期。在本节中,我们将讨论如何确保服务器正常工作,并学习如何使用Netty编写客户端。
Netty中服务器和客户端之间最大和唯一的区别是使用了不同的Bootstrap和Channel实现。请看下面的代码:
package netty_beginner; import io.netty.bootstrap.Bootstrap; 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.NioSocketChannel; /** * Created by moon on 2017/4/6. */ public class TimeClient { public static void main(String[] args) { String host = args[0]; int port = Integer.parseInt(args[1]); EventLoopGroup workerGroup = 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 public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); } }); // Start the client. ChannelFuture f = b.connect(host, port).sync(); // (5) // Wait until the connection is closed. f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } }
- Bootstrap类似于ServerBootstrap,但是它用于非服务器通道,如客户端或无连接通道。
- 如果只指定一个EventLoopGroup,它将同时用作一个老板组和一个工人组。虽然老板的工作人员不用于客户端。
- NioSocketChannel代替NioServerSocketChannel,用于创建客户端通道。
- 请注意,我们不使用childOption(),这与ServerBootstrap不同,因为客户端SocketChannel没有父级。
- 我们应该调用connect()方法而不是bind()方法。
正如你所看到的,与服务器端代码没有什么不同。ChannelHandler的实现如何?它应该从服务器接收一个32位整数,将其翻译成可读的格式,打印翻译时间,并关闭连接:
package netty_beginner; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import java.util.Date; /** * Created by moon on 2017/4/6. */ public class TimeClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // super.channelRead(ctx, msg); ByteBuf m = (ByteBuf) msg; // (1) try { long currentTimeMills = (m.readUnsignedInt() - 2208988800L) * 1000L; System.out.println(new Date(currentTimeMills)); ctx.close(); } finally { m.release(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // super.exceptionCaught(ctx, cause); cause.printStackTrace(); ctx.close(); } }
- 在TCP / IP中,Netty将从对等体发送的数据读入ByteBuf。
它看起来很简单,与服务器端的示例没有任何不同。但是,这个处理程序有时会拒绝工作并抛出IndexOutOfBoundsException异常。我们会在下一节讨论为什么会发生这种情况。
处理基于流的运输
套接字缓冲区的一个小小注意事项
在诸如TCP / IP的基于流的传输中,接收的数据被存储到套接字接收缓冲器中。不幸的是,基于流的传输的缓冲区不是数据包的队列,而是字节队列。这意味着,即使您发送两个消息作为两个独立数据包,操作系统也不会将它们视为两个消息,而只是一堆字节。因此,您无法保证您所读取的内容正是您远程对等人写的内容。例如,假设操作系统的TCP / IP堆栈已经收到三个数据包:
由于基于流的协议的这种一般属性,在应用程序中以下列分片形式读取它们的可能性很大:
因此,无论服务器端或客户端如何,接收部分都应将接收的数据进行碎片整理为一个或多个可以被应用逻辑轻松识别的有意义的框架。在上述示例的情况下,接收到的数据应该如下所示:
第一个解决方案
现在让我们回到TIME客户端的例子。我们在这里也有同样的问题。32位整数是非常少量的数据,它不太可能经常碎片化。然而,问题是它可以分散,碎片化的可能性会随着流量的增加而增加。
简单的解决方案是创建内部累积缓冲区,并等待所有4个字节都被接收到内部缓冲区。以下是修改的TimeClientHandler实现,可以解决问题:
- ChannelHandler有两个生命周期监听器方法:handlerAdded()和handlerRemoved()。只要不长时间阻止,您可以执行任意初始化或非初始化任务。
- 首先,所有收到的数据应该被累积到buf。
- 然后,处理程序必须检查buf是否有足够的数据,在此示例中为4个字节,并转到实际的业务逻辑。否则,Netty会在更多数据到达时再次调用channelRead()方法,最终所有4个字节都将被累积。
第二个解决方案
虽然第一个解决方案已经解决了TIME客户端的问题,但修改后的处理程序看起来并不那么干净。想象一个更复杂的协议,由多个字段组成,如可变长度字段。您的ChannelInboundHandler实现将很快变得不可靠。
您可能已经注意到,您可以向ChannelPipeline添加多个ChannelHandler,因此,您可以将一个单一的ChannelHandler拆分成多个模块化的,以减少应用程序的复杂性。例如,您可以将TimeClientHandler拆分为两个处理程序:
- 处理碎片问题的TimeDecoder
- 初始简单版本的TimeClientHandler
幸运的是,Netty提供了一个可扩展的类,可帮助您编写开箱即用的第一个处理程序:
package netty_beginner; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import java.util.List; /** * Created by moon on 2017/4/6. */ 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) } }
- ByteToMessageDecoder是ChannelInboundHandler的一个实现,它可以很容易地解决分片问题。
- 当接收到新数据时,ByteToMessageDecoder会在内部维护的累积缓冲区中调用decode()方法。
- decode()可以决定在累积缓冲区中没有足够数据的地方添加任何东西。当接收到更多数据时,ByteToMessageDecoder将再次调用decode()。
- 如果decode()将一个对象添加到out中,则表示解码器成功解码了消息。ByteToMessageDecoder将丢弃累积缓冲区的读取部分。请记住,您不需要解码多条消息。 ByteToMessageDecoder将继续调用decode()方法,直到它不添加任何内容。
现在我们有另一个处理程序插入ChannelPipeline,我们应该修改TimeClient中的ChannelInitializer实现:
b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeDecoder(),new TimeClientHandler()); } });如果你是一个冒险的人,你可能想尝试ReplayingDecoder,它更加简化了解码器。但是,您需要参考API参考资料。
public class TimeDecoder extends ReplayingDecoder<Void> { @Override protected void decode( ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { out.add(in.readBytes(4)); } }
此外,Netty还提供了开箱即用的解码器,使您能够轻松实现大多数协议,并帮助您避免一个不可维护的处理程序实现。有关更多详细示例,请参阅以下软件包:
io.netty.example.factorial
for a binary protocol, andio.netty.example.telnet
for a text line-based protocol.
Speaking in POJO instead of ByteBuf
我们迄今为止审查的所有示例都使用ByteBuf作为协议消息的主要数据结构。在本节中,我们将改进TIME协议客户端和服务器示例,以使用POJO而不是ByteBuf。
在您的ChannelHandlers中使用POJO的优势是显而易见的。通过分离从处理程序中提取ByteBuf信息的代码,你的处理程序变得更加可维护和可重用。在TIME客户端和服务器示例中,我们只读取一个32位整数,直接使用ByteBuf不是一个主要问题。但是,您将发现在实现真实世界的协议时需要进行分离。
首先,我们定义一个名为UnixTime的新类型。
package netty_beginner; import java.util.Date; /** * Created by moon on 2017/4/6. */ 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) { // (2) if (in.readableBytes() < 4) { return; // (3) } out.add(new UnixTime(in.readUnsignedInt())); }
使用更新的解码器,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(final ChannelHandlerContext ctx) throws Exception { @Override public void channelActive(ChannelHandlerContext ctx) { ChannelFuture f = ctx.writeAndFlush(new UnixTime()); f.addListener(ChannelFutureListener.CLOSE); } }
现在,唯一缺少的部分是编码器,它是ChannelOutboundHandler的一个实现,它将UnixTime转换为ByteBuf。它比编写解码器简单得多,因为在编码消息时不需要处理数据包碎片和汇编。
package netty_beginner; /** * Created by moon on 2017/4/6. */ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise;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()); ctx.write(encoded, promise); // (1) } }
- 这一行中有很多重要的事情。
首先,我们按原样通过原来的ChannelPromise,以便当编码数据实际写入电线时,Netty将其标记为成功或失败。
其次,我们没有调用ctx.flush()。有一个单独的处理方法void flush(ChannelHandlerContext ctx),用于覆盖flush()操作。
为了进一步简化,您可以使用MessageToByteEncoder:
public class TimeEncoder extends MessageToByteEncoder<UnixTime> { @Override protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) { out.writeInt((int)msg.value()); } }剩下的最后一个任务是在TimeServerHandler之前的服务器端的ChannelPipeline中插入一个TimeEncoder,并将其作为一个简单的练习。
关闭您的应用程序
关闭Netty应用程序通常就像关闭通过shutdownGracefully()创建的所有EventLoopGroups一样简单。当EventLoopGroup已完全终止,并且属于该组的所有通道都已关闭,他返回一个future来通知你。
概要
在本章中,我们快速浏览了Netty,并展示了如何在Netty上编写完整的网络应用程序。有关Netty的更多详细信息在即将到来的章节。我们还鼓励您查看io.netty.example包中的Netty示例。还请注意,社区一直在等待您的问题和想法,以帮助您,并根据您的反馈不断改进Netty及其文档。
好了 以上就是Netty官网给出的入门级别介绍。希望对大家有所帮助。