第一章 Netty基础
1.Netty结构
Channel:
代表一个到实体设备的连接,执行一个或多个I/O操作
Callback:回调
Netty 内部使用回调处理事件时。一旦这样的回调被触发,事件可以由接口 ChannelHandler 的实现来处理。如下面的代码,一旦一个新的连接建立了,调用 channelActive(),并将打印一条消息。可简单继承ChannelInboundHandlerAdapter类来实现,其中有很多方法可覆盖。
public class ConnectHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { //当建立一个新的连接时调用 ChannelActive()
System.out.println(
"Client " + ctx.channel().remoteAddress() + " connected");
}
}
Future:
除回调外,另外一种通知应用操作已经完成的方式。这个对象作为一个异步操作结果的占位符,它将在将来的某个时候完成并提供结果。ChannelFuture,用于在执行异步操作时使用。
ChannelFuture 提供多个附件方法来允许一个或者多个 ChannelFutureListener 实例。这个回调方法 operationComplete() 会在操作完成时调用。简而言之, ChannelFutureListener 提供的通知机制不需要手动检查操作是否完成的。
每个 Netty 的 outbound I/O 操作都会返回一个 ChannelFuture;这样就不会阻塞。
例子:
Channel channel = ...;
//不会阻塞
ChannelFuture future = channel.connect( //1 异步连接到远程对等节点。调用立即返回并提供 ChannelFuture。
new InetSocketAddress("192.168.0.1", 25));
future.addListener(new ChannelFutureListener() { //2 操作完成后通知注册一个 ChannelFutureListener
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()) { //3 当 operationComplete() 调用时检查操作的状态。
ByteBuf buffer = Unpooled.copiedBuffer(
"Hello", Charset.defaultCharset()); //4 如果成功就创建一个 ByteBuf 来保存数据。
ChannelFuture wf = future.channel().writeAndFlush(buffer); //5 异步发送数据到远程。再次返回ChannelFuture。
// ...业务代码
} else {
Throwable cause = future.cause(); //6 如果有一个错误则抛出 Throwable,描述错误原因。
cause.printStackTrace();
}
}
});
Event 和 Handler
Netty 使用不同的事件来通知我们更改的状态或操作的状态。这使我们能够根据发生的事件触发适当的行为。
ChannelHandler是许多方面的核心
CHANNELPIPELINE
- 其实,ChannelPipeline 就是 ChannelHandler 链的容器,容器中由许多Handler构成1条链。
- ChannelPipeline 提供了一个容器给 ChannelHandler 链并提供了一个API 用于管理沿着链入站和出站事件的流动。每个 Channel 都有自己的ChannelPipeline,当 Channel 创建时自动创建的。 ChannelHandler 是如何安装在 ChannelPipeline? 主要是实现了ChannelHandler 的抽象 ChannelInitializer。ChannelInitializer子类 通过 ServerBootstrap 进行注册。当它的方法 initChannel() 被调用时,这个对象将安装自定义的 ChannelHandler 集到 pipeline。当这个操作完成时,ChannelInitializer 子类则 从 ChannelPipeline 自动删除自身。
- 进站和出站的处理器都可以被安装在相同的 pipeline
包含入站和出站的ChannelInboundHandlers,ChannelInboundHandlers链
ChannelHandlerContext
- 当 ChannelHandler 被添加到的 ChannelPipeline 它得到一个 ChannelHandlerContext,它代表一个 ChannelHandler 和 ChannelPipeline 之间的“绑定”。
- 在 Netty 发送消息可以采用两种方式:直接写消息给 Channel 或者写入 ChannelHandlerContext 对象。
- 这两者主要的区别是, 前一种方法会导致消息从 ChannelPipeline的尾部开始,而后者导致消息从 ChannelPipeline 下一个处理器开始。
详解ChannelHandler
为了能够让开发处理逻辑变得简单,Netty提供了一些默认的处理程序来实现形式的“adapter(适配器)”类
会经常调用的适配器:ChannelHandlerAdapter、ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter、ChannelDuplexHandlerAdapter
- 编码器、解码器
严格地说,其他处理器可以做编码器和解码器能做的事。但正如适配器类简化创建通道处理器,所有的编码器/解码器适配器类 都实现自 ChannelInboundHandler 或 ChannelOutboundHandler。
对于入站数据,channelRead 方法/事件被覆盖。这种方法在每个消息从入站 Channel 读入时调用。该方法将调用特定解码器的“解码”方法,并将解码后的消息转发到管道中下个的 ChannelInboundHandler。
出站消息是类似的。编码器将消息转为字节,转发到下个的 ChannelOutboundHandler。
- SimpleChannelHandler
也许最常见的处理器是接收到解码后的消息并应用一些业务逻辑到这些数据。要创建这样一个 ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler 其中 T 是想要进行处理的类型。这样的处理器,你将覆盖基类的一个或多个方法,将获得被作为输入参数传递所有方法的 ChannelHandlerContext 的引用。
Bootstrapping
Bootstrapping 有以下两种类型:
- 一种是用于客户端的Bootstrap:连接到远程主机和端口,有1个EventLoopGroup
- 一种是用于服务端的ServerBootstrap:绑定本地端口,有2个EventLoopGroup
一个 ServerBootstrap 可以认为有2个 Channel 集合,第一个集合包含一个单例 ServerChannel,代表持有一个绑定了本地端口的 socket;第二集合包含所有创建的 Channel,处理服务器所接收到的客户端进来的连接。
与 ServerChannel 相关 EventLoopGroup 分配一个 EventLoop 是 负责创建 Channels 用于传入的连接请求。一旦连接接受,第二个EventLoopGroup 分配一个 EventLoop 给它的 Channel
2.整合
FUTURE, CALLBACK 和 HANDLER
Netty 的异步编程模型是建立在 future 和 callback 的概念上的。
拦截操作和转换入站或出站数据只需要您提供回调或利用 future 操作返回的。
SELECTOR, EVENT 和 EVENT LOOP
Netty 通过触发事件从应用程序中抽象出 Selector,从而避免手写调度代码。
EventLoop 本身是由只有一个线程驱动,它给一个 Channel 处理所有的 I/O 事件,并且在 EventLoop 的生命周期内不会改变。
这个简单而强大的线程模型消除你可能对你的 ChannelHandler 同步的任何关注
3.建立应用
先启动客户端
然后建立一个连接并发送一个或多个消息发送到服务器,其中每相呼应消息返回给客户端。
Netty 实现的 echo 服务器都需要下面这些:
一个服务器 handler:这个组件实现了服务器的业务逻辑,决定了连接创建后和接收到信息后该如何处理
Bootstrapping: 这个是配置服务器的启动代码。最少需要设置服务器绑定的端口,用来监听连接请求。
3.1通过 ChannelHandler 来实现服务器的逻辑
由于我们的应用很简单,只需要继承 ChannelInboundHandlerAdapter 就行了。这个类 提供了默认 ChannelInboundHandler 的实现,所以只需要覆盖下面的方法:
- channelRead() - 每个信息入站都会调用
- channelReadComplete() - 通知处理器最后的 channelread() 是当前批处理中的最后一条消息时调用
- exceptionCaught()- 读操作时捕获到异常时调用
@Sharable //1 标识这类的实例之间可以在 channel 里面共享
public class EchoServerHandler extends
ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx,
Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8)); //2 日志消息输出到控制台
ctx.write(in); //3 将所接收的消息返回给发送者。注意,这还没有冲刷数据
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)//4 冲刷所有待审消息到远程节点。关闭通道后,操作完成
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace(); //5 打印异常信息
ctx.close(); //6 关闭通道
}
}
3.2引导服务器
- 监听和接收进来的连接请求
- 配置 Channel 来通知一个关于入站消息的 EchoServerHandler 实例
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println(
"Usage: " + EchoServer.class.getSimpleName() +
" <port>");
return;
}
int port = Integer.parseInt(args[0]); //1 设置端口值(抛出一个 NumberFormatException 如果该端口参数的格式不正确)
new EchoServer(port).start(); //2 呼叫服务器的 start() 方法
}
public void start() throws Exception {
NioEventLoopGroup group = new NioEventLoopGroup(); //3 创建 EventLoopGroup
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group) //4 创建 ServerBootstrap
.channel(NioServerSocketChannel.class) //5 指定使用 NIO 的传输 Channel
.localAddress(new InetSocketAddress(port)) //6 设置 socket 地址使用所选的端口
.childHandler(new ChannelInitializer<SocketChannel>() { //7 添加 EchoServerHandler 到 Channel 的 ChannelPipeline
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new EchoServerHandler());
}
});
ChannelFuture f = b.bind().sync(); //8 绑定的服务器;sync 等待服务器关闭
System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
f.channel().closeFuture().sync(); //9 关闭 channel 和 块,直到它被关闭
} finally {
group.shutdownGracefully().sync(); //10 关机的 EventLoopGroup,释放所有资源。
}
}
}
第7步是关键:在这里我们使用一个特殊的类,ChannelInitializer 。当一个新的连接被接受,一个新的子 Channel 将被创建, ChannelInitializer 会添加我们EchoServerHandler 的实例到 Channel 的 ChannelPipeline。正如我们如前所述,如果有入站信息,这个处理器将被通知。
服务器的主代码组件是:
- EchoServerHandler 实现了的业务逻辑
- 在 main() 方法,引导了服务器
执行main()所需的步骤是:
- 创建 ServerBootstrap 实例来引导服务器并随后绑定
- 创建并分配一个 NioEventLoopGroup 实例来处理事件的处理,如接受新的连接和读/写数据。
- 指定本地 InetSocketAddress 给服务器绑定
- 通过 EchoServerHandler 实例给每一个新的 Channel 初始化
- 最后调用 ServerBootstrap.bind() 绑定服务器
写一个 echo 客户端
1.客户端的工作内容:
- 连接服务器
- 发送信息
- 发送的每个信息,等待和接收从服务器返回的同样的信息
- 关闭连接
2.用 ChannelHandler 实现客户端逻辑
2.1我们用 SimpleChannelInboundHandler 来处理所有的任务,需要覆盖三个方法(注意与服务端不同处):
- channelActive() - 服务器的连接被建立后调用
- channelRead0() - 数据后从服务器接收到调用
- exceptionCaught() - 捕获一个异常时调用
@Sharable //1 标记这个类的实例可以在 channel 里共享
public class EchoClientHandler extends
SimpleChannelInboundHandler<ByteBuf> {
/**
* 建立连接后该 channelActive() 方法被调用一次。
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", //2 当被通知该 channel 是活动的时候就发送信息
CharsetUtil.UTF_8));
}
/**
* 这种方法会在接收到数据时被调用。注意,由服务器所发送的消息可以以块的形式被接收。
* 即,当服务器发送 5 个字节是不是保证所有的 5 个字节会立刻收到 - 即使是只有 5 个字节,channelRead0() 方法可被调用两次,第一次用一个ByteBuf(Netty的字节容器)装载3个字节和第二次一个 ByteBuf 装载 2 个字节。唯一要保证的是,该字节将按照它们发送的顺序分别被接收。 (注意,这是真实的,只有面向流的协议如TCP)。
*/
@Override
public void channelRead0(ChannelHandlerContext ctx,
ByteBuf in) {
System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8)); //3 记录接收到的消息
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) { //4 异常处理
cause.printStackTrace();
ctx.close();
}
}
- SimpleChannelInboundHandler vs. ChannelInboundHandler
何时用这两个要看具体业务的需要。在客户端,当 channelRead0() 完成,我们已经拿到的入站的信息。当方法返回时,SimpleChannelInboundHandler 会小心的释放对 ByteBuf(保存信息) 的引用。
而在 EchoServerHandler,我们需要将入站的信息返回给发送者,由于 write() 是异步的,在 channelRead() 返回时,可能还没有完成。所以,我们使用 ChannelInboundHandlerAdapter,无需释放信息。最后在 channelReadComplete() 我们调用 ctxWriteAndFlush() 来释放信息。
2.2用 ChannelHandler 实现客户端逻辑
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); //1 创建 Bootstrap
b.group(group) //2 指定 EventLoopGroup 来处理客户端事件。由于我们使用 NIO 传输,所以用到了 NioEventLoopGroup 的实现
.channel(NioSocketChannel.class) //3 使用的 channel 类型是一个用于 NIO 传输
.remoteAddress(new InetSocketAddress(host, port)) //4 设置服务器的 InetSocketAddress
.handler(new ChannelInitializer<SocketChannel>() { //5 当建立一个连接和一个新的通道时,创建添加到 EchoClientHandler 实例 到 channel pipeline
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new EchoClientHandler());
}
});
ChannelFuture f = b.connect().sync(); //6 连接到远程;等待连接完成
f.channel().closeFuture().sync(); //7 阻塞直到 Channel 关闭
} finally {
group.shutdownGracefully().sync(); //8 调用 shutdownGracefully() 来关闭线程池和释放所有资源
}
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println(
"Usage: " + EchoClient.class.getSimpleName() +
" <host> <port>");
return;
}
final String host = args[0];
final int port = Integer.parseInt(args[1]);
new EchoClient(host, port).start();
}
}
2.3要点回顾
- 创建一个 Bootstrap 来初始化客户端
- 一个 NioEventLoopGroup 实例被分配给处理该事件的处理,这包括创建新的连接和处理入站和出站数据
- 创建一个 InetSocketAddress 以连接到服务器
- 连接好服务器之时,将安装一个 EchoClientHandler 在 pipeline
- 之后 Bootstrap.connect()被调用连接到远程的 - 本例就是 echo(回声)服务器。
Netty应用总结
本章中我们学会了构建并且运行第一个Netty的客户端及服务器。这个应用程序虽然没有多大的难度,但是也不要小瞧它,因为它可以扩展到几千个并发连接。
第二章 Netty核心之Transport(传输)
网络应用程序让人与系统之间可以进行通信,当然网络应用程序也可以将大量的数据从一个地方转移到另一个地方。如何做到这一点取决于具体的网络传输,但转移始终是相同的:字节通过线路。
1.NIO
在低连接数、需要低延迟时、阻塞时使用
2.OIO
NIO-在高连接数时使用
3.Local(本地)
在同一个JVM内通信时使用
4.Embedded(内嵌)
测试ChannelHandler时使用
第三章 Netty核心之Buffer(缓冲)
主要包括:
- ByteBuf
- ByteBufHolder
1.Netty字节数据的容器ByteBuf
ByteBuf 是一个已经经过优化的很好使用的数据容器,字节数据可以有效的被添加到 ByteBuf 中或者也可以从 ByteBuf 中直接获取数据。ByteBuf中有两个索引:一个用来读,一个用来写。
调用 ByteBuf 的以 “read” 或 “write” 开头的任何方法都将自动增加相应的索引。另一方面,“set” 、 "get"操作字节将不会移动索引位置,它们只会在指定的相对位置上操作字节。
ByteBuf 类似于一个字节数组
1.1ByteBuf 使用模式
- HEAP BUFFER(堆缓冲区)
ByteBuf heapBuf = ...;
if (heapBuf.hasArray()) { //1 检查 ByteBuf 是否有支持数组。
byte[] array = heapBuf.array(); //2 *如果有的话,得到引用数组,即是堆缓冲区
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); //3 计算第一字节的偏移量。
int length = heapBuf.readableBytes();//4 获取可读的字节数。
handleArray(array, offset, length); //5 使用数组,偏移量和长度作为调用方法的参数。
}
可以使用 ByteBuf.hasArray()来检查是否支持访问数组
这个用法与 JDK 的 ByteBuffer 类似
- DIRECT BUFFER(直接缓冲区)
“直接缓冲区”是另一个 ByteBuf 模式。对象的所有内存分配发生在 堆,对不对?好吧,并非总是如此。在 JDK1.4 中被引入 NIO 的ByteBuffer 类允许 JVM 通过本地方法调用分配内存,其目的是
通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
但是直接缓冲区的缺点是在内存空间的分配和释放上比堆缓冲区更复杂,另外一个缺点是如果要将数据传递给遗留代码处理,因为数据不是在堆上,你可能不得不作出一个副本
ByteBuf directBuf = ...
if (!directBuf.hasArray()) { //1 *检查 ByteBuf 是不是由数组支持。如果不是,这是一个直接缓冲区。
int length = directBuf.readableBytes();//2 获取可读的字节数
byte[] array = new byte[length]; //3 分配一个新的数组来保存字节
directBuf.getBytes(directBuf.readerIndex(), array); //4 字节复制到数组
handleArray(array, 0, length); //5 将数组,偏移量和长度作为参数调用某些处理方法
}
- COMPOSITE BUFFER(复合缓冲区)
Netty 提供了 ByteBuf 的子类 CompositeByteBuf 类来处理复合缓冲区,CompositeByteBuf 只是一个视图。
- 警告
CompositeByteBuf.hasArray() 总是返回 false,因为它可能既包含堆缓冲区,也包含直接缓冲区
例如,一条消息由 header 和 body 两部分组成,将 header 和 body 组装成一条消息发送出去,可能 body 相同,只是 header 不同
// 使用数组保存消息的各个部分
ByteBuffer[] message = { header, body };
// 使用副本来合并这两个部分
ByteBuffer message2 = ByteBuffer.allocate(
header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();
CompositeByteBuf 的改进版本
CompositeByteBuf messageBuf = ...; // 追加 ByteBuf 实例的 CompositeByteBuf
ByteBuf headerBuf = ...; // 可以支持或直接
ByteBuf bodyBuf = ...; // 可以支持或直接
messageBuf.addComponents(headerBuf, bodyBuf);
// ....
messageBuf.removeComponent(0); // 移除头 //2 删除 索引1的 ByteBuf
for (int i = 0; i < messageBuf.numComponents(); i++) { //3 遍历所有 ByteBuf 实例。
System.out.println(messageBuf.component(i).toString());
}
当作普通数组使用
CompositeByteBuf compBuf = ...;
int length = compBuf.readableBytes(); //1 取得数组长度
byte[] array = new byte[length]; //2 分配数组保存字节
compBuf.getBytes(compBuf.readerIndex(), array); //3 将字节复制到数组
handleArray(array, 0, length); //4 将数组,偏移量和长度作为参数调用某些处理方法
2.Netty字节级别的操作
2.1随机访问索引
- 第一个字节的索引是 0,最后一个字节的索引是 ByteBuf 的 capacity - 1
ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i);
System.out.println((char) b);
}
注意通过索引访问时不会推进 readerIndex (读索引)和 writerIndex(写索引),我们可以通过 ByteBuf 的 readerIndex(index) 或 writerIndex(index) 来分别推进读索引或写索引
flip() 方法来切换读和写模式
ByteBuf 一定符合:0 <= readerIndex <= writerIndex <= capacity。
2.2可丢弃字节的字节
- 标有“可丢弃字节”的段包含已经被读取的字节。他们可以被丢弃,通过调用discardReadBytes() 来回收空间。这个段的初始大小存储在readerIndex,为 0,当“read”操作被执行时递增(“get”操作不会移动 readerIndex)。
2.3可读字节
ByteBuf 的“可读字节”分段存储的是实际数据。
如果所谓的读操作是一个指定 ByteBuf 参数作为写入的对象,并且没有一个目标索引参数,目标缓冲区的 writerIndex 也会增加了。
readBytes(ByteBuf dest);
2.4索引管理
可以设置和重新定位ByteBuf readerIndex 和 writerIndex 通过调用 markReaderIndex(), markWriterIndex(), resetReaderIndex() 和 resetWriterIndex()。这些类似于InputStream 的调用,所不同的是,没有 readlimit 参数来指定当标志变为无效。
也可以通过调用 readerIndex(int) 或 writerIndex(int) 将指标移动到指定的位置。在尝试任何无效位置上设置一个索引将导致 IndexOutOfBoundsException 异常。
调用 clear() 可以同时设置 readerIndex 和 writerIndex 为 0。注意,这不会清除内存中的内容。让我们看看它是如何工作的。
2.5查询操作
最简单的是使用 indexOf() 方法
更复杂的搜索执行以 ByteBufProcessor 为参数的方法
寻找一个回车符,\ r的一个例子
ByteBuf buffer = ...;
int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);
2.6衍生的缓冲区
衍生的缓冲区”是代表一个专门的展示 ByteBuf 内容的“视图”
例
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1创建一个 ByteBuf 保存特定字节串。
ByteBuf sliced = buf.slice(0, 14); //2 创建从索引 0 开始,并在 14 结束的 ByteBuf 的新 slice。
System.out.println(sliced.toString(utf8)); //3 .打印 Netty in Action
buf.setByte(0, (byte) 'J'); //4
assert buf.getByte(0) == sliced.getByte(0);
另一个例子
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1
ByteBuf copy = buf.copy(0, 14); //2
System.out.println(copy.toString(utf8)); //3
buf.setByte(0, (byte) 'J'); //4
assert buf.getByte(0) != copy.getByte(0);
2.6读/写操作:2类
- gget()/set() 操作从给定的索引开始,保持不变
- read()/write() 操作从给定的索引开始,与字节访问的数量来适用,递增当前的写索引或读索引
常见的 get() 操作:
方法名称 | 描述 |
---|---|
getBoolean(int) | 返回当前索引的 Boolean 值 |
getByte(int) getUnsignedByte(int) | 返回当前索引的(无符号)字节 |
getMedium(int) getUnsignedMedium(int) | 返回当前索引的 (无符号) 24-bit 中间值 |
getInt(int) getUnsignedInt(int) | 返回当前索引的(无符号) 整型 |
getLong(int) getUnsignedLong(int) | 返回当前索引的 (无符号) Long 型 |
getShort(int) getUnsignedShort(int) | 返回当前索引的 (无符号) Short 型 |
getBytes(int, …) | 字节 |
常见的set()操作:
方法名称 | 描述 |
---|---|
setBoolean(int, boolean) | 在指定的索引位置设置 Boolean 值 |
setByte(int, int) | 在指定的索引位置设置 byte 值 |
setMedium(int, int) | 在指定的索引位置设置 24-bit 中间 值 |
setInt(int, int) | 在指定的索引位置设置 int 值 |
setLong(int, long) | 在指定的索引位置设置 long 值 |
setShort(int, int) | 在指定的索引位置设置 short 值 |
用法:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1 创建一个新的 ByteBuf 给指定 String 保存字节
System.out.println((char)buf.getByte(0)); //2 打印的第一个字符,
int readerIndex = buf.readerIndex(); //3 存储当前 readerIndex 和 writerIndex
int writerIndex = buf.writerIndex();
buf.setByte(0, (byte)'B'); //4 更新索引 0 的字符
System.out.println((char)buf.getByte(0)); //5 打印出的第一个字符,现在是B
assert readerIndex == buf.readerIndex(); //6 这些断言成功,因为这些操作永远不会改变索引
assert writerIndex == buf.writerIndex();
read() 方法
方法名称 | 描述 |
---|---|
readBoolean() | 读取当前readerIndex处的布尔值,并将readerIndex增加1。 |
readByte() readUnsignedByte() | 读取当前readerIndex处的(无符号)字节值,并将readerIndex增加1。 |
readMedium() readUnsignedMedium() | 在当前readerIndex处读取(无符号)24位中值,并将readerIndex增加3。 |
readInt() readUnsignedInt() | 读取当前readerIndex处的(无符号)int值,并将readerIndex增加4。 |
readLong() readUnsignedLong() | 在当前readerIndex处读取(无符号)long值,并将readerIndex增加8。 |
readShort() readUnsignedShort() | 在当前readerIndex处读取(无符号)short值,并将readerIndex增加2。 |
readBytes(int,int, …) | 将给定对象中给定长度的当前readerIndex上的值读取到给定对象中。也增加了readerIndex的长度。 |
- 每个read() 方法都对应一个write()
方法名称 | 描述 |
---|---|
writeBoolean(boolean) | 在当前writerIndex上写入布尔值,并将writerIndex增加1。 |
writeByte(int) | 将字节值写入当前writerIndex并将writerIndex增加1。 |
writeMedium(int) | 将中间值写到当前writerIndex上,并使writerIndex增加3。 |
writeInt(int) | 将int值写入当前的writerIndex并将writerIndex增加4。 |
writeLong(long) | 将long值写入当前的writerIndex并将writerIndex增加8。 |
writeShort(int) | 在当前writerIndex上写入short值,并将writerIndex增加2。 |
writeBytes(int,…) | 从给定资源中传输当前writerIndex上的字节。 |
用法:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1 创建一个新的 ByteBuf 保存给定 String 的字节。
System.out.println((char)buf.readByte()); //2 打印的第一个字符
int readerIndex = buf.readerIndex(); //3 存储当前的 readerIndex
int writerIndex = buf.writerIndex(); //4 保存当前的 writerIndex
buf.writeByte((byte)'?'); //5 更新索引0的字符 B
assert readerIndex == buf.readerIndex();
assert writerIndex != buf.writerIndex(); // 此断言成功,因为 writeByte() 在 5 移动了 writerIndex
2.7更多操作
方法名称 | 描述 |
---|---|
isReadable() | 如果可以读取至少一个字节,则返回true。 |
isWritable() | 如果可以写入至少一个字节,则返回true。 |
readableBytes() | 返回可以读取的字节数。 |
writablesBytes() | 返回可以写入的字节数。 |
capacity() | 返回ByteBuf可以容纳的字节数。之后,它将尝试再次扩展,直到达到maxCapacity()。 |
maxCapacity() | 返回ByteBuf可以容纳的最大字节数。 |
hasArray() | 如果ByteBuf由字节数组支持,则返回true。 |
array() | 如果ByteBuf由字节数组支持,则返回字节数组,否则抛出UnsupportedOperationException. |
3.ByteBufHolder的使用
我们时不时的会遇到这样的情况:即需要另外存储除有效的实际数据各种属性值。HTTP响应就是一个很好的例子;与内容一起的字节的还有状态码,cookies等
ByteBufHolder 还提供了对于 Netty 的高级功能,如缓冲池
方法名称 | 描述 |
---|---|
data() | 返回 ByteBuf 保存的数据 |
copy() | 制作一个 ByteBufHolder 的拷贝,但不共享其数据(所以数据也是拷贝). |
4.ByteBuf 分配
4.1ByteBufAllocator
为了减少分配和释放内存的开销,Netty 通过支持池类 ByteBufAllocator
方法名称 | 描述 |
---|---|
buffer() buffer(int) buffer(int, int) | 返回具有基于堆或直接数据存储的ByteBuf。 |
heapBuffer() heapBuffer(int) heapBuffer(int, int) | 返回具有基于堆的存储的ByteBuf。 |
directBuffer() directBuffer(int) directBuffer(int, int) | 返回具有直接存储的ByteBuf。 |
compositeBuffer() compositeBuffer(int) heapCompositeBuffer() heapCompositeBuffer(int) directCompositeBuffer()directCompositeBuffer(int) | 返回一个CompositeByteBuf,可以通过添加基于堆的缓冲区或直接缓冲区来对其进行扩展。 |
ioBuffer() | 返回一个ByteBuf,它将用于套接字上的I / O操作。 |
一些方法接受整型参数允许用户指定 ByteBuf 的初始和最大容量值
你可以得到从 Channel (在理论上,每 Channel 可具有不同的 ByteBufAllocator ),或通过绑定到的 ChannelHandler 的 ChannelHandlerContext 得到它,用它实现了你数据处理逻辑。
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc(); //1 从 channel 获得 ByteBufAllocator
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc(); //2 从 ChannelHandlerContext 获得 ByteBufAllocator
...
2种实现
- PooledByteBufAllocator Netty 默认使用 PooledByteBufAllocator
- 其他的实现
4.2ByteBufUtil
当未引用 ByteBufAllocator 时,上面的方法无法访问到 ByteBuf。对于这个用例 Netty 提供一个实用工具类称为 Unpooled,,它提供了静态辅助方法来创建非池化的 ByteBuf 实例
方法名称 | 描述 |
---|---|
buffer() buffer(int) buffer(int, int) | 返回具有基于堆的存储的未池化ByteBuf |
directBuffer() directBuffer(int) directBuffer(int, int) | 返回具有直接存储的未池化ByteBuf |
wrappedBuffer() | 返回一个ByteBuf,它包装给定的数据。 |
copiedBuffer() | 返回一个ByteBuf,它将复制给定的数据 |
在 非联网项目,该 Unpooled 类也使得它更容易使用的 ByteBuf API,获得一个高性能的可扩展缓冲 API,而不需要 Netty 的其他部分的。
5.引用计数器
在Netty 4中为 ByteBuf 和 ByteBufHolder(两者都实现了 ReferenceCounted 接口)引入了引用计数器。
它能够在特定的对象上跟踪引用的数目,实现了ReferenceCounted 的类的实例会通常开始于一个活动的引用计数器为 1。而如果对象活动的引用计数器大于0,就会被保证不被释放。当数量引用减少到0,将释放该实例。
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc(); //1 从 channel 获取 ByteBufAllocator
....
ByteBuf buffer = allocator.directBuffer(); //2 从 ByteBufAllocator 分配一个 ByteBuf
assert buffer.refCnt() == 1; //3 检查引用计数器是否是 1
boolean released = buffer.release(); //4 release()将会递减对象引用的数目。当这个引用计数达到0时,对象已被释放,并且该方法返回 true。
...
第四章 ChannelHandler 和 ChannelPipeline
1.ChannelHandler 家族
1.1 生命周期(顺序如下)
- Channel
状态 | 描述 |
---|---|
1 channelUnregistered | channel已创建但未注册到一个 EventLoop. |
2 channelRegistered | channel 注册到一个 EventLoop. |
3 channelActive | channel 变为活跃状态(连接到了远程主机),现在可以接收和发送数据了 |
4 channelInactive | channel 处于非活跃状态,没有连接到远程主机 |
- ChannelHandler
当 ChannelHandler 添加到 ChannelPipeline,或者从 ChannelPipeline 移除后,对应的方法将会被调用。每个方法都传入了一个 ChannelHandlerContext 参数
状态 | 描述 |
---|---|
handlerAdded | 当 ChannelHandler 添加到 ChannelPipeline 调用 |
handlerRemoved | 当 ChannelHandler 从 ChannelPipeline 移除时调用 |
exceptionCaught | 当 ChannelPipeline 执行抛出异常时调用 |
1.2 ChannelHandler 子接口
- ChannelInboundHandler - 处理进站数据和所有状态更改事件
- ChannelOutboundHandler - 处理出站数据,允许拦截各种操作
ChannelHandler 适配器
Netty 提供了一个简单的 ChannelHandler 框架实现,给所有声明方法签名。这个类 ChannelHandlerAdapter 的方法,主要推送事件 到 pipeline 下个 ChannelHandler 直到 pipeline 的结束。这个类 也作为 ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter 的基础。所有三个适配器类的目的是作为自己的实现的起点;您可以扩展它们,覆盖你需要自定义的方法。
1.3 ChannelInboundHandler
状态 | 描述 |
---|---|
channelRegistered | 当通道注册到其EventLoop并且能够处理I / O时调用。 |
channelUnregistered | 当通道从其EventLoop注销时调用,并且无法处理任何I / O。 |
channelActive | 当通道处于活动状态时调用;通道已连接/绑定并准备就绪。 |
channelInactive | 当Channel保持活动状态并且不再连接到其远程对等方时调用。 |
channelReadComplete | 在通道上的读取操作完成时调用。 |
channelRead | 如果从通道读取数据,则调用。 |
channelWritabilityChanged | 当Channel的可写状态更改时调用。用户可以确保写操作不会太快(有OutOfMemoryError的风险),或者可以在Channel再次变为可写状态时恢复写操作。Channel.isWritable()可用于检测通道的实际可写性。可写性阈值可以通过Channel.config()。setWriteHighWaterMark()和Channel.config()。setWriteLowWaterMark()设置。 |
userEventTriggered(…) | 当用户调用Channel.fireUserEventTriggered(…)以将pojo通过ChannelPipeline传递时调用。这可用于通过ChannelPipeline传递用户特定的事件,从而允许处理这些事件。 |
ChannelInboundHandler 实现覆盖了 channelRead() 方法处理进来的数据用来响应释放资源。Netty 在 ByteBuf 上使用了资源池,所以当执行释放资源时可以减少内存的消耗。
@ChannelHandler.Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter { //1 扩展(继承) ChannelInboundHandlerAdapter
@Override
public void channelRead(ChannelHandlerContext ctx,
Object msg) {
ReferenceCountUtil.release(msg); //2 ReferenceCountUtil.release() 来丢弃收到的信息
}
}
Netty 用一个 WARN-level 日志条目记录未释放的资源,使其能相当简单地找到代码中的违规实例。然而,由于手工管理资源会很繁琐,您可以通过使用 SimpleChannelInboundHandler 简化问题。如下:
@ChannelHandler.Sharable
public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> { //1 扩展 SimpleChannelInboundHandler
@Override
public void channelRead0(ChannelHandlerContext ctx,
Object msg) {
// No need to do anything special //2 不需做特别的释放资源的动作
}
}
1.4 ChannelOutboundHandler
ChannelOutboundHandler 提供了出站操作时调用的方法。这些方法会被 Channel, ChannelPipeline, 和 ChannelHandlerContext 调用。
ChannelOutboundHandler 另个一个强大的方面是它具有在请求时延迟操作或者事件的能力。比如,当你在写数据到 remote peer 的过程中被意外暂停,你可以延迟执行刷新操作,然后在迟些时候继续。
下面显示了 ChannelOutboundHandler 的方法(继承自 ChannelHandler 未列出来)
状态 | 描述 |
---|---|
bind | 根据要求调用以将通道绑定到本地地址 |
connect | 根据要求调用以将通道连接到远程对等方 |
disconnect | 根据请求调用,以断开通道与远程对等方的连接 |
close | 根据要求调用以关闭渠道 |
deregister | 根据请求调用以从其EventLoop中注销该Channel |
read | 根据要求调用以从Channel读取更多数据 |
flush | 根据请求调用以将排队的数据通过Channel刷新到远程对等方 |
write | 根据请求调用,以通过Channel将数据写入远程对等方 |
几乎所有的方法都将 ChannelPromise(通道承诺) 作为参数,一旦请求结束要通过 ChannelPipeline 转发的时候,必须通知此参数。
ChannelPromise 是 特殊的 ChannelFuture(通道将来),允许你的 ChannelPromise 及其 操作 成功或失败。所以任何时候调用例如 Channel.write(…) 一个新的 ChannelPromise将会创建并且通过 ChannelPipeline传递。这次写操作本身将会返回 ChannelFuture, 这样只允许你得到一次操作完成的通知。Netty 本身使用 ChannelPromise 作为返回的 ChannelFuture 的通知,事实上在大多数时候就是 ChannelPromise 自身(ChannelPromise 扩展了 ChannelFuture)
- ChannelOutboundHandlerAdapter 提供了一个实现了 ChannelOutboundHandler 所有基本方法的实现的框架
1.5 资源管理
当你通过 ChannelInboundHandler.channelRead(…) 或者 ChannelOutboundHandler.write(…) 来处理数据,重要的是在处理资源时要确保资源不要泄漏。
为了让用户更加简单的找到遗漏的释放,Netty 包含了一个 ResourceLeakDetector ,将会从已分配的缓冲区 1% 作为样品来检查是否存在在应用程序泄漏。因为 1% 的抽样,开销很小。
对于检测泄漏,您将看到类似于下面的日志消息。
LEAK: ByteBuf.release() was not called before it’s garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced
leak reporting, specify the JVM option ’-Dio.netty.leakDetectionLevel=advanced’ or call ResourceLeakDetector.setLevel()
Relaunch your application with the JVM option mentioned above, then you’ll see the recent locations of your application where the leaked buffer was accessed. The following output shows a leak from our unit test (XmlFrameDecoderTest.testDecodeWithXml()):
Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK:
ByteBuf.release() was not called before it’s garbage-collected.
Recent access records: 1
#1:
io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
1.6 泄漏检测等级
Netty 现在定义了四种泄漏检测等级,可以按需开启,见下表
等级说明 | 描述 |
---|---|
Disables | 完全泄漏检测。尽管这甚至消除了1%的开销,但您仅应在进行大量测试后再进行此操作。 |
SIMPLE | 告知是否发现泄漏。再次使用1%的采样率,默认级别和大多数情况下的合适值。 |
ADVANCED | (高级)使用1%的采样率告诉是否发现泄漏以及在何处访问该消息。 |
PARANOID | 与“高级”级别相同,主要区别在于每个访问都经过采样。这会对性能产生巨大影响。仅在调试阶段使用它。 |
修改检测等级,只需修改 io.netty.leakDetectionLevel 系统属性,举例
# java -Dio.netty.leakDetectionLevel=paranoid
当你处理 channelRead(…) 操作,并在消费消息(不是通过 ChannelHandlerContext.fireChannelRead(…) 来传递它到下个 ChannelInboundHandler) 时,要释放它,如下:
@ChannelHandler.Sharable
public class DiscardInboundHandler extends ChannelInboundHandlerAdapter { //1 继承 ChannelInboundHandlerAdapter
@Override
public void channelRead(ChannelHandlerContext ctx,
Object msg) {
ReferenceCountUtil.release(msg); //2 用 ReferenceCountUtil.release(...) 来释放资源
}
}
SimpleChannelInboundHandler -消费入站消息更容易
@ChannelHandler.Sharable
public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter { //1 继承 ChannelOutboundHandlerAdapter
@Override
public void write(ChannelHandlerContext ctx,
Object msg, ChannelPromise promise) {
ReferenceCountUtil.release(msg); //2 使用 ReferenceCountUtil.release(...) 来释放资源
promise.setSuccess(); //3 通知 ChannelPromise 数据已经被处理
}
重要的是,释放资源并通知 ChannelPromise。如果,ChannelPromise 没有被通知到,这可能会引发 ChannelFutureListener 不会被处理的消息通知的状况。
2.ChannelPipeline(ChannelHandler链)
- ChannelPipeline
ChannelPipeline 是一系列的ChannelHandler 实例,流经一个 Channel 的入站和出站事件可以被ChannelPipeline 拦截,ChannelPipeline能够让用户自己对入站/出站事件的处理逻辑,以及pipeline里的各个Handler之间的交互进行定义。
每当一个新的Channel被创建了,都会建立一个新的 ChannelPipeline,并且这个新的 ChannelPipeline 还会绑定到Channel上。这个关联是永久性的;Channel 既不能附上另一个 ChannelPipeline 也不能分离当前这个。这些都由Netty负责完成,,而无需开发人员的特别处理。
- ChannelHandlerContext
一个 ChannelHandlerContext 使 ChannelHandler 与 ChannelPipeline 和 其他处理程序交互。一个处理程序可以通知下一个 ChannelPipeline 中的 ChannelHandler 甚至动态修改 ChannelPipeline 的归属。
- ChannelPipeline 相对论
随着管道传播事件,它决定下个 ChannelHandler 是否是相匹配的方向运动的类型。如果没有,ChannelPipeline 跳过 ChannelHandler 并继续下一个合适的方向。记住,一个处理程序可能同时实现ChannelInboundHandler 和 ChannelOutboundHandler 接口。
- 修改 ChannelPipeline
ChannelHandler 可以实时修改 ChannelPipeline 的布局,通过添加、移除、替换其他 ChannelHandler(也可以从 ChannelPipeline 移除 ChannelHandler 自身)。这个 是 ChannelHandler 重要的功能之一。
名称 | 描述 |
---|---|
addFirst addBefore addAfter addLast | 添加 ChannelHandler 到 ChannelPipeline. |
Remove | 从 ChannelPipeline 移除 ChannelHandler. |
Replace | 在 ChannelPipeline 替换另外一个 ChannelHandler |
操作示例:
ChannelPipeline pipeline = null; // get reference to pipeline;
FirstHandler firstHandler = new FirstHandler(); //1 创建一个 FirstHandler 实例
pipeline.addLast("handler1", firstHandler); //2 添加该实例作为 "handler1" 到 ChannelPipeline
pipeline.addFirst("handler2", new SecondHandler()); //3 添加 SecondHandler 实例作为 "handler2" 到 ChannelPipeline 的第一个槽,这意味着它将替换之前已经存在的 "handler1"
pipeline.addLast("handler3", new ThirdHandler()); //4 添加 ThirdHandler 实例作为"handler3" 到 ChannelPipeline 的最后一个槽
pipeline.remove("handler3"); //5 通过名称移除 "handler3"
pipeline.remove(firstHandler); //6 通过引用移除 FirstHandler (因为只有一个,所以可以不用关联名字 "handler1").
pipeline.replace("handler2", "handler4", new ForthHandler()); //6 将作为"handler2"的 SecondHandler 实例替换为作为 "handler4"的 FourthHandler
- ChannelHandler 执行 ChannelPipeline 和阻塞
通常每个 ChannelHandler 添加到 ChannelPipeline 将处理事件 传递到 EventLoop( I/O 的线程)。至关重要的是不要阻塞这个线程, 它将会负面影响的整体处理I/O。 有时可能需要使用阻塞 api 接口来处理遗留代码。对于这个情况下,ChannelPipeline 已有 add() 方法,它接受一个EventExecutorGroup。如果一个定制的 EventExecutorGroup 传入事件将由含在这个 EventExecutorGroup 中的 EventExecutor之一来处理,并且从 Channel 的 EventLoop 本身离开。一个默认实现,称为来自 Netty 的 DefaultEventExecutorGroup
除了上述操作,其他访问 ChannelHandler 的方法如下:
名称 | 描述 |
---|---|
get(…) | 按类型或名称返回ChannelHandler |
context(…) | 返回绑定到ChannelHandler的ChannelHandlerContext。 |
names() iterator() | 返回ChannelPipeline中所有ChannelHander的名称或名称。 |
- 发送事件
ChannelPipeline API 有额外调用入站和出站操作的方法。下表列出了入站操作,用于通知 ChannelPipeline 中 ChannelInboundHandlers 正在发生的事件
名称 | 描述 |
---|---|
fireChannelRegistered | 在ChannelPipeline中的下一个ChannelInboundHandler上调用channelRegistered(ChannelHandlerContext)。 |
fireChannelUnregistered | 在ChannelPipeline中的下一个ChannelInboundHandler上调用channelUnregistered(ChannelHandlerContext)。 |
fireChannelActive | 在ChannelPipeline中的下一个ChannelInboundHandler上调用channelActive(ChannelHandlerContext)。 |
fireChannelInactive | 在ChannelPipeline中的下一个ChannelInboundHandler上调用channelInactive(ChannelHandlerContext)。 |
fireExceptionCaught | 在ChannelPipeline中的下一个ChannelHandler上调用exceptionCaught(ChannelHandlerContext,Throwable)。 |
fireUserEventTriggered | 在ChannelPipeline中的下一个ChannelInboundHandler上调用userEventTriggered(ChannelHandlerContext,Object)。 |
fireChannelRead | 在ChannelPipeline中的下一个ChannelInboundHandler上调用channelRead(ChannelHandlerContext,Object msg)。 |
fireChannelReadComplete | 在ChannelPipeline中的下一个ChannelStateHandler上调用channelReadComplete(ChannelHandlerContext)。 |
在出站方面,处理一个事件将导致底层套接字的一些行动。下表列出了ChannelPipeline API 出站的操作。
名称 | 描述 |
---|---|
bind | 将频道绑定到本地地址。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用bind(ChannelHandlerContext,SocketAddress,ChannelPromise)。 |
connect | 将通道连接到远程地址。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用connect(ChannelHandlerContext,SocketAddress,ChannelPromise)。 |
disconnect | 断开通道。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用Disconnect(ChannelHandlerContext,ChannelPromise)。 |
close | 关闭通道。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用close(ChannelHandlerContext,ChannelPromise)。 |
deregister | 从先前分配的EventExecutor(EventLoop)中注销通道。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用deregister(ChannelHandlerContext,ChannelPromise)。 |
flush | 刷新所有未完成的通道写入。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用flush(ChannelHandlerContext)。 |
write | 向频道写一条消息。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用write(ChannelHandlerContext,Object msg,ChannelPromise)。注意:这不会将消息写入底层套接字,而只是将其排队。要将其写入Socket,请调用flush()或writeAndFlush()。 |
writeAndFlush | 调用write()然后flush()的便捷方法。 |
read | 请求从Channel读取更多数据。这将在ChannelPipeline中的下一个ChannelOutboundHandler上调用read(ChannelHandlerContext)。 |
- 总结下:
一个 ChannelPipeline 是用来保存关联到一个 Channel 的ChannelHandler
可以修改 ChannelPipeline 通过动态添加和删除 ChannelHandler
ChannelPipeline 有着丰富的API调用动作来回应入站和出站事件。
第五章 Codec 框架
- Decoder(解码器)
- Encoder(编码器)
- Codec(编解码器)
解码器负责将消息从字节或其他序列形式转成指定的消息对象,编码器的功能则相反;解码器负责处理“入站”数据,编码器负责处理“出站”数据。
1.Netty提供的Decoder(解码器)
- 解码字节到消息(ByteToMessageDecoder 和 ReplayingDecoder)
- 解码消息到消息(MessageToMessageDecoder)
Netty的解码器是一种 ChannelInboundHandler 的抽象实现
1.1 ByteToMessageDecoder
2个最重要的方法
方法名称 | 描述 |
---|---|
Decode | 这是您需要实现的唯一抽象方法。通过具有输入字节的ByteBuf和添加了解码消息的List来调用它。重复调用encode(),直到列表返回时为空。然后将List的内容传递到管道中的下一个处理程序。 |
decodeLast | 所提供的默认实现只调用了decode()。当Channel变为非活动状态时,此方法被调用一次。提供特殊的替代 |
示例:每次从入站的 ByteBuf 读取四个字节,解码成整形,并添加到一个 List (本例是指 Integer),当不能再添加数据到 list 时,它所包含的内容就会被发送到下个 ChannelInboundHandler
public class ToIntegerDecoder extends ByteToMessageDecoder { //1 现继承了 ByteToMessageDecode 用于将字节解码为消息
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
if (in.readableBytes() >= 4) { //2 检查可读的字节是否至少有4个 ( int 是4个字节长度)
out.add(in.readInt()); //3 从入站 ByteBuf 读取 int , 添加到解码消息的 List 中
}
}
}
1.2 ReplayingDecoder
ReplayingDecoder 是 byte-to-message 解码的一种特殊的抽象基类,读取缓冲区的数据之前需要检查缓冲区是否有足够的字节(上例注释2),使用ReplayingDecoder就无需自己检查;若ByteBuf中有足够的字节,则会正常读取;若没有足够的字节则会停止解码。
ReplayingDecoder 继承自 ByteToMessageDecoder ,是对其的包装
- 不是所有的标准 ByteBuf 操作都被支持,如果调用一个不支持的操作会抛出 UnreplayableOperationException
- ReplayingDecoder 略慢于 ByteToMessageDecoder
示例:
public class ToIntegerDecoder2 extends ReplayingDecoder<Void> { //1 实现继承自 ReplayingDecoder 用于将字节解码为消息
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
out.add(in.readInt()); //2 从入站 ByteBuf 读取整型,并添加到解码消息的 List 中
}
}
1.3 更多 Decoder
- io.netty.handler.codec.LineBasedFrameDecoder 通过结束控制符("\n" 或 “\r\n”).解析入站数据。
- io.netty.handler.codec.http.HttpObjectDecoder 用于 HTTP 数据解码
方法名称 | 描述 |
---|---|
Decode | 解码是您需要实现的唯一抽象方法。每个入站消息都将被解码为另一种格式。然后,已解码的消息将传递到管道中的下一个ChannelInboundHandler。 |
decodeLast | 所提供的默认实现只调用了decode()。当Channel变为非活动状态时,此方法被调用一次。提供特殊的替代 |
将 Integer 转为 String,我们提供了 IntegerToStringDecoder,继承自 MessageToMessageDecoder。
因为这是一个参数化的类,实现的签名是:
public class IntegerToStringDecoder extends MessageToMessageDecoder<Integer>
decode() 方法的签名是
protected void decode( ChannelHandlerContext ctx,Integer msg, List<Object> out ) throws Exception
也就是说,入站消息是按照在类定义中声明的参数类型(这里是 Integer) 而不是 ByteBuf来解析的。在之前的例子,解码消息(这里是String)将被添加到List,并传递到下个 ChannelInboundHandler。
public class IntegerToStringDecoder extends
MessageToMessageDecoder<Integer> { //1 实现继承自 MessageToMessageDecoder
@Override
public void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out)
throws Exception {
out.add(String.valueOf(msg)); //2 通过 String.valueOf() 转换 Integer 消息字符串
}
}
- 更复杂 的 MessageToMessageEncoder 应用案例,可以查看 io.netty.handler.codec.protobuf 包下的 ProtobufEncoder
2.Netty的Encoder(编码器)
- 编码从消息到字节
- 编码从消息到消息
2.1 MessageToByteEncoder
方法名称 | 描述 |
---|---|
encode | 编码方法是您需要实现的唯一抽象方法。它将与出站消息一起调用,该消息将被编码为ByteBuf。然后,将ByteBuf转发到ChannelPipeline中的下一个ChannelOutboundHandler。 |
下面示例,我们想产生 Short 值,并想将他们编码成 ByteBuf 来发送到 线上,我们提供了 ShortToByteEncoder 来实现该目的。
public class ShortToByteEncoder extends MessageToByteEncoder<Short> { //1 实现继承自 MessageToByteEncoder
@Override
public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out)
throws Exception {
out.writeShort(msg); //2 写 Short 到 ByteBuf
}
}
Netty 提供很多 MessageToByteEncoder 类来帮助你的实现自己的 encoder 。其中 WebSocket08FrameEncoder 就是个不错的范例。可以在 io.netty.handler.codec.http.websocketx 包找到。
2.2 MessageToMessageEncoder
方法名称 | 描述 |
---|---|
encode | 编码方法是您需要实现的唯一抽象方法。对于使用write(…)编写的每条消息,都会调用该消息,以将消息编码为一个或多个新的出站消息。编码后的消息然后被转发 |
示例:encoder 从出站字节流提取 Integer,以 String 形式传递给ChannelPipeline 中的下一个 ChannelOutboundHandler
public class IntegerToStringEncoder extends
MessageToMessageEncoder<Integer> { //1 实现继承自 MessageToMessageEncoder
@Override
public void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out)
throws Exception {
out.add(String.valueOf(msg)); //2 转 Integer 为 String,并添加到 MessageBuf
}
}
更复杂 的 MessageToMessageEncoder 应用案例,可以查看 io.netty.handler.codec.protobuf 包下的 ProtobufEncoder
3.Netty抽象 Codec(编解码器)类
我们在讨论解码器和编码器的时候,都是把它们当成不同的实体的,但是有时候如果在同一个类中同时放入入站和出站的数据和信息转换的话,发现会更加实用。
3.1 ByteToMessageCodec
方法名称 | 描述 |
---|---|
decode | 只要可以使用字节,就会调用此方法。它将入站ByteBuf转换为指定的消息格式,并将其转发到管道中的下一个ChannelInboundHandler。 |
decodeLast | 此方法的默认实现委托decode()。当通道变为非活动状态时,仅被调用一次。对于特殊处理,可以将其包裹起来。 |
encode | 对于通过ChannelPipeline写入的每条消息,都会调用此方法。编码的消息包含在ByteBuf中, |
3.2 MessageToMessageCodec
方法名称 | 描述 |
---|---|
decode | 该方法与编解码器的入站消息一起调用,并将它们解码为消息。这些消息将转发到ChannelPipeline中的下一个ChannelInboundHandler |
decodeLast | 默认实现委托decode()。decodeLast只会被调用一次,即通道变为非活动状态。如果您需要在此处进行特殊处理,则可以重写encodeLast()来实现它。 |
encode | 对于每个要通过ChannelPipeline移动的出站消息,调用encode方法。编码的消息被转发到管道中的下一个ChannelOutboundHandler |
protected abstract void encode(ChannelHandlerContext ctx,OUTBOUND msg, List<Object> out)
protected abstract void decode(ChannelHandlerContext ctx,INBOUND msg, List<Object> out)
示例:继承了参数为 WebSocketFrame(类型为 INBOUND)和 WebSocketFrame(类型为 OUTBOUND)的 MessageToMessageCode
public class WebSocketConvertHandler extends MessageToMessageCodec<WebSocketFrame, WebSocketConvertHandler.WebSocketFrame> { //1 编码 WebSocketFrame 消息转为 WebSocketFrame 消息
public static final WebSocketConvertHandler INSTANCE = new WebSocketConvertHandler();
@Override
protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
ByteBuf payload = msg.getData().duplicate().retain();
switch (msg.getType()) { //2 测 WebSocketFrame 的 FrameType 类型,并且创建一个新的响应的 FrameType 类型的 WebSocketFrame
case BINARY:
out.add(new BinaryWebSocketFrame(payload));
break;
case TEXT:
out.add(new TextWebSocketFrame(payload));
break;
case CLOSE:
out.add(new CloseWebSocketFrame(true, 0, payload));
break;
case CONTINUATION:
out.add(new ContinuationWebSocketFrame(payload));
break;
case PONG:
out.add(new PongWebSocketFrame(payload));
break;
case PING:
out.add(new PingWebSocketFrame(payload));
break;
default:
throw new IllegalStateException("Unsupported websocket msg " + msg);
}
}
@Override
protected void decode(ChannelHandlerContext ctx, io.netty.handler.codec.http.websocketx.WebSocketFrame msg, List<Object> out) throws Exception {
if (msg instanceof BinaryWebSocketFrame) { //3 通过 instanceof 来检测正确的 FrameType
out.add(new WebSocketFrame(WebSocketFrame.FrameType.BINARY, msg.content().copy()));
} else if (msg instanceof CloseWebSocketFrame) {
out.add(new WebSocketFrame(WebSocketFrame.FrameType.CLOSE, msg.content().copy()));
} else if (msg instanceof PingWebSocketFrame) {
out.add(new WebSocketFrame(WebSocketFrame.FrameType.PING, msg.content().copy()));
} else if (msg instanceof PongWebSocketFrame) {
out.add(new WebSocketFrame(WebSocketFrame.FrameType.PONG, msg.content().copy()));
} else if (msg instanceof TextWebSocketFrame) {
out.add(new WebSocketFrame(WebSocketFrame.FrameType.TEXT, msg.content().copy()));
} else if (msg instanceof ContinuationWebSocketFrame) {
out.add(new WebSocketFrame(WebSocketFrame.FrameType.CONTINUATION, msg.content().copy()));
} else {
throw new IllegalStateException("Unsupported websocket msg " + msg);
}
}
public static final class WebSocketFrame { //4 自定义消息类型 WebSocketFrame
public enum FrameType { //5 枚举类明确了 WebSocketFrame 的类型
BINARY,
CLOSE,
PING,
PONG,
TEXT,
CONTINUATION
}
private final FrameType type;
private final ByteBuf data;
public WebSocketFrame(FrameType type, ByteBuf data) {
this.type = type;
this.data = data;
}
public FrameType getType() {
return type;
}
public ByteBuf getData() {
return data;
}
}
}
3.3 CombinedChannelDuplexHandler
如前所述,结合解码器和编码器在一起可能会牺牲可重用性。为了避免这种方式,并且部署一个解码器和编码器到 ChannelPipeline 作为逻辑单元而不失便利性。
public class CombinedChannelDuplexHandler<I extends ChannelInboundHandler,O extends ChannelOutboundHandler>
这个类是扩展 ChannelInboundHandler 和 ChannelOutboundHandler 参数化的类型。这提供了一个容器,单独的解码器和编码器类合作而无需直接扩展抽象的编解码器类。
public class ByteToCharDecoder extends
ByteToMessageDecoder { //1 继承 ByteToMessageDecoder
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
if (in.readableBytes() >= 2) { //2 写 char 到 MessageBuf
out.add(in.readChar());
}
}
}
现在我们有编码器和解码器,将他们组成一个编解码器。
// 1.CombinedByteCharCodec 的参数是解码器和编码器的实现用于处理进站字节和出站消息
public class CombinedByteCharCodec extends CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
public CombinedByteCharCodec() {
// 2.传递 ByteToCharDecoder 和 CharToByteEncoder 实例到 super 构造函数来委托调用使他们结合起来。
super(new ByteToCharDecoder(), new CharToByteEncoder());
}
}
第六章 Netty已经提供的 ChannelHandler 和 Codec(略)
- 使用 SSL/TLS 加密 Netty 程序
- 构建 Netty HTTP/HTTPS 程序
- 处理空闲连接和超时
- 解码分隔符和基于长度的协议
- 写大数据
- 序列化数据
第七章 Netty引导(Bootstrapping)
- 引导客户端和服务器
- 从Channel引导客户端
- 添加 ChannelHandler
- 使用 ChannelOption 和属性
到目前为止我们使用这个词有点模糊,时间可以来定义它。在最简单的条件下,引导就是配置应用程序的过程。但正如我们看到的,不仅仅如此;Netty 的引导客户端和服务器的类从网络基础设施使您的应用程序代码在后台可以连接和启动所有的组件。
1.Bootstrap 类型
Netty中的引导类型有两种:服务器、客户端
,“服务器”应用程序把一个“父”管道接受连接和创建“子”管道,
“客户端”很可能只需要一个单一的、非“父”对所有网络交互的管道(对于无连接的比如 UDP 协议也是一样)。
2.Netty引导客户端和无连接协议
2.1 客户端引导方法
方法名称 | 描述 |
---|---|
group | 设置 EventLoopGroup 用于处理所有的 Channel 的事件 |
channel channelFactory | channel() 指定 Channel 的实现类。如果类没有提供一个默认的构造函数,你可以调用 channelFactory() 来指定一个工厂类被 bind() 调用。 |
localAddress | 指定应该绑定到本地地址 Channel。如果不提供,将由操作系统创建一个随机的。或者,您可以使用 bind() 或 connect()指定localAddress |
option | 设置 ChannelOption 应用于 新创建 Channel 的 ChannelConfig。这些选项将被 bind 或 connect 设置在通道,这取决于哪个被首先调用。这个方法在创建管道后没有影响。所支持 ChannelOption 取决于使用的管道类型。请参考9.6节和 ChannelConfig 的 API 文档 的 Channel 类型使用。 |
attr | 这些选项将被 bind 或 connect 设置在通道,这取决于哪个被首先调用。这个方法在创建管道后没有影响。请参考9.6节。 |
handler | 设置添加到 ChannelPipeline 中的 ChannelHandler 接收事件通知。 |
clone | 创建一个当前 Bootstrap的克隆拥有原来相同的设置。 |
remoteAddress | 设置远程地址。此外,您可以通过 connect() 指定 |
connect | 连接到远端,返回一个 ChannelFuture, 用于通知连接操作完成 |
bind | 将通道绑定并返回一个 ChannelFuture,用于通知绑定操作完成后,必须调用 Channel.connect() 来建立连接。 |
2.2 如何引导客户端
- 1.当 bind() 调用时,Bootstrap 将创建一个新的管道, 当 connect() 调用在 Channel 来建立连接
- 2.Bootstrap 将创建一个新的管道, 当 connect() 调用时
- 3.新的 Channel
示例:
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap(); //1 创建一个新的 Bootstrap 来创建和连接到新的客户端管道
bootstrap.group(group) //2 指定 EventLoopGroup
.channel(NioSocketChannel.class) //3 指定 Channel 实现来使用
.handler(new SimpleChannelInboundHandler<ByteBuf>() { //4 设置处理器给 Channel 的事件和数据
@Override
protected void channeRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
System.out.println("Received data");
byteBuf.clear();
}
});
ChannelFuture future = bootstrap.connect(
new InetSocketAddress("www.manning.com", 80)); //5 连接到远端主机
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture)
throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Connection established");
} else {
System.err.println("Connection attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
2.3 兼容性
Channel 和 EventLoopGroup 的 EventLoop 必须相容。相兼容的实现一般在同一个包下面,例如使用NioEventLoop,NioEventLoopGroup 和 NioServerSocketChannel 在一起
记住,EventLoop 分配给该 Channel 负责处理 Channel 的所有操作。当你执行一个方法,该方法返回一个 ChannelFuture ,它将在 分配给 Channel 的 EventLoop 执行。
示例:使用一个 Channel 类型与一个 EventLoopGroup 兼容。
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap(); //1 创建新的 Bootstrap 来创建新的客户端管道
bootstrap.group(group) //2 注册 EventLoopGroup 用于获取 EventLoop
.channel(OioSocketChannel.class) //3 指定要使用的 Channel 类。通知我们使用 NIO 版本用于 EventLoopGroup , OIO 用于 Channel
.handler(new SimpleChannelInboundHandler<ByteBuf>() { //4 设置处理器用于管道的 I/O 事件和数据
@Override
protected void channelRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
System.out.println("Reveived data");
byteBuf.clear();
}
});
ChannelFuture future = bootstrap.connect(
new InetSocketAddress("www.manning.com", 80)); //5 尝试连接到远端。当 NioEventLoopGroup 和 OioSocketChannel 不兼容时,会抛出 IllegalStateException 异常
future.syncUninterruptibly();
3.Netty引导服务器
3.1 引导服务器的方法
方法名称 | 描述 |
---|---|
group | 设置 EventLoopGroup 用于 ServerBootstrap。这个 EventLoopGroup 提供 ServerChannel 的 I/O 并且接收 Channel |
channel channelFactory | channel() 指定 Channel 的实现类。如果管道没有提供一个默认的构造函数,你可以提供一个 ChannelFactory。 |
localAddress | 指定 ServerChannel 实例化的类。如果不提供,将由操作系统创建一个随机的。或者,您可以使用 bind() 或 connect()指定localAddress |
option | 指定一个 ChannelOption 来用于新创建的 ServerChannel 的 ChannelConfig 。这些选项将被设置在管道的 bind() 或 connect(),这取决于谁首先被调用。在此调用这些方法之后设置或更改 ChannelOption 是无效的。所支持 ChannelOption 取决于使用的管道类型。请参考9.6节和 ChannelConfig 的 API 文档 的 Channel 类型使用。 |
childOption | 当管道已被接受,指定一个 ChannelOption 应用于 Channel 的 ChannelConfig。 |
attr | 指定 ServerChannel 的属性。这些属性可以被 管道的 bind() 设置。当调用 bind() 之后,修改它们不会生效。 |
childAttr | 应用属性到接收到的管道上。后续调用没有效果。 |
handler | 设置添加到 ServerChannel 的 ChannelPipeline 中的 ChannelHandler。 具体详见 childHandler() 描述 |
childHandler | 设置添加到接收到的 Channel 的 ChannelPipeline 中的 ChannelHandler。handler() 和 childHandler()之间的区别是前者是接 |
3.2 如何引导一个服务器
- 当调用 bind() 后 ServerBootstrap 将创建一个新的管道,这个管道将会在绑定成功后接收子管道
- 接收新连接给每个子管道
- 接收连接的 Channel
示例:
NioEventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap(); //1 创建要给新的 ServerBootstrap 来创建新的 SocketChannel 管道并绑定他们
bootstrap.group(group) //2 定 EventLoopGroup 用于从注册的 ServerChannel 中获取EventLoop 和接收到的管道
.channel(NioServerSocketChannel.class) //3 指定要使用的管道类
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() { //4 设置子处理器用于处理接收的管道的 I/O 和数据
@Override
protected void channelRead0(ChannelHandlerContext ctx,
ByteBuf byteBuf) throws Exception {
System.out.println("Reveived data");
byteBuf.clear();
}
}
);
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080)); //5 通过配置引导来绑定管道
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture)
throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Server bound");
} else {
System.err.println("Bound attempt failed");
channelFuture.cause().printStackTrace();
}
}
}
);
4.从 Channel 引导客户端
引导客户端 Channel 从另一个 Channel的情况
由于 EventLoop 继承自 EventLoopGroup ,您可以通过传递 接收到的 Channel 的 EventLoop 到 Bootstrap 的 group() 方法。这允许客户端 Channel 来操作 相同的 EventLoop,这样就能消除了额外的线程创建和所有相关的上下文切换的开销。
- bind() 调用时,ServerBootstrap 创建一个新的ServerChannel 。 当绑定成功后,这个管道就能接收子管道了
- ServerChannel 接收新连接并且创建子管道来服务它们
- Channel 用于接收到的连接
- 管道自己创建了 Bootstrap,用于当 connect() 调用时创建一个新的管道
- 新管道连接到远端
- 在 EventLoop 接收通过 connect() 创建后就在管道间共享
示例:
ServerBootstrap bootstrap = new ServerBootstrap(); //1 创建一个新的 ServerBootstrap 来创建新的 SocketChannel 管道并且绑定他们
bootstrap.group(new NioEventLoopGroup(), //2 指定 EventLoopGroups 从 ServerChannel 和接收到的管道来注册并获取 EventLoops
new NioEventLoopGroup()).channel(NioServerSocketChannel.class) //3 指定 Channel 类来使用
.childHandler( //4 设置处理器用于处理接收到的管道的 I/O 和数据
new SimpleChannelInboundHandler<ByteBuf>() {
ChannelFuture connectFuture;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Bootstrap bootstrap = new Bootstrap();//5 创建一个新的 Bootstrap 来连接到远程主机
bootstrap.channel(NioSocketChannel.class) //6 设置管道类
.handler(new SimpleChannelInboundHandler<ByteBuf>() { //7 设置处理器来处理 I/O
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
System.out.println("Reveived data");
}
});
bootstrap.group(ctx.channel().eventLoop()); //8 ***使用相同的 EventLoop 作为分配到接收的管道
connectFuture = bootstrap.connect(new InetSocketAddress("www.manning.com", 80)); //9 连接到远端
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
if (connectFuture.isDone()) {
// do something with the data //10 连接完成处理业务逻辑 (比如, proxy)
}
}
});
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080)); //11 通过配置了的 Bootstrap 来绑定到管道
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("Server bound");
} else {
System.err.println("Bound attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
5.在一个引导中添加多个 ChannelHandler
Netty 通过添加多个 ChannelHandler,从而使每个 ChannelHandler 分工明确,结构清晰
Netty 提供 ChannelInitializer 抽象类用来初始化 ChannelPipeline 中的 ChannelHandler
ServerBootstrap bootstrap = new ServerBootstrap();//1 创建一个新的 ServerBootstrap 来创建和绑定新的 Channel
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup()) //2 指定 EventLoopGroups 从 ServerChannel 和接收到的管道来注册并获取 EventLoops
.channel(NioServerSocketChannel.class) //3 指定 Channel 类来使用
.childHandler(new ChannelInitializerImpl()); //4 设置处理器用于处理接收到的管道的 I/O 和数据
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080)); //5 通过配置的引导来绑定管道
future.sync();
final class ChannelInitializerImpl extends ChannelInitializer<Channel> { //6 ChannelInitializer 负责设置 ChannelPipeline
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline(); //7 实现 initChannel() 来添加需要的处理器到 ChannelPipeline。一旦完成了这方法 ChannelInitializer 将会从 ChannelPipeline 删除自身
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
}
}
通过 ChannelInitializer, Netty 允许你添加你程序所需的多个 ChannelHandler 到 ChannelPipeline
6.使用Netty 的 ChannelOption 和属性
如果每次创建通道后都不得不手动配置每个通道,这样会很麻烦,所幸,Netty提供了 ChannelOption 来帮助引导配置。这些选项都会自动的应用到引导创建的所有通道中去,可用的各种选项可以配置底层连接的详细信息,如通道“keep-alive(保持活跃)”或“timeout(超时)”的特性。
示例:如何使用 ChannelOption 配置 Channel 和一个属性来存储一个整数值。
final AttributeKey<Integer> id = new AttributeKey<Integer>("ID"); //1 新建一个 AttributeKey 用来存储属性值
Bootstrap bootstrap = new Bootstrap(); //2 新建 Bootstrap 用来创建客户端管道并连接他们
bootstrap.group(new NioEventLoopGroup()) //3 指定 EventLoopGroups 从和接收到的管道来注册并获取 EventLoop
.channel(NioSocketChannel.class) //4 指定 Channel 类
.handler(new SimpleChannelInboundHandler<ByteBuf>() { //5 设置处理器来处理管道的 I/O 和数据
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
Integer idValue = ctx.channel().attr(id).get(); //6 检索 AttributeKey 的属性及其值
// do something with the idValue
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("Reveived data");
}
});
bootstrap.option(ChannelOption.SO_KEEPALIVE, true).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000); //7 设置 ChannelOption 将会设置在管道在连接或者绑定
bootstrap.attr(id, 123456); //8 存储 id 属性
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.manning.com", 80)); //9 通过配置的 Bootstrap 来连接到远程主机
future.syncUninterruptibly();
7.关闭Netty之前已经引导的客户端或服务器
主要是记住关闭 EventLoopGroup,将处理任何悬而未决的事件和任务并随后释放所有活动线程。这只是一种叫EventLoopGroup.shutdownGracefully()。这个调用将返回一个 Future 用来通知关闭完成。注意,shutdownGracefully()也是一个异步操作,所以你需要阻塞,直到它完成或注册一个侦听器直到返回的 Future 来通知完成。
EventLoopGroup group = new NioEventLoopGroup() //1 创建 EventLoopGroup 用于处理 I/O
Bootstrap bootstrap = new Bootstrap(); //2 创建一个新的 Bootstrap 并且配置他
bootstrap.group(group)
.channel(NioSocketChannel.class);
...
...
Future<?> future = group.shutdownGracefully(); //3 终优雅的关闭 EventLoopGroup 释放资源。这个也会关闭中当前使用的 Channel
// block until the group has shutdown
future.sync();
或者,您可以调用 Channel.close() 显式地在所有活动管道之前调用EventLoopGroup.shutdownGracefully()。但是在所有情况下,记得关闭EventLoopGroup 本身
第八章 NETTY 实例
8.1 Netty单元测试
8.1.1 Netty单元测试总览
Netty 的促进 ChannelHandler 的测试使用的是的所谓“嵌入式”传输。这是由一个特殊 Channel 实现,EmbeddedChannel提供了一个简单的方法通过管道传递事件。
这种想法很简单:你把入站或者出站的数据写入一个EmbeddedChannel 然后检查是否能够达到 ChannelPipeline 的结束。以此来确定消息编码或解码和 ChannelHandler 是否操作被触发。
名称 | 职责 |
---|---|
writeInbound | 写一个入站消息到 EmbeddedChannel。 如果数据能从 EmbeddedChannel 通过 readInbound() 读到,则返回 true |
readInbound | 从 EmbeddedChannel 读到入站消息。任何返回遍历整个ChannelPipeline。如果读取还没有准备,则此方法返回 null |
writeOutbound | 写一个出站消息到 EmbeddedChannel。 如果数据能从 EmbeddedChannel 通过 readOutbound() 读到,则返回 true |
readOutbound | 从 EmbeddedChannel 读到出站消息。任何返回遍历整个ChannelPipeline。如果读取还没有准备,则此方法返回 null |
Finish | 如果从入站或者出站中能读到数据,标记 EmbeddedChannel 完成并且返回。这同时会调用 EmbeddedChannel 的关闭方法 |
8.1.2 如何测试 ChannelHandler
测试入站消息
public class FixedLengthFrameDecoder extends ByteToMessageDecoder { //1 继承 ByteToMessageDecoder 用来处理入站的字节并将他们解码为消息
private final int frameLength;
public FixedLengthFrameDecoder(int frameLength) { //2 指定产出的帧的长度
if (frameLength <= 0) {
throw new IllegalArgumentException(
"frameLength must be a positive integer: " + frameLength);
}
this.frameLength = frameLength;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= frameLength) { //3 检查是否有足够的字节用于读到下个帧
ByteBuf buf = in.readBytes(frameLength);//4 从 ByteBuf 读取新帧
out.add(buf); //5 添加帧到解码好的消息 List
}
}
}
下面是单元测试的例子,使用 EmbeddedChannel
public class FixedLengthFrameDecoderTest {
@Test //1 测试增加 @Test 注解
public void testFramesDecoded() {
ByteBuf buf = Unpooled.buffer(); //2 新建 ByteBuf 并用字节填充它
for (int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3)); //3 新增 EmbeddedChannel 并添加 FixedLengthFrameDecoder 用于测试
Assert.assertFalse(channel.writeInbound(input.readBytes(2))); //4 写数据到 EmbeddedChannel
Assert.assertTrue(channel.writeInbound(input.readBytes(7)));
Assert.assertTrue(channel.finish()); //5 标记 channel 已经完成
ByteBuf read = (ByteBuf) channel.readInbound(); // 6 读产生的消息并且校验
Assert.assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
Assert.assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
Assert.assertEquals(buf.readSlice(3), read);
read.release();
Assert.assertNull(channel.readInbound());
buf.release();
}
@Test
public void testFramesDecoded2() {
ByteBuf buf = Unpooled.buffer();
for (int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
Assert.assertFalse(channel.writeInbound(input.readBytes(2)));
Assert.assertTrue(channel.writeInbound(input.readBytes(7)));
Assert.assertTrue(channel.finish());
ByteBuf read = (ByteBuf) channel.readInbound();
Assert.assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
Assert.assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
Assert.assertEquals(buf.readSlice(3), read);
read.release();
Assert.assertNull(channel.readInbound());
buf.release();
}
}
测试出站消息
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> { //1 继承 MessageToMessageEncoder 用于编码消息到另外一种格式
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
while (in.readableBytes() >= 4) { //2 检查是否有足够的字节用于编码
int value = Math.abs(in.readInt());//3 读取下一个输入 ByteBuf 产出的 int 值,并计算绝对值
out.add(value); //4 写 int 到编码的消息 List
}
}
}
测试代码:
public class AbsIntegerEncoderTest {
@Test //1 用 @Test 标记
public void testEncoded() {
ByteBuf buf = Unpooled.buffer(); //2 新建 ByteBuf 并写入负整数
for (int i = 1; i < 10; i++) {
buf.writeInt(i * -1);
}
EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder()); //3 新建 EmbeddedChannel 并安装 AbsIntegerEncoder 来测试
Assert.assertTrue(channel.writeOutbound(buf)); //4 写 ByteBuf 并预测 readOutbound() 产生的数据
Assert.assertTrue(channel.finish()); //5 标记 channel 已经完成
for (int i = 1; i < 10; i++) {
Assert.assertEquals(i, channel.readOutbound()); //6 读取产生到的消息,检查负值已经编码为绝对值
}
Assert.assertNull(channel.readOutbound());
}
}
8.1.3 测试异常处理
示例:一旦输入的字节数超过了限制长度,TooLongFrameException 就会被抛出,用这样的功能来防止资源耗尽的问题
public class FrameChunkDecoder extends ByteToMessageDecoder { //1 继承 ByteToMessageDecoder 用于解码入站字节到消息
private final int maxFrameSize;
public FrameChunkDecoder(int maxFrameSize) {
this.maxFrameSize = maxFrameSize;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int readableBytes = in.readableBytes(); //2 指定最大需要的帧产生的体积
if (readableBytes > maxFrameSize) {
// discard the bytes //3 如果帧太大就丢弃并抛出一个 TooLongFrameException 异常
in.clear();
throw new TooLongFrameException();
}
ByteBuf buf = in.readBytes(readableBytes); //4 同时从 ByteBuf 读到新帧
out.add(buf); //5 添加帧到解码消息 List
}
}
测试代码:
public class FrameChunkDecoderTest {
@Test //1 使用 @Test 注解
public void testFramesDecoded() {
ByteBuf buf = Unpooled.buffer(); //2 新建 ByteBuf 写入 9 个字节
for (int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3)); //3 新建 EmbeddedChannel 并安装一个 FixedLengthFrameDecoder 用于测试
Assert.assertTrue(channel.writeInbound(input.readBytes(2))); //4 写入 2 个字节并预测生产的新帧(消息)
try {
channel.writeInbound(input.readBytes(4)); //5 写一帧大于帧的最大容量 (3) 并检查一个 TooLongFrameException 异常
Assert.fail(); //6 如果异常没有被捕获,测试将失败。注意如果类实现 exceptionCaught() 并且处理了异常 exception,那么这里就不会捕捉异常
} catch (TooLongFrameException e) {
// expected
}
Assert.assertTrue(channel.writeInbound(input.readBytes(3))); //7 写剩余的 2 个字节预测一个帧
Assert.assertTrue(channel.finish()); //8 标记 channel 完成
ByteBuf read = (ByteBuf) channel.readInbound();
Assert.assertEquals(buf.readSlice(2), read); //9 读到的产生的消息并且验证值。注意 assertEquals(Object,Object)测试使用 equals() 是否相当,不是对象的引用是否相当
read.release();
read = (ByteBuf) channel.readInbound();
Assert.assertEquals(buf.skipBytes(4).readSlice(3), read);
read.release();
buf.release();
}
}
8.2 实现WebSocket聊天功能
8.2.1 WebSocket 程序示例
- 1客户端/用户连接到服务器,并且是聊天的一部分
- 2聊天消息通过 WebSocket 进行交换
- 3消息双向发送
- 4服务器处理所有的客户端/用户
逻辑:
1.客户端发送一个消息。
2.消息被广播到所有其他连接的客户端。
8.2.2 添加 WebSocket 支持
一种被称作“Upgrade handshake(升级握手)”的机制能够将标准的HTTP或者HTTPS协议转成 WebSocket。
在我们的应用中,要想升级协议为 WebSocket,只有当 URL 请求以“/ws”结束时才可以,如果没有达到该要求,服务器仍将使用基本的 HTTP/S,一旦连接升级,之后的数据传输都将使用 WebSocket 。
- 1客户端/用户连接到服务器并加入聊天
- 2 HTTP 请求页面或 WebSocket 升级握手
- 3服务器处理所有客户端/用户
- 4响应 URI “/”的请求,转到 index.html
- 5如果访问的是 URI“/ws” ,处理 WebSocket 升级握手
- 6升级握手完成后 ,通过 WebSocket 发送聊天消息
8.2.2.1示例:处理 HTTP 请求
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> { //1 扩展 SimpleChannelInboundHandler 用于处理 FullHttpRequest信息
private final String wsUri;
private static final File INDEX;
static {
URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
try {
String path = location.toURI() + "index.html";
path = !path.contains("file:") ? path : path.substring(5);
INDEX = new File(path);
} catch (URISyntaxException e) {
throw new IllegalStateException("Unable to locate index.html", e);
}
}
public HttpRequestHandler(String wsUri) {
this.wsUri = wsUri;
}
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (wsUri.equalsIgnoreCase(request.getUri())) {
ctx.fireChannelRead(request.retain()); //2 如果请求是一次升级了的 WebSocket 请求,则递增引用计数器(retain)并且将它传递给在 ChannelPipeline 中的下个 ChannelInboundHandler
} else {
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(ctx); //3 处理符合 HTTP 1.1的 "100 Continue" 请求
}
RandomAccessFile file = new RandomAccessFile(INDEX, "r");//4 读取 index.html
HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8");
boolean keepAlive = HttpHeaders.isKeepAlive(request);
if (keepAlive) { //5 判断 keepalive 是否在请求头里面
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, file.length());
response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ctx.write(response); //6 写 HttpResponse 到客户端
if (ctx.pipeline().get(SslHandler.class) == null) { //7 写 index.html 到客户端,根据 ChannelPipeline 中是否有 SslHandler 来决定使用 DefaultFileRegion 还是 ChunkedNioFile
ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
} else {
ctx.write(new ChunkedNioFile(file.getChannel()));
}
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); //8 写并刷新 LastHttpContent 到客户端,标记响应完成
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE); //9 如果 请求头中不包含 keepalive,当写完成时,关闭 Channel
}
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
HttpRequestHandler 做了下面几件事:
- 如果该 HTTP 请求被发送到URI “/ws”,则调用 FullHttpRequest 上的 retain(),并通过调用 fireChannelRead(msg) 转发到下一个 ChannelInboundHandler。retain() 的调用是必要的,因为 channelRead() 完成后,它会调用 FullHttpRequest 上的 release() 来释放其资源。 (请参考我们先前在第6章中关于 SimpleChannelInboundHandler 的讨论)
- 如果客户端发送的 HTTP 1.1 头是“Expect: 100-continue” ,则发送“100 Continue”的响应。
- 在 头被设置后,写一个 HttpResponse 返回给客户端。注意,这不是 FullHttpResponse,这只是响应的第一部分。另外,这里我们也不使用 writeAndFlush(), 这个是在留在最后完成。
- 如果传输过程既没有要求加密也没有要求压缩,那么把 index.html 的内容存储在一个 DefaultFileRegion 里就可以达到最好的效率。这将利用零拷贝来执行传输。出于这个原因,我们要检查 ChannelPipeline 中是否有一个 SslHandler。如果是的话,我们就使用 ChunkedNioFile。
- 写 LastHttpContent 来标记响应的结束,并终止它
- 如果不要求 keepalive ,添加 ChannelFutureListener 到 ChannelFuture 对象的最后写入,并关闭连接。注意,这里我们调用 writeAndFlush() 来刷新所有以前写的信息。
8.2.2.2 处理 WebSocket frame(帧)
WebSocket “Request for Comments” (RFC) 定义了六种不同的 frame; Netty 给他们每个都提供了一个 POJO 实现 ,见下表:
名称 | 描述 |
---|---|
BinaryWebSocketFrame | 包含二进制数据 |
TextWebSocketFrame | 包含文字数据 |
ContinuationWebSocketFrame | 包含属于先前BinaryWebSocketFrame或TextWebSocketFrame的文本或二进制数据 |
CloseWebSocketFrame | 表示关闭请求,并包含关闭状态代码和短语 |
PingWebSocketFrame | 请求传输PongWebSocketFrame |
PongWebSocketFrame | 作为对PingWebSocketFrame的响应发送 |
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { //1 扩展 SimpleChannelInboundHandler 用于处理 TextWebSocketFrame 信息
private final ChannelGroup group;
public TextWebSocketFrameHandler(ChannelGroup group) {
this.group = group;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { //2 覆写userEventTriggered() 方法来处理自定义事件
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
ctx.pipeline().remove(HttpRequestHandler.class); //3 如果接收的事件表明握手成功,就从 ChannelPipeline 中删除HttpRequestHandler ,因为接下来不会接受 HTTP 消息了
group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));//4 写一条消息给所有的已连接 WebSocket 客户端,通知它们建立了一个新的 Channel 连接
group.add(ctx.channel()); //5 添加新连接的 WebSocket Channel 到 ChannelGroup 中,这样它就能收到所有的信息
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
group.writeAndFlush(msg.retain()); //6 保留收到的消息,并通过 writeAndFlush() 传递给所有连接的客户端。
}
}
上面显示了 TextWebSocketFrameHandler 仅作了几件事:
- 当WebSocket 与新客户端已成功握手完成,通过写入信息到 ChannelGroup 中的 Channel 来通知所有连接的客户端,然后添加新 Channel 到 ChannelGroup
- 如果接收到 TextWebSocketFrame,调用 retain() ,并将其写、刷新到 ChannelGroup,使所有连接的 WebSocket Channel 都能接收到它。和以前一样,retain() 是必需的,因为当 channelRead0()返回时,TextWebSocketFrame 的引用计数将递减。由于所有操作都是异步的,writeAndFlush() 可能会在以后完成,我们不希望它访问无效的引用。
8.2.2.3 初始化 ChannelPipeline
接下来,我们需要安装我们上面实现的两个 ChannelHandler 到 ChannelPipeline。为此,我们需要继承 ChannelInitializer 并且实现 initChannel()。看下面 ChatServerInitializer 的代码实现
public class ChatServerInitializer extends ChannelInitializer<Channel> { //1 扩展 ChannelInitializer
private final ChannelGroup group;
public ChatServerInitializer(ChannelGroup group) {
this.group = group;
}
@Override
protected void initChannel(Channel ch) throws Exception { //2 添加 ChannelHandler 到 ChannelPipeline
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpRequestHandler("/ws"));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TextWebSocketFrameHandler(group));
}
}
initChannel() 方法用于设置所有新注册的 Channel 的ChannelPipeline,安装所有需要的 ChannelHandler。总结如下:
ChannelHandler | 职责 |
---|---|
HttpServerCodec | 将字节解码为HttpRequest,HttpContent,LastHttpContent.Encoding HttpRequest,HttpContent,LastHttpContent为字节。 |
ChunkedWriteHandler | 写入文件的内容 |
HttpObjectAggregator | 该ChannelHandler将HttpMessage及其后续的HttpContents聚合为单个FullHttpRequest或FullHttpResponse(取决于它是用于处理请求还是响应)。安装此程序后,管道中的下一个ChannelHandler将仅接收完整的HTTP请求。 |
HttpRequestHandler | 处理FullHttpRequests(未发送到“ / ws” URI的那些) |
WebSocketServerProtocolHandler | 根据WebSockets规范的要求,处理WebSocket升级握手,PingWebSocketFrames,PongWebSocketFrames和CloseWebSocketFrames |
TextWebSocketFrameHandler | 处理TextWebSocketFrames和握手完成事件 |
该 WebSocketServerProtocolHandler 处理所有规定的 WebSocket 帧类型和升级握手本身。如果握手成功所需的 ChannelHandler 被添加到管道,而那些不再需要的则被去除。管道升级之前的状态如下图。这代表了 ChannelPipeline 刚刚经过 ChatServerInitializer 初始化。
握手升级成功后 WebSocketServerProtocolHandler 替换HttpRequestDecoder 为 WebSocketFrameDecoder,HttpResponseEncoder 为WebSocketFrameEncoder。 为了最大化性能,WebSocket 连接不需要的 ChannelHandler 将会被移除。其中就包括了 HttpObjectAggregator 和 HttpRequestHandler
下图,展示了 ChannelPipeline 经过这个操作完成后的情况。注意 Netty 目前支持四个版本 WebSocket 协议,每个通过其自身的方式实现类。选择正确的版本WebSocketFrameDecoder 和 WebSocketFrameEncoder 是自动进行的,这取决于在客户端(在这里指浏览器)的支持(在这个例子中,我们假设使用版本是 13 的 WebSocket 协议,从而图中显示的是 WebSocketFrameDecoder13 和 WebSocketFrameEncoder13)
8.2.2.4 引导
public class ChatServer {
private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);//1 创建 DefaultChannelGroup 用来 保存所有连接的的 WebSocket channel
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel;
public ChannelFuture start(InetSocketAddress address) {
ServerBootstrap bootstrap = new ServerBootstrap(); //2 引导 服务器
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(createInitializer(channelGroup));
ChannelFuture future = bootstrap.bind(address);
future.syncUninterruptibly();
channel = future.channel();
return future;
}
protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) { //3 创建 ChannelInitializer
return new ChatServerInitializer(group);
}
public void destroy() { //4 处理服务器关闭,包括释放所有资源
if (channel != null) {
channel.close();
}
channelGroup.close();
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception{
if (args.length != 1) {
System.err.println("Please give port as argument");
System.exit(1);
}
int port = Integer.parseInt(args[0]);
final ChatServer endpoint = new ChatServer();
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
endpoint.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}
8.2.3 测试及加密
测试 (略)
https://www.w3cschool.cn/essential_netty_in_action/essential_netty_in_action-iqbn28e1.html
加密
只需要向 ChannelPipeline 中添加 SslHandler ,然后配置一下即可
public class SecureChatServerIntializer extends ChatServerInitializer { //1 扩展 ChatServerInitializer 来实现加密
private final SslContext context;
public SecureChatServerIntializer(ChannelGroup group, SslContext context) {
super(group);
this.context = context;
}
@Override
protected void initChannel(Channel ch) throws Exception {
super.initChannel(ch);
SSLEngine engine = context.newEngine(ch.alloc());
engine.setUseClientMode(false);
ch.pipeline().addFirst(new SslHandler(engine)); //2 向 ChannelPipeline 中添加SslHandler
}
}
最后修改 ChatServer,使用 SecureChatServerInitializer 并传入 SSLContext
public class SecureChatServer extends ChatServer {//1 扩展 ChatServer
private final SslContext context;
public SecureChatServer(SslContext context) {
this.context = context;
}
@Override
protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
return new SecureChatServerIntializer(group, context); //2 返回先前创建的 SecureChatServerInitializer 来启用加密
}
public static void main(String[] args) throws Exception{
if (args.length != 1) {
System.err.println("Please give port as argument");
System.exit(1);
}
int port = Integer.parseInt(args[0]);
SelfSignedCertificate cert = new SelfSignedCertificate();
SslContext context = SslContext.newServerContext(cert.certificate(), cert.privateKey());
final SecureChatServer endpoint = new SecureChatServer(context);
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
endpoint.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}
8.3 SPDY
SPDY 总览
ChannelHandler, Decoder, 和 Encoder
引导一个基于 Netty 的应用
测试 SPDY/HTTPS
SPDY(读作“speedy”)是一个谷歌开发的开放的网络协议,主要运用于 web 内容传输。SPDY 操纵 HTTP 流量,目标是减少 web 页面加载延迟,提高网络安全。
8.3.1 Netty中的SPDY实现
SPDY 使用 TLS 的扩展称为 Next Protocol Negotiation (NPN)。在Java 中,我们有两种不同的方式选择的基于 NPN 的协议:
- 使用 ssl_npn,NPN 的开源 SSL 提供者。
- 使用通过 Jetty 的 NPN 扩展库。
在这个例子中使用 Jetty 库。如果你想使用 ssl_npn,请参阅https://github.com/benmmurphy/ssl_npn项目文档
8.3.1.1 集成 Next Protocol Negotiation
public class DefaultServerProvider implements NextProtoNego.ServerProvider {
private static final List<String> PROTOCOLS =
Collections.unmodifiableList(Arrays.asList("spdy/2", "spdy/3", "http/1.1")); //1 定义所有的 ServerProvider(服务器提供者) 实现的协议
private String protocol;
@Override
public void unsupported() {
protocol = "http/1.1"; //2 设置如果 SPDY 协议失败了就转到 http/1.1
}
@Override
public List<String> protocols() {
return PROTOCOLS; //3 返回支持的协议的列表
}
@Override
public void protocolSelected(String protocol) {
this.protocol = protocol; //4 设置选择的协议
}
public String getSelectedProtocol() {
return protocol; //5 返回选择的协议
}
}
第一个 ChannelInboundHandler 是用于不支持 SPDY 的情况下处理客户端 HTTP 请求,如果不支持 SPDY 就回滚使用默认的 HTTP 协议。
8.3.1.2 实现各种 ChannelHandler
@ChannelHandler.Sharable
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { //1 重写 channelRead0() ,可以被所有的接收到的 FullHttpRequest 调用
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(ctx); //2 查如果接下来的响应是预期的,就写入
}
FullHttpResponse response = new DefaultFullHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK); //3 新建 FullHttpResponse,用于对请求的响应
response.content().writeBytes(getContent().getBytes(CharsetUtil.UTF_8)); //4 生成响应的内容,将它写入 payload
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8"); //5 设置头文件,这样客户端就能知道如何与 响应的 payload 交互
boolean keepAlive = HttpHeaders.isKeepAlive(request);
if (keepAlive) { //6 检查请求设置是否启用了 keepalive;如果是这样,将标题设置为符合HTTP RFC
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ChannelFuture future = ctx.writeAndFlush(response); //7 写响应给客户端,并获取到 Future 的引用,用于写完成时,获取到通知
if (!keepAlive) {
future.addListener (ChannelFutureListener.CLOSE); //8 如果响应不是 keepalive,在写完成时关闭连接
}
}
protected String getContent() { //9 返回内容作为响应的 payload
return "This content is transmitted via HTTP\r\n";
}
private static void send100Continue(ChannelHandlerContext ctx) { //10 Helper 方法生成了100 持续的响应,并写回给客户端
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception { //11 若执行阶段抛出异常,则关闭管道
cause.printStackTrace();
ctx.close();
}
}
HttpRequestHandler 虽然是我们可以重用代码,我们将改变我们的内容写回客户端只是强调协议变化;通常您会返回相同的内容。下面的清单展示了实现,它扩展了先前的 HttpRequestHandler。
@ChannelHandler.Sharable
public class SpdyRequestHandler extends HttpRequestHandler { //1 继承 HttpRequestHandler 这样就能共享相同的逻辑
@Override
protected String getContent() {
return "This content is transmitted via SPDY\r\n"; //2 生产内容写到 payload。这个重写了 HttpRequestHandler 的 getContent() 的实现
}
}
我们可以实现两个处理程序逻辑,将选择一个相匹配的协议。然而添加以前写过的处理程序到 ChannelPipeline 是不够的;正确的编解码器还需要补充。它的责任是检测传输字节数,然后使用 FullHttpResponse 和 FullHttpRequest 的抽象进行工作。
Netty 的附带一个基类,完全能做这个。所有您需要做的是实现逻辑选择协议和选择适当的处理程序。
public class DefaultSpdyOrHttpChooser extends SpdyOrHttpChooser {
public DefaultSpdyOrHttpChooser(int maxSpdyContentLength, int maxHttpContentLength) {
super(maxSpdyContentLength, maxHttpContentLength);
}
@Override
protected SelectedProtocol getProtocol(SSLEngine engine) {
DefaultServerProvider provider = (DefaultServerProvider) NextProtoNego.get(engine); //1 使用 NextProtoNego 用于获取 DefaultServerProvider 的引用, 用于 SSLEngine
String protocol = provider.getSelectedProtocol();
if (protocol == null) {
return SelectedProtocol.UNKNOWN; //2 协议不能被检测到。一旦字节已经准备好读,检测过程将重新开始。
}
switch (protocol) {
case "spdy/2":
return SelectedProtocol.SPDY_2; //3 SPDY 2 被检测到
case "spdy/3.1":
return SelectedProtocol.SPDY_3_1; //4 SPDY 3 被检测到
case "http/1.1":
return SelectedProtocol.HTTP_1_1; //5 HTTP 1.1 被检测到
default:
return SelectedProtocol.UNKNOWN; //6 未知协议被检测到
}
}
@Override
protected ChannelInboundHandler createHttpRequestHandlerForHttp() {
return new HttpRequestHandler(); //7 将会被调用给 FullHttpRequest 消息添加处理器。该方法只会在不支持 SPDY 时调用,那么将会使用 HTTPS
}
@Override
protected ChannelInboundHandler createHttpRequestHandlerForSpdy() {
return new SpdyRequestHandler(); //8 将会被调用给 FullHttpRequest 消息添加处理器。该方法在支持 SPDY 时调用
}
}
SPDY 需要两个 ChannelHandler:
8.3.1.3 设置 ChannelPipeline
- SslHandler,用于检测 SPDY 是否通过 TLS 扩展
- DefaultSpdyOrHttpChooser,用于当协议被检测到时,添加正确的 ChannelHandler 到 ChannelPipeline
public class SpdyChannelInitializer extends ChannelInitializer<SocketChannel> { //1 继承 ChannelInitializer 是一个简单的开始
private final SslContext context;
public SpdyChannelInitializer(SslContext context) //2 传递 SSLContext 用于创建 SSLEngine {
this.context = context;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
SSLEngine engine = context.newEngine(ch.alloc()); //3 新建 SSLEngine,用于新的管道和连接
engine.setUseClientMode(false); //4 配置 SSLEngine 用于非客户端使用
NextProtoNego.put(engine, new DefaultServerProvider()); //5 通过 NextProtoNego helper 类绑定 DefaultServerProvider 到 SSLEngine
NextProtoNego.debug = true;
pipeline.addLast("sslHandler", new SslHandler(engine)); //6 添加 SslHandler 到 ChannelPipeline 这将会在协议检测到时保存在 ChannelPipeline
pipeline.addLast("chooser", new DefaultSpdyOrHttpChooser(1024 * 1024, 1024 * 1024));
}
}
8.3.1.4 组合在一起
public class SpdyServer {
private final NioEventLoopGroup group = new NioEventLoopGroup(); //1 构建新的 NioEventLoopGroup 用于处理 I/O
private final SslContext context;
private Channel channel;
public SpdyServer(SslContext context) { //2 传递 SSLContext 用于加密
this.context = context;
}
public ChannelFuture start(InetSocketAddress address) {
ServerBootstrap bootstrap = new ServerBootstrap(); //3 新建 ServerBootstrap 用于配置服务器
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new SpdyChannelInitializer(context)); //4 配置 ServerBootstrap
ChannelFuture future = bootstrap.bind(address); //5 绑定服务器用于接收指定地址的连接
future.syncUninterruptibly();
channel = future.channel();
return future;
}
public void destroy() { //6 销毁服务器,用于关闭管道和 NioEventLoopGroup
if (channel != null) {
channel.close();
}
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Please give port as argument");
System.exit(1);
}
int port = Integer.parseInt(args[0]);
SelfSignedCertificate cert = new SelfSignedCertificate();
SslContext context = SslContext.newServerContext(cert.certificate(), cert.privateKey()); //7 从 BogusSslContextFactory 获取 SSLContext 。这是一个虚拟实现进行测试。真正的实现将为 SslContext 配置适当的密钥存储库。
final SpdyServer endpoint = new SpdyServer(context);
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
endpoint.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}
8.3.1.5 启动并测试
https://www.w3cschool.cn/essential_netty_in_action/essential_netty_in_action-9i8428e8.html
第九章 Netty通过UDP广播事件
9.3.1 Netty的UDP基础
类似TCP一样的面向连接的传输协议管理建立一个两个网络端点之间的调用(或“连接”),在调用的生命周期期间传输命令和可靠的消息,最后有序的在调用终止时终止。与之相反的是,无连接协议UDP中没有持久连接这样的概念,每个消息(UDP数据报)都是一个独立的传播。
9.3.2 Netty UDP 广播
- 多播:传送给一组主机
- 广播:传送到网络上的所有主机(或子网)
为此我们将使用特殊的“有限广播”或“零”网络地址255.255.255.255。消息发送到这个地址是规定要在本地网络(0.0.0.0)的所有主机和从不转发到其他网络通过路由器。
9.3.3 Netty UDP示例
9.3.3.1 Publish/Subscribe(发布/订阅)
- 应用监听新文件内容
- 事件通过 UDP 广播
- 事件监视器监听并显示内容
应用程序有两个组件:广播器和监视器或(可能有多个实例)。为了简单起见我们不会添加身份验证、验证、加密。
9.3.4 Netty LogEvent的POJO
因为数据来自于一个日志文件,我们就将其称之为LogEvent。
public final class LogEvent {
public static final byte SEPARATOR = (byte) ':';
private final InetSocketAddress source;
private final String logfile;
private final String msg;
private final long received;
public LogEvent(String logfile, String msg) { //1 构造器用于出站消息
this(null, -1, logfile, msg);
}
public LogEvent(InetSocketAddress source, long received, String logfile, String msg) { //2 构造器用于入站消息
this.source = source;
this.logfile = logfile;
this.msg = msg;
this.received = received;
}
public InetSocketAddress getSource() { //3 返回发送 LogEvent 的 InetSocketAddress 的资源
return source;
}
public String getLogfile() { //4 返回用于发送 LogEvent 的日志文件的名称
return logfile;
}
public String getMsg() { //5 返回消息的内容
return msg;
}
public long getReceivedTimestamp() { //6 返回 LogEvent 接收到的时间
return received;
}
}
9.3.5 广播器
下图展示了广播一个 DatagramPacket 在每个日志实体里面的方法:
1.日志文件
2.日志文件中的日志实体
3.一个 DatagramPacket 保持一个单独的日志实体
9.3.5.1 编码器和解码器
下例实现了编码器:
public class LogEventEncoder extends MessageToMessageEncoder<LogEvent> {
private final InetSocketAddress remoteAddress;
public LogEventEncoder(InetSocketAddress remoteAddress) { //1 LogEventEncoder 创建了 DatagramPacket 消息类发送到指定的 InetSocketAddress
this.remoteAddress = remoteAddress;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, LogEvent logEvent, List<Object> out) throws Exception {
byte[] file = logEvent.getLogfile().getBytes(CharsetUtil.UTF_8); //2 写文件名到 ByteBuf
byte[] msg = logEvent.getMsg().getBytes(CharsetUtil.UTF_8);
ByteBuf buf = channelHandlerContext.alloc().buffer(file.length + msg.length + 1);
buf.writeBytes(file);
buf.writeByte(LogEvent.SEPARATOR); //3 添加一个 SEPARATOR
buf.writeBytes(msg); //4 写一个日志消息到 ByteBuf
out.add(new DatagramPacket(buf, remoteAddress)); //5 添加新的 DatagramPacket 到出站消息
}
}
为什么使用 MessageToMessageEncoder?
当然我们可以编写自己的自定义 ChannelOutboundHandler 来转换 LogEvent 对象到 DatagramPackets。但是继承自MessageToMessageEncoder 为我们简化和做了大部分的工作。
9.3.5.2 引导
public class LogEventBroadcaster {
private final Bootstrap bootstrap;
private final File file;
private final EventLoopGroup group;
public LogEventBroadcaster(InetSocketAddress address, File file) {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new LogEventEncoder(address)); //1 引导 NioDatagramChannel 。为了使用广播,我们设置 SO_BROADCAST 的 socket 选项
this.file = file;
}
public void run() throws IOException {
Channel ch = bootstrap.bind(0).syncUninterruptibly().channel(); //2 绑定管道。注意当使用 Datagram Channel 时,是没有连接的
System.out.println("LogEventBroadcaster running");
long pointer = 0;
for (;;) {
long len = file.length();
if (len < pointer) {
// file was reset
pointer = len; //3 如果需要,可以设置文件的指针指向文件的最后字节
} else if (len > pointer) {
// Content was added
RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(pointer); //4 设置当前文件的指针,这样不会把旧的发出去
String line;
while ((line = raf.readLine()) != null) {
ch.writeAndFlush(new LogEvent(null, -1, file.getAbsolutePath(), line)); //5 写一个 LogEvent 到管道用于保存文件名和文件实体。(我们期望每个日志实体是一行长度)
}
pointer = raf.getFilePointer(); //6 存储当前文件的位置,这样,我们可以稍后继续
raf.close();
}
try {
Thread.sleep(1000); //7 睡 1 秒。如果其他中断退出循环就重新启动它。
} catch (InterruptedException e) {
Thread.interrupted();
break;
}
}
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new IllegalArgumentException();
}
LogEventBroadcaster broadcaster = new LogEventBroadcaster(new InetSocketAddress("255.255.255.255",
Integer.parseInt(args[0])), new File(args[1])); //8 构造一个新的实例 LogEventBroadcaster 并启动它
try {
broadcaster.run();
} finally {
broadcaster.stop();
}
}
}
9.3.6 监视器(EventLogMonitor )
1.接收 LogEventBroadcaster 广播的 UDP DatagramPacket
2.解码 LogEvent 消息
3.输出 LogEvent 消息
9.3.6.1首先是负责将网络上接收到的 DatagramPacket 解码到 LogEvent 消息。清单显示了实现。
public class LogEventDecoder extends MessageToMessageDecoder<DatagramPacket> {
@Override
protected void decode(ChannelHandlerContext ctx, DatagramPacket datagramPacket, List<Object> out) throws Exception {
ByteBuf data = datagramPacket.content(); //1 获取 DatagramPacket 中数据的引用
int i = data.indexOf(0, data.readableBytes(), LogEvent.SEPARATOR); //2 获取 SEPARATOR 的索引
String filename = data.slice(0, i).toString(CharsetUtil.UTF_8); //3 从数据中读取文件名
String logMsg = data.slice(i + 1, data.readableBytes()).toString(CharsetUtil.UTF_8); //4 读取数据中的日志消息
LogEvent event = new LogEvent(datagramPacket.recipient(), System.currentTimeMillis(),
filename,logMsg); //5 构造新的 LogEvent 对象并将其添加到列表中
out.add(event);
}
}
9.3.6.2 第二个 ChannelHandler 将执行一些首先创建的 LogEvent 消息。在这种情况下,我们只会写入 system.out。在真实的应用程序可能用到一个单独的日志文件或放到数据库。
public class LogEventHandler extends SimpleChannelInboundHandler<LogEvent> { //1 继承 SimpleChannelInboundHandler 用于处理 LogEvent 消息
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace(); //2 在异常时,输出消息并关闭 channel
ctx.close();
}
@Override
public void channelRead0(ChannelHandlerContext channelHandlerContext, LogEvent event) throws Exception {
StringBuilder builder = new StringBuilder(); //3 建立一个 StringBuilder 并构建输出
builder.append(event.getReceivedTimestamp());
builder.append(" [");
builder.append(event.getSource().toString());
builder.append("] [");
builder.append(event.getLogfile());
builder.append("] : ");
builder.append(event.getMsg());
System.out.println(builder.toString()); //4 打印出 LogEvent 的数据
}
}
9.3.6.3 LogEventMonitor(安装处理程序到 ChannelPipeline )
public class LogEventMonitor {
private final Bootstrap bootstrap;
private final EventLoopGroup group;
public LogEventMonitor(InetSocketAddress address) {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(group) //1 引导 NioDatagramChannel。设置 SO_BROADCAST socket 选项。
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new LogEventDecoder()); //2 添加 ChannelHandler 到 ChannelPipeline
pipeline.addLast(new LogEventHandler());
}
}).localAddress(address);
}
public Channel bind() {
return bootstrap.bind().syncUninterruptibly().channel(); //3 绑定的通道。注意,在使用 DatagramChannel 是没有连接,因为这些 无连接
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
throw new IllegalArgumentException("Usage: LogEventMonitor <port>");
}
LogEventMonitor monitor = new LogEventMonitor(new InetSocketAddress(Integer.parseInt(args[0]))); //4 构建一个新的 LogEventMonitor
try {
Channel channel = monitor.bind();
System.out.println("LogEventMonitor running");
channel.closeFuture().await();
} finally {
monitor.stop();
}
}
}
9.3.7 Netty 运行LogEventBroadcaster和LogEventMonitor
https://www.w3cschool.cn/essential_netty_in_action/essential_netty_in_action-z6ea28eh.html
高级主题
第十章 实现Netty自定义的编解码器
在接下来的内容中我们只会学习如何实现 Memcached 协议的一个子集,这足够我们进行添加、检索、删除对象;这些操作一一对应 Memcached 中的 SET,GET,DELETE 命令。
如果想要实现一个给定协议的编解码器,那么我们就该了解了解它的运作原理。一般情况下,对于协议本身都有详细的记录。你会发现其中的多少细致之处呢?值得庆幸的是 Memcached 的二进制协议可以很好的扩展。
在 RFC 中有相应的规范,可以在 https://code.google.com/p/Memcached/wiki/MemcacheBinaryProtocol 找到 。
我们不会实现 Memcached 的所有命令,只会实现三种操作:SET,GET 和 DELETE。这样做事为了让事情变得简单。
10.1 了解Memcached二进制协议
一般协议有 24 字节头用于请求和响应
字段 | 字节偏移量 | 值 |
---|---|---|
Magic | 0 | 0x80 用于请求 0x81 用于响应 |
OpCode | 1 | 0x01…0x1A |
Key length | 2 和 3 | 1…32,767 |
Extra length | 4 | 0x00, x04, 或 0x08 |
Data type | 5 | 0x00 |
Reserved | 6 和 7 | 0x00 |
Total body length | 8-11 | 所有 body 的长度 |
Opaque | 12-15 | 任何带带符号的 32-bit 整数; 这个也包含在响应中,因此更容易将请求映射到响应。 |
CAS | 16-23 | 数据版本检查 |
第十一章 EventLoop和线程模型
使用多个 Thread 提供了资源和管理成本,作为一个副作用,引入了太多的上下文切换。这种会随着运行的线程的数量和任务执行的数量的增加而恶化。尽管使用多个线程在开始时似乎不是一个问题,但一旦你把真正工作负载放在系统上,可以会遭受到重击。
11.1 事件循环EventLoop
事件循环
意思就是:它运行在一个循环中,直到它停止。网络框架需要需要在一个循环中为一个特定的连接运行事件,所以这符合网络框架的设计。
示例:
while (!terminated) {
List<Runnable> readyEvents = blockUntilEventsReady(); //1 阻塞直到事件可以运行
for (Runnable ev: readyEvents) {
ev.run(); //2 循环所有事件,并运行他们
}
}
在 Netty 中使用 EventLoop 接口代表事件循环,EventLoop 是从EventExecutor 和 ScheduledExecutorService 扩展而来,所以可以将任务直接交给 EventLoop 执行。类关系图如下:
Netty 4 中的 I/O 和事件处理
I/O 和事件处理的一个重要的事情在 Netty 4,是每一个 I/O 操作和事件总是由 EventLoop 本身处理,以及分配给 EventLoop 的 Thread。
Netty 3 中的 I/O 操作
Netty 保证只将入站(以前称为 upstream)事件在执行 I/O Thread 执行 (I/O Thread 现在在 Netty 4 叫 EventLoop )。所有的出站(以前称为 downstream)事件被调用Thread 处理,这可能是 I/O Thread 也可以能是其他 Thread
Netty 线程模型的内部
Netty 的内部实现使其线程模型表现优异,它会检查正在执行的 Thread 是否是已分配给实际 Channel (和 EventLoop),在 Channel 的生命周期内,EventLoop 负责处理所有的事件。
如果 Thread 是相同的 EventLoop 中的一个,讨论的代码块被执行;如果线程不同,它安排一个任务并在一个内部队列后执行。通常是通过EventLoop 的 Channel 只执行一次下一个事件,这允许直接从任何线程与通道交互,同时还确保所有的 ChannelHandler 是线程安全,不需要担心并发访问问题。
- 设计是非常重要的,以确保不要把任何长时间运行的任务放在执行队列中,因为长时间运行的任务会阻止其他在相同线程上执行的任务。这多少会影响整个系统依赖于 EventLoop 实现用于特殊传输的实现。
- 传输之间的切换在你的代码库中可能没有任何改变,重要的是:切勿阻塞 I/O 线程。如果你必须做阻塞调用(或执行需要长时间才能完成的任务),使用 EventExecutor。
11.2 EventLoop实现调度任务执行
11.2.1 使用普通的 Java API 调度任务
ScheduledThreadExecutorService 用于调度命令来延迟或者周期性的执行。 corePoolSize 用于计算线程的数量 newSingleThreadScheduledExecutor() newSingleThreadScheduledExecutor(ThreadFact orythreadFactory) | 新建一个 ScheduledThreadExecutorService 可以用于调度命令来延迟或者周期性的执行。它将使用一个线程来执行调度的任务
ScheduledExecutorService executor = Executors
.newScheduledThreadPool(10); //1 新建 ScheduledExecutorService 使用10个线程
ScheduledFuture<?> future = executor.schedule(
new Runnable() { //2 新建 runnable 调度执行
@Override
public void run() {
System.out.println("Now it is 60 seconds later"); //3 稍后运行
}
}, 60, TimeUnit.SECONDS); //4 调度任务60秒后执行
// do something
//
executor.shutdown(); //5 关闭 ScheduledExecutorService 来释放任务完成的资源
11.2.2 使用 EventLoop 调度任务
Netty 允许使用 EventLoop 调度任务分配到通道,如下面代码:
Channel ch = null; // Get reference to channel
ScheduledFuture<?> future = ch.eventLoop().schedule(
new Runnable() { // 1. 新建 runnable 用于执行调度
@Override
public void run() {
System.out.println("Now its 60 seconds later"); //2.稍后执行
}
}, 60, TimeUnit.SECONDS); // 3.调度任务60秒后运行
每隔60秒执行
Channel ch = null; // Get reference to channel
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
System.out.println("Run every 60 seconds");
}
}, 60, 60, TimeUnit.SECONDS);
ScheduledFuture 提供一个方法用于取消一个调度了的任务或者检查它的状态。一个简单的取消操作如下:
ScheduledFuture<?> future = ch.eventLoop()
.scheduleAtFixedRate(..); //1 调度任务并获取返回的 ScheduledFuture
// Some other code that runs...
future.cancel(false); //2 取消任务,阻止它再次运行
11.2.3 调度的内部实现
这种实现只保证一个近似执行,也就是说任务的执行可能不是100%准确;在实践中,这已经被证明是一个可容忍的限制,不影响多数应用程序。所以,定时执行任务不可能100%准确的按时执行。
我们可以这样认为:
- 在指定的延迟时间后调度任务;
- 任务被插入到 EventLoop 的 Schedule-Task-Queue(调度任务队列);
- 如果任务需要马上执行,EventLoop 检查每个运行;
- 如果有一个任务要执行,EventLoop 将立刻执行它,并从队列中删除;
- EventLoop 等待下一次运行,从第4步开始一遍又一遍的重复。
但是如果需要更准确的执行呢?很容易,你需要使用ScheduledExecutorService 的另一个实现,这不是 Netty 的内容。
11.2.4 Netty I/O和EventLoop/Thread的分配细节
Netty 使用一个包含 EventLoop 的 EventLoopGroup 为 Channel 的 I/O 和事件服务。EventLoop 创建并分配方式不同基于传输的实现。异步实现使用只有少数 EventLoop(和 Threads)共享于 Channel 之间 。这允许最小线程数服务多个 Channel,不需要为他们每个人都有一个专门的 Thread。
- 所有的 EventLoop 由 EventLoopGroup 分配。这里它将使用三个EventLoop 实例
- 这个 EventLoop 处理所有分配给它管道的事件和任务。每个EventLoop 绑定到一个 Thread
- 管道绑定到 EventLoop,所以所有操作总是被同一个线程在 Channel 的生命周期执行。一个管道属于一个连接
EventLoop 和 Channel
我们应该注意,在 Netty 4 , Channel 可能从 EventLoop 注销稍后又从不同 EventLoop 注册。这个功能是不赞成,因为它在实践中没有很好的工作
语义跟其他传输略有不同,如 OIO(Old Blocking I/O)运输,可以看到如图14.8所示。
-
所有 EventLoop 从 EventLoopGroup 分配。每个新的 channel 将会获得新的 EventLoop
-
EventLoop 分配给 channel 用于执行所有事件和任务
-
Channel 绑定到 EventLoop。一个 channel 属于一个连接