netty(2)–netty初探
Netty实战
服务器客户端通信
Server
public final class Server {
public static void main(String[] args) throws Exception {
//Configure the server
//创建两个EventLoopGroup对象
//创建boss线程组 用于服务端接受客户端的连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 创建 worker 线程组 用于进行 SocketChannel 的数据读写
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建 ServerBootstrap 对象
ServerBootstrap b = new ServerBootstrap();
//设置使用的EventLoopGroup
b.group(bossGroup,workerGroup)
//设置要被实例化的为 NioServerSocketChannel 类
.channel(NioServerSocketChannel.class)
// 设置 NioServerSocketChannel 的处理器
.handler(new LoggingHandler(LogLevel.INFO))
// 设置连入服务端的 Client 的 SocketChannel 的处理器
.childHandler(new ServerInitializer());
// 绑定端口,并同步等待成功,即启动服务端
ChannelFuture f = b.bind(8888);
// 监听服务端关闭,并阻塞等待
f.channel().closeFuture().sync();
} finally {
// 优雅关闭两个 EventLoopGroup 对象
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
private static final StringDecoder DECODER = new StringDecoder();
private static final StringEncoder ENCODER = new StringEncoder();
private static final ServerHandler SERVER_HANDLER = new ServerHandler();
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 添加帧限定符来防止粘包现象
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
// 解码和编码,应和客户端一致
pipeline.addLast(DECODER);
pipeline.addLast(ENCODER);
// 业务逻辑实现类
pipeline.addLast(SERVER_HANDLER);
}
}
@Sharable
public class ServerHandler extends SimpleChannelInboundHandler<String> {
/**
* 建立连接时,发送一条庆祝消息
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 为新连接发送庆祝
ctx.write("Welcome to " + InetAddress.getLocalHost().getHostName() + "!\r\n");
ctx.write("It is " + new Date() + " now.\r\n");
ctx.flush();
}
//业务逻辑处理
@Override
public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception {
// Generate and write a response.
String response;
boolean close = false;
if (request.isEmpty()) {
response = "Please type something.\r\n";
} else if ("bye".equals(request.toLowerCase())) {
response = "Have a good day!\r\n";
close = true;
} else {
response = "Did you say '" + request + "'?\r\n";
}
ChannelFuture future = ctx.write(response);
if (close) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Client
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ClientInitializer());
Channel ch = b.connect("127.0.0.1",8888).sync().channel();
ChannelFuture lastWriteFuture = null;
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
for (;;) {
String line = in.readLine();
if (line == null) {
break;
}
// Sends the received line to the server.
lastWriteFuture = ch.writeAndFlush(line + "\r\n");
// If user typed the 'bye' command, wait until the server closes
// the connection.
if ("bye".equals(line.toLowerCase())) {
ch.closeFuture().sync();
break;
}
}
// Wait until all messages are flushed before closing the channel.
if (lastWriteFuture != null) {
lastWriteFuture.sync();
}
} finally {
group.shutdownGracefully();
}
}
public class ClientInitializer extends ChannelInitializer<SocketChannel> {
private static final StringDecoder DECODER = new StringDecoder();
private static final StringEncoder ENCODER = new StringEncoder();
private static final ClientHandler CLIENT_HANDLER = new ClientHandler();
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
pipeline.addLast(DECODER);
pipeline.addLast(ENCODER);
pipeline.addLast(CLIENT_HANDLER);
}
}
@Sharable
public class ClientHandler extends SimpleChannelInboundHandler<String> {
//打印读取到的数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.err.println(msg);
}
//异常数据捕获
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Netty组件
Channel
基本的 I/O 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提供的原 语。在基于 Java 的网络编程中,其基本的构造是类 Socket。Netty 的 Channel 接口所提供 的 API,被用于所有的 I/O 操作。大大地降低了直接使用 Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
由于Channel 是独一无二的,所以为了保证顺序将Channel 声明为java.lang.Comparable 的一个子接口。因此,如果两个不同的 Channel 实例都返回了相同的散列码,那么 AbstractChannel 中的 compareTo()方法的实现将会抛出一个 Error。
Channel 的生命周期状态
-
ChannelUnregistered :Channel 已经被创建,但还未注册到 EventLoop
-
ChannelRegistered :Channel 已经被注册到了 EventLoop
-
ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
-
ChannelInactive :Channel 没有连接到远程节点
当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给ChannelPipeline 中的 ChannelHandler,其可以随后对它们做出响应。
Channel 的重要方法
-
eventLoop: 返回分配给 Channel 的 EventLoop
-
pipeline: 返回分配给 Channel 的 ChannelPipeline
-
isActive: 如果 Channel 是活动的,则返回 true。活动的意义可能依赖于底层的传输。 例如,一个 Socket 传输一旦连接到了远程节点便是活动的,而一个 Datagram 传输一旦被 打开便是活动的。
-
localAddress: 返回本地的 SokcetAddress
-
remoteAddress: 返回远程的 SocketAddress
-
write: 将数据写到远程节点。这个数据将被传递给 ChannelPipeline,并且排队直到它 被冲刷
-
flush: 将之前已写的数据冲刷到底层传输,如一个 Socket writeAndFlush: 一个简便的方法,等同于调用 write()并接着调用 flush()
EventLoop和EventLoopGroup
回想一下我们在 NIO 中是如何处理我们关心的事件的?在一个 while 循环中 select 出事 件,然后依次处理每种事件。我们可以把它称为事件循环,这就是 EventLoop。interface io.netty.channel. EventLoop 定义了 Netty 的核心抽象,用于处理网络连接的生命周期中所发 生的事件。
io.netty.util.concurrent 包构建在JDK 的java.util.concurrent 包上。而io.netty.channel 包 中的类,为了与 Channel 的事件进行交互,扩展了这些接口/类。一个 EventLoop 将由一个 永远都不会改变的 Thread 驱动,同时任务(Runnable 或者 Callable)可以直接提交给 EventLoop 实现,以立即执行或者调度执行。
![截屏2022-02-23 下午8.37.54](https://gitee.com/RoyalH/pic-cloud/raw/master/img/%E6%88%AA%E5%B1%8F2022-02-23%20%E4%B8%8B%E5%8D%888.37.54.png)
Netty 的 EventLoop 在继承了 ScheduledExecutorService 的同时,只定义了一个方法, parent()。在 Netty 4 中,所有的 I/O 操作和事件都由已经被分配给了 EventLoop 的那个 Thread 来处理。
线程管理
在内部,当提交任务到如果(当前)调用线程正是支撑 EventLoop 的线程,那么所提交 的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入 到内部队列中。当 EventLoop 下次处理它的事件时,它会执行队列中的那些任务/事件。
![截屏2022-02-23 下午8.43.24](https://gitee.com/RoyalH/pic-cloud/raw/master/img/%E6%88%AA%E5%B1%8F2022-02-23%20%E4%B8%8B%E5%8D%888.43.24.png)
线程的分配
服务于 Channel 的 I/O 和事件的 EventLoop 则包含在 EventLoopGroup 中。
异步传输实现只使用了少量的 EventLoop(以及和它们相关联的 Thread),而且在当前 的线程模型中,它们可能会被多个Channel 所共享。这使得可以通过尽可能少量的Thread 来 支撑大量的 Channel,而不是每个 Channel 分配一个 Thread。EventLoopGroup 负责为每个 新创建的 Channel 分配一个 EventLoop。在当前实现中,使用顺序循环(round-robin)的方 式进行分配以获取一个均衡的分布,并且相同的 EventLoop 可能会被分配给多个 Channel。
一旦一个 Channel 被分配给一个 EventLoop,它将在它的整个生命周期中都使用这个 EventLoop(以及相关联的 Thread)。请牢记这一点,因为它可以使你从担忧你的 ChannelHandler 实现中的线程安全和同步问题中解脱出来。
注意,EventLoop 的分配方式对 ThreadLocal 的使用的影响。因为一个 EventLoop 通 常会被用于支撑多个 Channel,所以对于所有相关联的 Channel 来说,ThreadLocal 都将是 一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上 下文中,它仍然可以被用于在多个 Channel 之间共享一些重度的或者代价昂贵的对象,甚 至是事件。
ChannelFuture
Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要 一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了 ChannelFuture 接口, 其 addListener()方法注册了一个 ChannelFutureListener,以便在某个操作完成时(无论是否 成功)得到通知。
可以将 ChannelFuture 看作是将来要执行的操作的结果的占位符。它究竟什么时候被执 行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。
ChannelHandler
从应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有 处理入站和出站数据的应用程序逻辑的容器。
ChannelInboundHandler
ChannelHandler 子接口,处理入站数据以及各种状态变化。
下面列出了接口 ChannelInboundHandler 的生命周期方法。这些方法将会在数据被接收 时或者与其对应的 Channel 状态发生改变时被调用。
-
channelRegistered 当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用
-
channelUnregistered 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用
-
channelActive 当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就 绪
-
channelInactive 当 Channel 离开活动状态并且不再连接它的远程节点时被调用
-
channelReadComplete 当 Channel 上的一个读操作完成时被调用
-
channelRead 当从 Channel 读取数据时被调用
-
ChannelWritabilityChanged当 Channel 的可写状态发生改变时被调用。可以通过调用 Channel 的 isWritable()方法 来检测 Channel 的可写性。与可写性相关的阈值可以通过Channel.config().setWriteHighWaterMark()Channel.config().setWriteLowWaterMark()方法来 设置
-
userEventTriggered 当 ChannelnboundHandler.fireUserEventTriggered()方法被调用时被 调用。
ChannelOutboundHandler
ChannelHandler 子接口,处理出站数据并且允许拦截所有的操作。
出站操作和数据将由 ChannelOutboundHandler 处理。它的方法将被 Channel、Channel- Pipeline 以及 ChannelHandlerContext 调用。
所有由 ChannelOutboundHandler 本身所定义的方法:
-
bind(ChannelHandlerContext,SocketAddress,ChannelPromise) 当请求将 Channel 绑定到本地地址时被调用
-
connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise) 当请求将 Channel 连接到远程节点时被调用
-
disconnect(ChannelHandlerContext,ChannelPromise) 当请求将 Channel 从远程节点断开时被调用
-
close(ChannelHandlerContext,ChannelPromise) 当请求关闭 Channel 时被调用
-
deregister(ChannelHandlerContext,ChannelPromise) 当请求将 Channel 从它的 EventLoop 注销时被调用
-
read(ChannelHandlerContext) 当请求从 Channel 读取更多的数据时被调用
-
flush(ChannelHandlerContext) 当请求通过 Channel 将入队数据冲刷到远程节点时被调 用
-
write(ChannelHandlerContext,Object,ChannelPromise) 当请求通过 Channel 将数据写到 远程节点时被调用
ChannelHandler的适配器
有一些适配器类可以将编写自定义的 ChannelHandler 所需要的工作降到最低限度,因 为它们提供了定义在对应接口中的所有方法的默认实现。因为你有时会忽略那些不感兴趣的 事件,所以 Netty 提供了抽象基类 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter。
你可以使用 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 类作为 自己的 ChannelHandler 的起始点。这两个适配器分别提供了 ChannelInboundHandler 和 ChannelOutboundHandler 的基本实现。通过扩展抽象类 ChannelHandlerAdapter,它们获得 了它们共同的超接口 ChannelHandler 的方法。
ChannelHandlerAdapter 还提供了实用方法 isSharable()。如果其对应的实现被标注为 Sharable,那么这个方法将返回 true,表示它可以被添加到多个 ChannelPipeline。
![截屏2022-02-23 下午9.09.40](https://gitee.com/RoyalH/pic-cloud/raw/master/img/%E6%88%AA%E5%B1%8F2022-02-23%20%E4%B8%8B%E5%8D%889.09.40.png)
资源管理和 SimpleChannelInboundHandler
在 Read 网络数据时由 Netty 创建 Buffer, Write 网络数据时 Buffer 往往是由业务方创建的。不管是读和写,Buffer 用完后都必须进行 释放,否则可能会造成内存泄露。
在 Write 网络数据时,可以确保数据被写往网络了,Netty 会自动进行 Buffer 的释放, 但是如果 Write 网络数据时,我们有 outBoundHandler 处理了 write()操作并丢弃了数据,没 有继续往下写,要由我们负责释放这个 Buffer,就必须调用 ReferenceCountUtil.release 方法, 否则就可能会造成内存泄露。
在 Read 网络数据时,如果我们可以确保每个 InboundHandler 都把数据往后传递了,也 就是调用了相关的 fireChannelRead 方法,Netty 也会帮我们释放,同样的,如果我们有 InboundHandler 处理了数据,又不继续往后传递,又不调用负责释放的 ReferenceCountUtil.release 方法,就可能会造成内存泄露
但是由于消费入站数据是一项常规任务,所以 Netty 提供了一个特殊的被称为 SimpleChannelInboundHandler 的 ChannelInboundHandler 实现。这个实现会在数 据被 channelRead0()方法消费之后自动释放数据。
同时系统为我们提供的各种预定义 Handler 实现,都实现了数据的正确处理,所以我们 自行在编写业务 Handler 时,也需要注意这一点:要么继续传递,要么自行释放。
ChannelPipeline 和 ChannelHandlerContext
ChannelPipeline
当 Channel 被创建时,它将会被自动地分配一个新的 ChannelPipeline。这项关联是永久 性的;Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。在 Netty 组件 的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。
使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化 或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给 链中的下一个 ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。
ChannelHandler 的生命周期
在ChannelHandler被添加到ChannelPipeline 中或者被从ChannelPipeline 中移除时会调用下面这些方法。这些方法中的每一个都接受一个 ChannelHandlerContext 参数。
- handlerAdded 当把 ChannelHandler 添加到 ChannelPipeline 中时被调用
- handlerRemoved 当从 ChannelPipeline 中移除 ChannelHandler 时被调用
- exceptionCaught 当处理过程中在 ChannelPipeline 中有错误产生时被调用
ChannelPipeline 中 ChannelHandler
入站和出站 ChannelHandler 可以被安装到同一个 ChannelPipeline 中。如果一个消息或 者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部开始流动,最终,数据 将会到达 ChannelPipeline 的尾端,届时,所有处理就都结束了。
数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从 ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站 数据将会到达网络传输层,这里显示为 Socket。通常情况下,这将触发一个写操作。
ChannelPipeline 上的方法
- addFirst、addBefore、addAfter、addLast 将一个 ChannelHandler 添加到 ChannelPipeline 中
- remove 将一个 ChannelHandler 从 ChannelPipeline 中移除
- replace 将 ChannelPipeline 中的一个 ChannelHandler 替换为另一个ChannelHandler get 通过类型或者名称返回 ChannelHandler
- context 返回和 ChannelHandler 绑定的 ChannelHandlerContext
- names 返回 ChannelPipeline 中所有 ChannelHandler 的名称ChannelPipeline 的 API 公开了用于调用入站和出站操作的附加方法。
ChannelHandlerContext
通过使用作为参数传递到每个方法的 ChannelHandlerContext,事件可以被传递给当前 ChannelHandler 链中的下一个 ChannelHandler。虽然这个对象可以被用于获取底层的 Channel,但是它主要还是被用于写出站数据。
ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。 ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。
ChannelHandlerContext 有很多的方法,其中一些方法也存在于 Channel 和 Channel-Pipeline 本身上,但是有一点重要的不同。如果调用Channel 或者ChannelPipeline 上 的这些方法,它们将沿着整个 ChannelPipeline 进行传播。而调用位于 ChannelHandlerContext上的相同方法,则将从当前所关联的 ChannelHandler 开始,并且只会传播给位于该 ChannelPipeline 中的下一个(入站下一个,出站上一个)能够处理该事件的 ChannelHandler。
-
alloc 返回和这个实例相关联的 Channel 所配置的 ByteBufAllocator
-
bind 绑定到给定的 SocketAddress,并返回 ChannelFuture
-
channel 返回绑定到这个实例的 Channel
-
close 关闭 Channel,并返回 ChannelFuture
-
connect 连接给定的 SocketAddress,并返回 ChannelFuture
-
deregister 从之前分配的 EventExecutor 注销,并返回 ChannelFuture
-
disconnect 从远程节点断开,并返回 ChannelFuture
-
executor 返回调度事件的 EventExecutor
-
fireChannelActive 触发对下一个 ChannelInboundHandler 上的 channelActive()方法(已 连接)的调用
-
fireChannelInactive 触发对下一个 ChannelInboundHandler 上的 channelInactive()方法 (已关闭)的调用
-
fireChannelRead 触发对下一个 ChannelInboundHandler 上的 channelRead()方法(已接 收的消息)的调用
-
fireChannelReadComplete 触发对下一个 ChannelInboundHandler 上的 channelReadComplete()方法的调用
-
fireChannelRegistered 触发对下一个 ChannelInboundHandler 上的 fireChannelRegistered()方法的调用
-
fireChannelUnregistered 触发对下一个 ChannelInboundHandler 上的 fireChannelUnregistered()方法的调用
-
fireChannelWritabilityChanged 触发对下一个 ChannelInboundHandler 上的 fireChannelWritabilityChanged()方法的调用
-
fireExceptionCaught 触发对下一个 ChannelInboundHandler 上的 fireExceptionCaught(Throwable)方法的调用
-
fireUserEventTriggered 触发对下一个 ChannelInboundHandler 上的 fireUserEventTriggered(Object evt)方法的调用
-
handler 返回绑定到这个实例的 ChannelHandler
-
isRemoved 如果所关联的 ChannelHandler 已经被从 ChannelPipeline 中移除则返回 true name 返回这个实例的唯一名称
-
pipeline 返回这个实例所关联的 ChannelPipeline
-
read 将数据从 Channel 读取到第一个入站缓冲区;如果读取成功则触发一个 channelRead 事件,并(在最后一个消息被读取完成后)通知 ChannelInboundHandler 的 channelReadComplete(ctx)方法
-
write 通过这个实例写入消息并经过 ChannelPipeline
-
writeAndFlush 通过这个实例写入并冲刷消息并经过 ChannelPipeline
注意ChannelHandlerContext 和 ChannelHandler 之间的关联(绑定)是永远不会改变的, 所以缓存对它的引用是安全的;如同我们在本节开头所解释的一样,相对于其他类的同名方法,ChannelHandler Context 的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。
选择合适的内置通信传输模式
NIO io.netty.channel.socket.nio 使用 java.nio.channels 包作为基础——基于选择器的方式
Epoll io.netty.channel.epoll 由 JNI 驱动的 epoll()和非阻塞 IO。这个传输支持只有在 Linux 上可用的多种特性,如 SO_REUSEPORT,比 NIO 传输更快,而且是完全非阻塞的。将 NioEventLoopGroup 替换为 EpollEventLoopGroup , 并且将 NioServerSocketChannel.class 替 换为 EpollServerSocketChannel.class 即可。
OIO io.netty.channel.socket.oio 使用 java.net 包作为基础——使用阻塞流
Local io.netty.channel.local 可以在 VM 内部通过管道进行通信的本地传输
Embedded io.netty.channel.embedded Embedded 传输,允许使用 ChannelHandler 而又 不需要一个真正的基于网络的传输。在测试 ChannelHandler 实现时非常有用
引导 Bootstrap
网络编程里,“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。
因此,有两种类型的引导:一种用于客户端(简单地称为 Bootstrap),而另一种 (ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据, 唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。
Bootstrap | ServerBootstrap | |
---|---|---|
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup 的数目 | 1 | 2 |
ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由 想要连接到远程节点的客户端应用程序所使用的。
客户端只需要一个 EventLoopGroup,但是一个 ServerBootstrap 则需要两个(也可以是同一个实例)。
因为服务器需要两组不同的 Channel。第一组将只包含一个 ServerChannel,代表服务器 自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理 传入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel。
与 ServerChannel 相关联的 EventLoopGroup 将分配一个负责为传入连接请求创建 Channel 的 EventLoop。一旦连接被接受,第二个 EventLoopGroup 就会给它的 Channel 分配 一个 EventLoop。
Netty 提供了一个特殊的 ChannelInboundHandlerAdapter 子类:public abstract class ChannelInitializer<C extends Channel> ext ends ChannelInboundHandlerAdapter
它定义了下面的方法:protect ed abstract void initChannel(C ch) throws Exception
。这个方法提供了一种将多个 ChannelHandler 添加到一个 ChannelPipeline 中的简便方法。 你只需要简单地向 Bootstrap 或 ServerBootstrap 的实例提供你的 ChannelInitializer 实现即 可,并且一旦 Channel 被注册到了它的 EventLoop 之后,就会调用你的 initChannel()版本。 在该方法返回之后,ChannelInitializer 的实例将会从 ChannelPipeline 中移除它自己。
ChannelOption
ChannelOption 的各种属性在套接字选项中都有对应。
- ChannelOption.SO_BACKLOG ChannelOption.SO_BACKLOG 对应的是 tcp/ip 协议 listen 函数中的 backlog 参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列的大小。
- ChannelOption.SO_REUSEADDR ChanneOption.SO_REUSEADDR 对应于套接字选项中的 SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,比如,某个服务器进程占用了 TCP 的 80 端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置 SO_REUSEADDR就无法正常使用该端口。
- ChannelOption.SO_KEEPALIVE Channeloption.SO_KEEPALIVE 参数对应于套接字选项中的 SO_KEEPALIVE,该参数用于设置 TCP 连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP 会自动发送一个活动探测数据报文。
- ChannelOption.SO_SNDBUF 和 ChannelOption.SO_RCVBUF ChannelOption.SO_SNDBUF 参数对应于套接字选项中的 SO_SNDBUF,
ChannelOption.SO_RCVBUF 参数对应于套接字选项中的 SO_RCVBUF 这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。 - ChannelOption.SO_LINGER ChannelOption.SO_LINGER 参数对应于套接字选项中的 SO_LINGER,Linux内核默认的处理方式是当用户调用 close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用 SO_LINGER 可以阻塞 close()的调用时间,直到数据完全发送
- ChannelOption.TCP_NODELAY ChannelOption.TCP_NODELAY 参数对应于套接字选项中的 TCP_NODELAY,该参数的使用与 Nagle 算法有关,Nagle 算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用 Nagle 算法,使用于小数据即时传输,于 TCP_NODELAY 相对应的是 TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
ByteBuf
ByteBuf API 的优点:
它可以被用户自定义的缓冲区类型扩展;
通过内置的复合缓冲区类型实现了透明的零拷贝;
容量可以按需增长(类似于 JDK 的 StringBuilder);
在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法;
读和写使用了不同的索引;
支持方法的链式调用;
支持引用计数;
支持池化。
ByteBuf 维护了两个不同的索引,名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或者 get 开头的操作则不会。如果打算读取字节直到 readerIndex 达到和 writerIndex 同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个 IndexOutOf-BoundsException。可以指定 ByteBuf 的最大容量。试图移动写索引(即 writerIndex)超过这个值将会触发一个异常。(默认的限制是 Integer.MAX_VALUE。)
使用模式
堆缓冲区
最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。可以由 hasArray()来判断检查 ByteBuf 是否由数组支撑。如果不是,则这是一个直接缓冲区。
直接缓冲区
将缓冲区建立在物理内存中。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。
复合缓冲区
复合缓冲区 CompositeByteBuf,它为多个 ByteBuf 提供一个聚合视图。比如 HTTP 协议,分为消息头和消息体,这两部分可能由应用程序的不同模块产生,各有各的 ByteBuf,将会在消息被发送的时候组装为一个 ByteBuf,此时可以将这两个 ByteBuf 聚合为一个CompositeByteBuf,然后使用统一和通用的 ByteBuf API 来操作。
分配
Netty 提供了两种方式分配Buf
ByteBufAllocator 接口
Netty 通过 interface ByteBufAllocator 分配我们所描述过的任意类型的 ByteBuf 实例。
名称 | 描述 |
---|---|
buffer() | 返回一个基于堆或者直接内存存储的 ByteBuf |
heapBuffer() | 返回一个基于堆内存存储的 ByteBuf |
directBuffer() | 返回一个基于直接内存存储的 ByteBuf |
compositeBuffer() | 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的 CompositeByteBuf |
ioBuffer() | 返回一个用于套接字的 I/O 操作的 ByteBuf,当所运行的环境具有 sun.misc.Unsafe 支持时,返回基于直接内存存储的 ByteBuf,否则返回基于堆内存存储的 ByteBuf;当指定使用PreferHeapByteBufAllocator 时,则只会返回基于堆内存存储的ByteBuf。 |
可以通过 Channel(每个都可以有一个不同的 ByteBufAllocator 实例)或者绑定到ChannelHandler 的 ChannelHandlerContext 获取一个到 ByteBufAllocator 的引用。
Netty 提供了两种 ByteBufAllocator 的实现:PooledByteBufAllocator 和Unpooled-ByteBufAllocator。前者池化了 ByteBuf 的实例以提高性能并最大限度地减少内存碎片。后者的实现不池化 ByteBuf 实例,并且在每次它被调用时都会返回一个新的实例。
Netty4.1 默认使用了 PooledByteBufAllocator。
Unpooled 缓冲区
Netty 提供了一个简单的称为 Unpooled 的工具类,它提供了静态的辅助方法来创建未
池化的 ByteBuf 实例。
- buffer() 返回一个未池化的基于堆内存存储的 ByteBuf
- directBuffer()返回一个未池化的基于直接内存存储的 ByteBuf
- wrappedBuffer() 返回一个包装了给定数据的 ByteBuf
- copiedBuffer() 返回一个复制了给定数据的 ByteBuf
Unpooled 类还可用于 ByteBuf 同样可用于那些并不需要 Netty 的其他组件的非网络项
目。
随机访问索引 / 顺序访问索引 / 读写操作
如同在普通的 Java 字节数组中一样,ByteBuf 的索引是从零开始的:第一个字节的索引是 0,最后一个字节的索引总是 capacity() - 1。使用那些需要一个索引值参数( 随机访问,也即是数组下标)的方法(的其中)之一来访问数据既不会改变 readerIndex 也不会改变writerIndex。如果有需要,也可以通过调用 readerIndex(index)或者 writerIndex(index)来手动移动这两者。 顺序访问通过索引访问
有两种类别的读/写操作:
get()和 set()操作,从给定的索引开始,并且保持索引不变;get+数据字长(bool.byte,int,short,long,bytes)
read()和 write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
- isReadable() 如果至少有一个字节可供读取,则返回 true
- isWritable() 如果至少有一个字节可被写入,则返回 true
- readableBytes() 返回可被读取的字节数
- writableBytes() 返回可被写入的字节数
- capacity() 返回 ByteBuf 可容纳的字节数。在此之后,它会尝试再次扩展直到达maxCapacity()
- maxCapacity() 返回 ByteBuf 可以容纳的最大字节数
- hasArray() 如果 ByteBuf 由一个字节数组支撑,则返回 true
- array() 如果 ByteBuf 由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException 异常
可丢弃字节
可丢弃字节的分段包含了已经被读过的字节。通过调用 discardReadBytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为 0,存储在 readerIndex 中,会随着 read 操作的执行而增加(get*操作不会移动 readerIndex)。
缓冲区上调用 discardReadBytes()方法后,可丢弃字节分段中的空间已经变为可写的了。频繁地调用 discardReadBytes()方法以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节必须被移动到缓冲区的开始位置。建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候。
可读字节
ByteBuf 的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的 readerIndex 值为
可写字节
可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的
writerIndex 的默认值为 0。任何名称以 write 开头的操作都将从当前的 writerIndex 处开始
写数据,并将它增加已经写入的字节数。
索引管理
调用 markReaderIndex()、markWriterIndex()、resetWriterIndex()和 resetReaderIndex()来标记和重置 ByteBuf 的 readerIndex 和 writerIndex。也可以通过调用 readerIndex(int)或者 writerIndex(int)来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个 IndexOutOfBoundsException。可以通过调用 clear()方法来将 readerIndex 和 writerIndex 都设置为 0。注意,这并不会清除内存中的内容。
查找操作
在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf()方法。较复杂的查找可以通过调用 forEachByte()。
派生缓冲区
派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:
- duplicate();
- slice();
- slice(int, int);
- Unpooled.unmodifiableBuffer(…);
- order(ByteOrder);
- readSlice(int)。
每个这些方法都将返回一个新的 ByteBuf 实例,它具有自己的读索引、写索引和标记索引。其内部存储和 JDK 的 ByteBuffer 一样也是共享的。
ByteBuf 复制 如果需要一个现有缓冲区的真实副本,请使用 copy()或者 copy(int, int)方
法。不同于派生缓冲区,由这个调用所返回的 ByteBuf 拥有独立的数据副本
引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第 4 版中为ByteBuf引入了引用计数技术,interface ReferenceCounted
工具类
ByteBufUtil 提供了用于操作 ByteBuf 的静态的辅助方法。因为这个 API 是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。这些静态方法中最有价值的可能就是 hexdump()方法,它以十六进制的表示形式打印ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录 ByteBuf 的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。
另一个有用的方法是 boolean equals(ByteBuf, ByteBuf),它被用来判断两个 ByteBuf 实例的相等性。
资源释放
当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它要负责显式地释放与池化的 ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法ReferenceCountUtil.release()Netty 将使用 WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。
- 对于入站请求,Netty 的 EventLoo 在处理 Channel 的读操作时进行分配 ByteBuf,对于这类 ByteBuf,需要我们自行进行释放,有三种方式,或者使用
SimpleChannelInboundHandler,或者在重写 channelRead()方法使用ReferenceCountUtil.release()或者使用 ctx.fireChannelRead 继续向后传递;
的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。
另一个有用的方法是 boolean equals(ByteBuf, ByteBuf),它被用来判断两个 ByteBuf 实例的相等性。
资源释放
当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它要负责显式地释放与池化的 ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法ReferenceCountUtil.release()Netty 将使用 WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。
-
对于入站请求,Netty 的 EventLoo 在处理 Channel 的读操作时进行分配 ByteBuf,对于这类 ByteBuf,需要我们自行进行释放,有三种方式,或者使用
SimpleChannelInboundHandler,或者在重写 channelRead()方法使用ReferenceCountUtil.release()或者使用 ctx.fireChannelRead 继续向后传递; -
对于出站请求,不管 ByteBuf 是否由我们的业务创建的,当调用了 write 或者writeAndFlush 方法后,Netty 会自动替我们释放,不需要我们业务代码自行释放。