Netty用户指南

一、前言

1.问题

当今世界我们需要使用通用的软件或库与其他组件进行通信,例如使用HTTP客户端从服务器中获取信息,或通过网络服务调用一个远程的方法。然而通用的协议及其实现通常不具备较好的伸缩性。所以问题看起来是我们怎么不使用通用的HTTP服务器去传输大文件、e-mail、实事数据、多媒体数据等。我们需要的是针对特定问题而进行优化的协议实现。例如我们可能需要重新实现一个HTTP服务器来与AJAX的客户端进行通信。另外一种情况是需要处理历史遗留的协议保证与旧的系统兼容。这些例子的关键在于怎样快速的实现协议而不损失目标系统的稳定性和性能。

2.解决方案

Netty是一个异步事件驱动的网络应用框架,可以用来快速开发可维护的、高性能、可扩展的协议服务器和客户端。

换句话说,Netty是一个基于NIO的客户端和服务器框架,可以简单快速的开发网络应用程序,如协议的客户端和服务器。它极大的简化了TCP、UDP服务器之类的网络编程。

二、开始

1.编写DiscardServer

最简单的协议并不是“hello world”,而是丢弃。丢弃协议会丢弃任何接受到的数据不做任何的响应。

要实现丢弃协议,需要做的就是丢弃任何接收到的数据。首先从handler的实现开始,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)
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
  1. DiscardServerHandler继承了ChannelInboundHandlerAdapter,而他又实现了ChannelInboundHandlerChannelInboundHandler提供了不同的事件处理方法,你可以根据需要去覆写相应的方法。ChannelInboundHandlerAdapter提供了一些默认的实现,所以在这个例子中只需要去继承它就可以了。
  2. 覆写了channelRead方法,Netty从客户端收到数据时就会调用该方法。消息的类型是ByteBuf
  3. ByteBuf是一个引用计数对象,需要进行手动的释放。需要注意的是,handler需要释放任何传递给他的引用计数对象。通常情况下channelRead()方法通常的实现方式如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}
  1. 由于IO错误Netty抛出异常或handle处理事件抛出异常,都会使exceptionCaught()方法被调用。在大多数情况下,都需要对异常记日志,并且关闭相关连的channel

