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. JavaScript 语法总结2

    1. 对象的toString()和valueOf(). - toString() 和Java中的toString() 一样 - valueOf(), 和toString() 都是用来进行类型转换的方法 ...

  2. java如何集成支付宝移动快捷支付功能

    项目需要,需要在客户端集成支付宝接口.第一次集成,过程还是挺简单的,不过由于支付宝官方文档写的不够清晰,也是走了一些弯路,下面把过程写出来分享给大家.就研究了一下:因为使用支付宝接口,就需要到支付宝官 ...

  3. 基于智能手机的3D地图导航

    https://www.gpsworld.com/resources/archives/ Going 3D Personal Nav and LBS To enrich user experience ...

  4. HRBUST1200 装修 2017-03-06 15:41 94人阅读 评论(0) 收藏

    装修 hero为了能顺利娶princess ,花了血本,买了个房子,现在决定装修.房子的长度为n米,宽度为3米,现在我们有2种地砖,规格分别是1米×1米,2米×2米,如果要为该教室铺设地砖,请问有几种 ...

  5. Codeforces766B Mahmoud and a Triangle 2017-02-21 13:47 113人阅读 评论(0) 收藏

    B. Mahmoud and a Triangle time limit per test 2 seconds memory limit per test 256 megabytes input st ...

  6. 企业搜索引擎开发之连接器connector(二十二)

    下面来分析线程执行类,线程池ThreadPool类 对该类的理解需要对java的线程池比较熟悉 该类引用了一个内部类 /** * The lazily constructed LazyThreadPo ...

  7. Linux C 网络编程——3. TCP套接口编程

    1. 基本流程 2. socket() int socket(int domain, int type, int protocol); socket()打开一个网络通讯端口,如果成功的话,就像open ...

  8. 18-11-2 Scrum Meeting 5

    1. 会议照片 2. 工作记录 - 昨天完成工作 1 把数据导入数据库 2 中译英选择题和英译中选择题的查询接口 - 今日计划工作 1 配置页面 2 实现中译英选择题和英译中选择题的查询接口 3 整理 ...

  9. iOS9 Https技术预研

    一.服务器需要做的事情: 1.要注意 App Transport Security 要求 TLS 1.2, 2.而且它要求站点使用支持forward secrecy协议的密码. 3.证书也要求是符合A ...

  10. Reverting back to the R12.1.1 and R12.1.3 Homepage Layout

    Reverting back to the 12.1.1 Homepage Layout Set the following profiles: FND: Applications Navigator ...