到目前为止实现了DISCARD服务的一般,接下来需要实现main()方法来启动服务。

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; /**
* Discards any incoming 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)
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128) // (5)
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6) // Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync(); // (7) // Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
} public static void main(String[] args) throws Exception {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new DiscardServer(port).run();
}
}
  1. NioEventLoopGroup 是一个多线程的事件循环,用来处理I/O操作。Netty为不同的通信方式提供了多种EventLoopGroup实现。在本例中,我们只需要实现服务器端的应用,所以需要两个NioEventLoopGroup 。第一个通常称为boss,用来接收客户端的链接请求。第二个称为worker,用来处理boss已接收连接的I/O请求和把接收的连接注册到worker
  2. ServerBootstrap是用来创建服务器的辅助类。
  3. 使用NioServerSocketChannel类来实例化channel,用来接收连接请求。
  4. 在这里设置的handler会被每一个新channel调用,ChannelInitializer是一个特殊的handler用来配置一个新的channel。在本例中,我们将DiscardServerHandler添加到新channel 的管道中。随着应用程序的复杂度增加,可能会向管道中加入更多的handler。
  5. 可以通过option()方法给channel设置一些参数。
  6. option()方法是用来设置NioServerSocketChannel参数的,而childOption()是给接收的连接设置参数的。
  7. 剩下的就是绑定端口然后启动服务了。

2. 测试DiscardServer是否成功

最简单的方法是使用telnet命令。例如输入telnet localhost 8080。DiscarServer丢弃了任何接受的数据,我们可以把DiscardServer的接收的数据打印出来。

@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.CharsetUtil.US_ASCII))
  2. 等价于in.release()

3.写一个Echo Server

一个服务器通常需要对请求作出响应,而一个Echo服务仅仅需要做的是把请求的内容返回给客户端。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
  1. ChannelHandlerContext对象提供了各种出发IO时间的操作。通过调用write(Object)方法把数据发给客户端。在这里没有手动的释放msg,这是因为当把msg写入时Netty会自动的释放它。
  2. ctx.write(Object)并不会把数据写到外部,而是在内部的缓冲区中,通过调用ctx.flush()把数据刷出到外部。可以简洁的调用ctx.wirteAndFlush(msg)达到同样的效果。

4. 写一个Timer Server

TIME协议与前面的例子不同之处在于,它发送一个32位的整数,不接收任何请求,并且只要消息发送了就立刻关闭连接。

因为我们不需要接收任何数据,而且在连接建立时就发送数据,所以不能使用channelRead()方法。需要覆写channelActive()方法

package io.netty.example.time;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@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. 当一个连接建立时,activeChannel()方法会被调用,然后写一个32位的整数。

  2. 为了发送一个新的信息,需要分配一个缓冲区。通过调用ctx.alloc()获取ByteBufAllocator来分配缓冲区。

  3. 在Netty中的Buffer不需要像Java NIO一样调用flip(),这是因为Netty中的Buffer具有两个指针,分别用于读写操作。当进行写操作时写指针在移动而读指针不移动,读写指针分别代表数据的开始和结束。

    另外需要指出的是,ctx.write()返回一个ChannelFuture对象,该对象代表着一个还未发生的IO操作。这意味着,任何一个请求操作可能都未发生,这是因为在Netty中,所有操作都是异步的。例如下面的代码可能在发送信息前关闭连接:

    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();

    所以要在ChannelFuture完成前调用close(),当操作完成时,ChannelFuture会通知他的监听器。close()可能也不会立即关闭连接。

  4. 本例中添加一个匿名内部类作为监听器,来关闭连接。也可以使用预定义的监听器:

    f.addListener(ChannelFutureListener.CLOSE);

5.Time Client

不同于DISCARD和ECHO,TIME协议需要一个客户端将32位的整数转为一个日期。Netty中的客户端和服务器最大的不同在于使用了不同的BootStrapChannel现实。

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 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();
}
}
}
  1. BootStapServerBootStrap很相似,但它是用于客户端的。
  2. 只需指定一个EventLoopGroup,在客户端中不需要boss。
  3. 使用NioSocketChannel而不是NioServerSocketChannel
  4. 不需要childOption()
  5. 使用connect()方法而不是bind()

TimeClientHandler中,将整数翻译成日期格式的类型。

package io.netty.example.time;

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg; // (1)
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
} finally {
m.release();
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

6.处理基于流的传输问题。

TCP/IP协议接收数据并储存到Socket缓冲区中,但是缓冲区不是数据包的队列,而是字节的队列,这意味着你发送了两条消息,但操作系统会并不认为是两条消息而是一组字节。所以在读数据时并不能确定读到了对方发过来的数据。

在TIME协议中,在调用m.readUnsignedInt()时缓冲区中需要有四个字节,如果缓冲区中还未接收到四个字节时就会抛出异常。

解决方法是,再加一个ChannelHandleChannelPipeline。该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)
}
}
  1. ByteToMessageDecoderChannelInboundHandler的一个实现,专门用于编码问题。
  2. 当新的数据到达时,Netty会调用decode方法,并且其内部维护着一个累加Buffer。
  3. 当累加Buffer中没有足够的数据时,可以不在out中添加任何数据。当新数据到达后Netty又会调用decode方法。
  4. 如果decode()添加一个对象到out中,意味着编码信息成功了。Netty会丢弃Buffer中已读取的部分数据。

TimeDecoder添加到ChannelPipeline中:

b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
}
});

另外一种更简单的方式是使用ReplayingDecoder

public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(
ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(in.readBytes(4));
}
}

当调用in.readBytes(4)抛出异常时,ReplayingDecoder会捕捉异常并重复执行decode()

7.使用POJO代替ByteBuf

在之前的TIME服务中,都是直接使用ByteBuf作为协议的数据结构。在Handler中使用POJO对象,可以把从ByteBuf抽取POJO的代码分离开。

首先定义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对象

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}
out.add(new UnixTime(in.readUnsignedInt()));
}

TimeClientHandler中不再需要使用ByteBuf了。

在服务器端,首先更改TimeServerHandler

@Override
public void channelActive(ChannelHandlerContext ctx) {
ChannelFuture f = ctx.writeAndFlush(new UnixTime());
f.addListener(ChannelFutureListener.CLOSE);
}

还需要创建一个编码器,将UnixTime转为ByteBuf以便网络传输

public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
@Override
protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
out.writeInt((int)msg.value());
}
}

netty用户指南的更多相关文章

  1. Netty权威指南

    Netty权威指南(异步非阻塞通信领域的经典之作,国内首本深入剖析Netty的著作,全面系统讲解原理.实战和源码,带你完美进阶Netty工程师.) 李林锋 著   ISBN 978-7-121-233 ...

  2. 【翻译】Flume 1.8.0 User Guide(用户指南) source

    翻译自官网flume1.8用户指南,原文地址:Flume 1.8.0 User Guide 篇幅限制,分为以下5篇: [翻译]Flume 1.8.0 User Guide(用户指南) [翻译]Flum ...

  3. dubbo用户指南

    用户指南 入门 背景 需求 架构 用法 快速启动 服务提供者 服务消费者 依赖 必需依赖 缺省依赖 可选依赖 成熟度 功能成熟度 策略成熟度 配置 Xml配置 属性配置 注解配置 API配置 示例 启 ...

  4. dubbo用户指南-总结

    dubbo用户指南-总结 入门 背景 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进. 单一应用 ...

  5. flume1.9 用户指南(中文版)

    概述 Apache Flume是一个分布式,可靠且可用的系统,用于有效地从许多不同的source收集,聚合和移动大量日志数据到集中式数据存储. Apache Flume的使用不仅限于日志数据聚合.由于 ...

  6. 重磅!阿里P8费心整理Netty实战+指南+项目白皮书PDF,总计1.08G

    前言 Netty是一款用于快速开发高性能的网络应用程序的Java框架.它封装了网络编程的复杂性,使网络编程和Web技术的最新进展能够被比以往更广泛的开发人员接触到. Netty不只是一个接口和类的集合 ...

  7. Gradle用户指南(1)-Gradle安装

    前置条件 Gradle 需要 Java JDK 或者 JRE,版本是 6 及以上.Gradle 将会装载自己的 Groovy 库,因此,Groovy 不需要被安装.任何存在的 Groovy 安装都会被 ...

  8. Gradle用户指南(章9:Groovy快速入门)

    Gradle用户指南(章9:Groovy快速入门) 你可以使用groovy插件来构建groovy项目.这个插件继承了java插件的功能,且扩展了groovy编译.你的项目可以包含groovy代码.ja ...

  9. Gradle用户指南

    下载安装gradle 2.1 下载地址:http://www.gradle.org/learn 安装先决条件:gradle安装需要1.6或者更高版本的jdk(jre)(可以使用java –versio ...

随机推荐

  1. 一起做RGB-D SLAM(7) (完结篇)

    第七讲 添加回环检测 2016.11 更新 把原文的SIFT替换成了ORB,这样你可以在没有nonfree模块下使用本程序了. 回环检测的阈值作出了相应的调整. 请以现在的github上源码为准. 简 ...

  2. clone一行div tr 每次增量赋值

    $("#add_tan").click(function () { num++; $("tbody tr.tab_xue").eq(0).clone(true) ...

  3. Android 上传文件到XP

    Android部分: AsyncHttpClient client = new AsyncHttpClient(); RequestParams requestParams = new Request ...

  4. struct 和union的区别

    union ( 共用体):构造数据类型,也叫联合体  用途:使几个不同类型的变量共占一段内存(相互覆盖) struct ( 结构体 ):是一种构造类型 用途: 把不同的数据组合成一个整体——自定义数据 ...

  5. PHP(九)数组(2)

  6. 一个GIS系统需具备的功能

    数据的存储(数据结构) 图形显示 功能:拓扑分析 开源的GIS系统:QGIS,SharpMap 各个功能组件

  7. Vivado安装教程

    Vivado的各个版本的安流程其实都差不多,本教程用Vivado2016.4为例进行安装,同样适用于之前和之后的各个版本. 下载好安装包后打开,双击xsetup.exe运行安装程序 弹出的窗口,提示现 ...

  8. Oracle EBS客户化程序中格式化金额

    在Oracle EBS系统中,随处可见金额的显示格式,通常情况下都具有千分位符,同时有一定位数的精度,让我们先来看看一些现成的例子    上面这些列子中的金额都显示了千分位符,同时具备以2位小数,难道 ...

  9. 如何彻底删除TFS的工作项字段

    TFS的工作项字段可以在所有工作项类型之间共享.例如自定义了一个字段"验证迭代"(Mycompany.IterationValidation)那么在需求.Bug中都可以添加这个字段 ...

  10. 在linux中使用包管理器安装node.js

    网上文章中,在linux下安装node.js都是使用源码编译,其实node的github上已经提供了各个系统下使用各自的包管理器(package manager)安装node.js的方法. 1. 在U ...