Netty源码笔记

概述

在网络编程领域,Netty是Java的卓越框架。对于我们许多人来说,它们已经变得不可或缺,因为它们既能满足我们的技术需求,又能满足我们的时间表。

它驾驭了Java高级API的能力,并将其隐藏在一个易于使用的API之后。Netty使你可以专注于自己真正感兴趣的------你的应用程序的独一无二的价值。

Netty用来做HTTP长连接,而后者用来支持各种各样的推送通知。

通过实现 FTP、SMTP、HTTP 和 WebSocket 以及其他的基于二进制和基于文本的协议,Netty 扩展了它的应用范围及灵活性。

非阻塞网络调用使得我们可以不必等待一个操作的完成。完全异步的 I/O 则是更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户。

选择器使得我们能够通过较少的线程便可监视许多连接上的事件。将这些元素结合在一起,与使用阻塞 I/O 来处理大量事件相比,使用非阻塞 I/O 来处理更快速、更经济

它可以以任意的顺序响应在任意的时间点产生的事件

* 可以使用 epoll 替代 NIO,只需要将 NioEventLoopGroup替换为 EpollEventLoopGroup ,并且将 NioServerSocketChannel.class 替换为EpollServerSocketChannel.class 即可。

* 用于异步传输相同的API来支持OIO的呢。

Netty利用了SO_TIMEOUT这个Socket标志,它指定了等待一个I/O操作完成的最大毫秒数。如果操作在指定的时间间隔内没有完成,则将会抛出一个SocketTimeout Exception。

Netty将捕获这个异常并继续处理循环。在EventLoop下一次运行时,它将再次尝试。这实际上也是类似于Netty这样的异步框架能够支持OIO的唯一方式

这样调用者线程和select线程就相当于阻塞同步了

Netty自带的拆包/粘包、异常检测等机制让你从NIO的繁重细节中脱离出来,只需要关心业务逻辑即可。

Netty解决了JDK很多包括空轮询在内的Bug。

Netty底层对线程、Selector做了很多细小的优化,精心设计的Reactor线程模型可以做到非常高效的并发处理

原生的Nio Socket实现

public static void selector() {
Selector selector = null ;
ServerSocketChannel ssc = null ;
try {
// 创建 selector
selector = Selector.open();
// 创建 ServerSocketChannel 通道
ssc= ServerSocketChannel.open();
...
//ServerSocketChannel 通道注册在 selector
ssc.register(selector, SelectionKey.OP_ACCEPT);

// 循环监听事件
while (true ){
if (selector.select(TIMEOUT) == 0){
continue ;
}
// 根据事件操作码,决定什么操作
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()){
SelectionKey key = iter.next();
if (key.isAcceptable()){
handleAccept(key);
} if (key.isReadable()){
...
}
iter.remove();
}
}
}

// 处理方法
public void handleAccept(SelectionKey key) throwsIOException{
SocketChannel sc=((ServerSocketChannel) key.channel()).accept();
sc.configureBlocking(false );
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}

public void handleRead(SelectionKey key) throwsIOException{// 获取 Channel
SocketChannel sc =(SocketChannel) key.channel();// 获取 buffer 并重置
ByteBuffer buffer =(ByteBuffer)key.attachment();
buffer.clear();// 没有读到内容则关闭
if (sc.read(buffer) == -1)
sc.close();else {// buffer 转换为读状态
buffer.flip();// buffer 中接收到的值按 localCharset 格式编码后保存到 receivedString
String receivedString =Charset.forName(localCharset).newDecoder().decode(buffer).toString();
System.out .println("received from client:" +receivedString);// 返回数据给客户端
String sendString = "this data is from Server" ;
buffer=ByteBuffer.wrap(sendString.getBytes(localCharset));
sc.write(buffer);
sc.close();
}

}

Netty 的核心思想

(1)在本节中我将要讨论 Netty 的主要构件块:可以被认为是 Netty 网络抽象的代表

n Channel;---Socket

n 回调Future;---异步通知

n 事件和 ChannelHandler。---selectKey处理

EventLoop---控制流、多线程处理、并发;

Netty的异步编程模型是建立在Future和回调的概念之上的,而将事件派发到ChannelHandler的方法则发生在更深的层次上。结合在一起,这些元素就提供了一个处理环境,

使你的应用程序逻辑可以独立于任何网络操作相关的顾虑而独立地演变。这也是 Netty 的设计方式的一个关键目标。拦截操作以及高速地转换入站数据和出站数据,都只需要你提供回调或者利用操作所返回的Future。这使得链接操作变得既简单又高效,并且促进了可重用的通用代码的编写。

Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了所有本来将需要手动编写的派发代码。在内部,将会为每个 Channel 分配一个 EventLoop,用以处理所有事件,包括:

n 注册感兴趣的事件;

n 将事件派发给 ChannelHandler;

n 安排进一步的动作。

EventLoop 本身只由一个线程驱动,其内实现Runable的run方法:处理了一个 Channel 的所有 I/O 事件,并且在该EventLoop 的整个生命周期内都不会改变

其也实现了线程池的executor方法,用于实现指定的回调。

* 例如NiEventLoop的run方法实现:此NiEventLoop为NioServerSocketChannel().eventLoop()生成的

  • 会遍历selectionKey,如果其中的事件可用,例如read事件,那么就会读取底层的socket流数据到byteBuf接着把此byteBuf丢到管道中即可,然后自己处理下一个可用的时间

  • 管道代表着对当前channel的一个处理流如DefaultChannelPipeline,其持有一个 ChannelHandler 的实例链(AbstracChannelHandlerContext实现类)

AbstracChannelHandlerContext是ChannelHandler 接口的组合实现类,维护了ChannelHandler 的实例链的增删改查

对ChannelHandler接口的钩子方法调用会转发给链中的每一个 ChannelHandler对应的方法,(使用EventExcetutor事件处理器执行对应方法)

一个管道内公共同一个EventExcetutor事件处理器,由底层Channel().eventLoop()生成的

  • 对一个channel添加一个ChannelHandler 处理器时,就会封装为AbstracChannelHandlerContext加入之前的AbstracChannelHandlerContext形成拦截器链

* 一个EventLoop可以看做是一个线程执行器

  • 它也实现了java并发包下的executorSeevice接口,由自己的实现逻辑。

一个channel先要交于执行器去支持,那么需要把自己注册到指定的EventLoop(或EventLoopGroup)即可

  • EventLoop 本身只由一个线程驱动,其处理了一个 此Channel 的注册进来所有 I/O 事件,并且在该EventLoop 的整个生命周期内都不会改变。

Channel

* Channel 是 Java NIO 的一个基本构造。它的抽象和IO流程类似

它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作

* netty会基于JDK提供的Channel实现自己的

如netty 的NioSocketChannel就是基于nio 的SocketChannelImp实现的

* 顶层AbstractChannel有如下API

eventLoop 返回分配给 Channel 的 EventLoop

pipeline 返回分配给 Channel 的 ChannelPipeline

isActive 如果 Channel 是活动的,则返回 true。活动的意义可能依赖于底层的传输。例如,一个 Socket 传输一旦连接到了远程节点便是活动的

localAddress 返回本地的 SokcetAddress

remoteAddress 返回远程的 SocketAddress

write 将数据写到远程节点。这个数据将被传递给 ChannelPipeline,并且排队直到它被冲刷

flush 将之前已写的数据冲刷到底层传输,如一个 Socket

writeAndFlush 一个简便的方法,等同于调用 write()并接着调用 flush()

unsafe():见下,有关IO的操作最终都是落地到Unsafe

* 生命周期状态

ChannelUnregistered Channel 已经被创建,但还未注册到 EventLoop

ChannelRegistered Channel 已经被注册到了 EventLoop

ChannelActive Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了

ChannelInactive Channel 没有连接到远程节点

当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给 ChannelPipeline 中的 ChannelHandler

* 存有自己的属性表

可以通过chanel.attr进行设置和获取

* Channel都是默认开启自动读取模式的,即只要Channel是活跃的,读完一波数据之后就继续向Selector注册读事件,这样就可以连续不断地读取数据,

最终通过ChannelPipeline,传递到HeadContext节点。

处理器链

ChannelHandler

* 在 ChannelHandler被添加到 ChannelPipeline 中或者被从 ChannelPipeline 中移除时会调用这些操作

这些方法中的每一个都接受一个 ChannelHandlerContext 参数。

* ChannelHandler接口方法

handlerAdded 当把 ChannelHandler 添加到 ChannelPipeline 中时被调用,如ChannelInitializer就是重写了此方法,去调用自己的initChannel()方法

handlerRemoved 当从 ChannelPipeline 中移除 ChannelHandler 时被调用

exceptionCaught 当处理过程中在 ChannelPipeline 中有错误产生时被调用

* Netty 定义了下面两个重要的 ChannelHandler 子接口:

n ChannelInboundHandler------处理入站数据以及各种状态变化;

n ChannelOutboundHandler------处理出站数据并且允许拦截所有的操作。

* Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。

在内部,ChannelHandler 自己也使用了事件和 Future,使得它们也成为了你的应用程序将使用的相同抽象的消费者。

* Netty使用一个成员变量added标识一个ChannelHandler是否已经添加。如果当前要添加的Handler是非共享的,并且已经添加过,那么就抛出异常;否则,标识该Handler已经添加。

由此可见,一个Handler如果是支持共享的,就可以无限次被添加

到ChannelPipeline中。

*Channelhandler的生命周期回调方法

* ChannelInboundHandler 的方法

channelRegistered 当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用

channelUnregistered 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用

channelActive 当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪

channelInactive 当 Channel 离开活动状态并且不再连接它的远程节点时被调用

channelReadComplete 当Channel上的一个读操作完成时被调用

channelRead 当从 Channel 读取数据时被调用,表示有数据可读。

ChannelWritabilityChanged当 Channel 的可写状态发生改变时被调用。用户可以确保写操作不会完成得太快(以避免发生 OutOfMemoryError)

或者可以在 Channel 变为再次可写时恢复写入。可以通过调用 Channel 的 isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过 Channel.config().

setWriteHighWaterMark()和 Channel.config().setWriteLowWaterMark()方法来设置

userEventTriggered 当ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用,因为一个 POJO 被传经了 ChannelPipeline

* 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 将数据写到远程节点时被调用

注意write不触发底层chanel真正的写入,只有flush才会

* Netty为了确保线程的安全性,将确保出入站的操作只在Reactor线程中被执行,因为是在Reactor线程中执行的,所以直接调用HeadContext.fireChannelActive()方法

ChannelHandler 适配器

//ChannelHandlerAdapter 仅仅添加了 isSharable() 。如果其对应的实现被标注为 Sharable ,那么这个方法将返回 true ,表示它可以被添加到多个 ChannelPipeline
//ChannelInboundHandlerAdapter 调用了其相关联的 ChannelHandlerContext 上的等效方法,从而将事件转发到了 ChannelPipeline 中的下一个 ChannelHandler
// 这样自定义的 ChannelInboundHandler 只需要继承此类,重写对应方法即可,记得在自己的实现类里也需要对 ChannelHandlerContext 的传递
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {

@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelRegistered();
}

// 。。。。
}

@ChannelHandler.Sharable
public class LoggingHandler extends ChannelInboundHandlerAdapter {

//.....

@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
if (logger.isEnabled(internalLevel)) {
logger.log(internalLevel, format(ctx, "REGISTERED" ));
}
ctx.fireChannelRegistered();
}

}

* ChannelInboundHandler 和ChannelOutboundHandler资源管理

  • 当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,实现类需要自己将负责显式地释放与池化的 ByteBuf 实例相关的内存。

  • 如果一个消息被消费或者丢弃了,并且没有传递给 ChannelPipeline 中的下一个ChannelOutboundHandler,那么用户就有责任调用 ReferenceCountUtil.release()。

如果消息到达了实际的传输层,那么当它被写入时或者 Channel 关闭时,都将被自动释放

  • Netty提供了class ResourceLeakDetector它将对你应用程序的缓冲区分配做大约 1%的采样来检测内存泄露。相关的开销是非常小的。

*/
@Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 没有手动把 msg 传递给下一个时,那么自己就需要负责 msg 的释放
ReferenceCountUtil.release (msg);
}

}

@Sharable
public class DiscardOutboundHandler
extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx,
Object msg, ChannelPromise promise) {
// 没有手动把 msg 传递给下一个时,那么自己就需要负责 msg 的释放
// 如果参数存在监听赋值对象时,还需要调用其相关的终止方法,以触发注册的监听器
ReferenceCountUtil.release (msg);
promise.setSuccess();
}
}

* 事件和 ChannelHandler

  • Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发自定义的动作。

  • Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:

连接已被激活或者连接失活;

数据读取;

n用户事件;

n错误事件。

打开或者关闭到远程节点的连接;

n将数据写到或者冲刷到套接字。

每个事件都可以被分发给 ChannelHandler 类中的某个方法---这样就抽象了NIO的SelectKey,与只需处理好ChannelHandler 的方法实现即可

ChannelPipeline

* 每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。

在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。

* ChannelHandler链表

  • 这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个 ChannelHandler。执行顺序是由它们被添加的顺序所决定的。称为ChannelHandler 的编排顺序。

  • 注意从底层实现来看,是一条链表

  • 支持在代码中添加、删除或者替换其他的 ChannelHandler 来实时地修改ChannelPipeline 的布局,以做到热插拔

例如在客户端校验通过之后,我们不再需要AuthHandler这段逻辑,就使用删除这段逻辑

public class AuthHandler extends ChannelInboundHandlerAdapter {
public static final AuthHandler AUTH_HANDLER = new AuthHandler();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (SessionUtil.hasLogin (ctx.channel())) {
// 如果登录成功则移除该处理节点
ctx.pipeline().remove(this );
super .channelRead(ctx, msg);
} else {
// 没有成功的话需要关闭连接
ctx.channel().close();
}
}

}

* 一个 Netty 应用程序中入站和出站数据流之间的区别。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,

反之则称为入站的。如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部开始流动,并被传递给第一个 ChannelInboundHandler。

这个 ChannelHandler 不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。

最终,数据将会到达 ChannelPipeline 的尾端,届时,所有处理就都结束了。

* Netty在触发链表方法时,会区分好出站和入站两个大的类别的,过滤之后再调用对应的方法

Netty寻找下一个Inbound节点的过程是一个线性搜索的过程,它会遍历双向链表的下一个节点,直到下一个节点为Inbound。

@Override
public ChannelHandlerContext fireChannelRegistered() {
invokeChannelRegistered (findContextInbound());
return this ;
}

private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this ;
do {
ctx = ctx.next ;
} while (!ctx.inbound );
return ctx;
}

private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this ;
do {
ctx = ctx.prev ;
} while (!ctx.outbound );
return ctx;
}

* 在 Netty 中,有两种发送消息的方式。你可以直接写到Netty Channel 中,也可以 写到和 ChannelHandler相关联的ChannelHandlerContext对象中。

前一种方式将会导致消息从ChannelPipeline 的尾端开始流动,而后者将导致消息从 ChannelPipeline 中的下一个 ChannelHandler 开始流动。

* 提供了对ChannelHandler链表的事件的开始触发

  • 触发的方法对应了ChannelInboundHandler 和ChannelOutboundHandler接口的方法。

  • 例如在NioEventLoop的select线程中,当查询到可读事件,则会调用pipeline.fireChannelRead(byteBuf);

* ChannelHandlerContext

  • ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之间的关联,之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;

每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。

ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互

  • 因此ChannelHandlerContext 接口的方法就是ChannelInboundHandler + ChannelOutboundHandler接口

* 在运行时得以操作 ChannelPipeline 的 ChannelHandler,我们可以利用这一点来实现一些复杂的设计。例如,

  • 你可以通过将 ChannelHandler 添加到 ChannelPipeline 中来实现动态的协议切换。

  • 另一种高级的用法是缓存到 ChannelHandlerContext 的引用以供稍后使用,这可能会发 生在任何的 ChannelHandler 方法之外,甚至来自于不同的线程

* DefaultChannelPipeline的实现

  • 维护了链头和链尾两个结点

  • 维护拦截器链的增删改查

public class DefaultChannelPipeline implements ChannelPipeline {

//fireChannelActive fireChannelInactive fireChannelInactive fireExceptionCaught
// fireUserEventTriggered fireChannelReadComplete fireChannelWritabilityChanged
@Override
public final ChannelPipeline fireChannelRegistered() {
AbstractChannelHandlerContext.invokeChannelRegistered(head);
return this ;
}

@Override
public final ChannelPipeline fireChannelRead(Object msg) {
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this ;
}

//connect disconnect deregister 、、、
@Override
public final ChannelFuture bind(SocketAddress localAddress) {
return tail.bind(localAddress);
}

@Override
public final ChannelPipeline flush() {
tail.flush();
return this ;
}

@Override
public final ChannelPipeline read() {
tail.read();
return this ;
}

@Override
public final ChannelFuture write(Object msg) {
return tail.write(msg);
}

@Override
public final ChannelFuture writeAndFlush(Object msg) {
return tail.writeAndFlush(msg);
}

//..

}

* HeadContext节点

  • 同时属于Inbound和Outbound类型的Handler,实现了出站和入站的方法,大多数方法基于Unsafe类型实现,已经调用下一个ctx的fireXXX以广播事件

  • 主要作用就是作为ChannelPipeline的头节点,开始传递读事件,调用Unsafe进行实际的读写操作。

//HeadContext 链头
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
//io.netty.channel.socket.nio.NioSocketChannel.doWrite
unsafe.flush();
}

@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
unsafe .flush();
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();

readIfIsAutoRead();
}

* TailContext节点

  • 大部分作用为终止事件的传播(方法体为空)。除此之外,有两个重要的方法我们必须提一下,即exceptionCaught和channelRead

  • exceptionCaught实现

最终如果用户自定义ChannelHandler没有处理拦截exceptionCaught,则会落到TailContext节点,TailContext节点可不会简单地"吞下"这个异常,而是发出警告

-channelRead实现

TailContext节点在这个方法实现中:会发出一个警告,告诉你:"我已经将你未处理的数据丢掉了!,将未处理的对象尽心释放

* 异常处理

  • 一般该异常处理器需要加在自定义节点的末尾,此类ExceptionHandler一般继承自ChannelDuplexHandler,标识该节点既是一个Inbound节点,又是一个Outbound节点

  • 在AbstractChannelHandlerContext中出入站的方法实现中,会try捕捉异常,如果我们在自定义Handler中没有处理异常exceptionCaught,那么默认情况下该异常将一直传递下去,

遍历每一个节点,到最后一个处理器ExceptionHandler来终结这个异常。

* 一个标准的Pipeline链式结构如下图所示(我们省去了异常处理Handler)。

数据从head节点流入,先拆包,然后解码成业务对象,最后经过业务Handler处理,调用write,将结果对象写出去。而写的过程先通过tail节点,

然后通过Encoder节点将对象编码成ByteBuf,最后将该ByteBuf对象传递到head节点,调用底层的Unsafe写到JDK底层管道。

Unsafe

概述

* Unsafe和ChannelPipeline密切相关。ChannelPipeline中有关IO的操作最终都是落地到Unsafe

* 接口方法按功能可以分为分配内存、Socket四元组信息、注册事件循环、绑定端口、Socket的连接和关闭、Socket的读写,看得出来,这些操作都和JDK底层相关。

* 两种类型的Unsafe,一个是与连接的字节数据读写相关的NioByteUnsafe,一个是与新连接建立操作相关的NioMessageUnsafe。

  • NioByteUnsafe的读为核心功能:JDK的SelectableChannel的字节数据读取到Netty的ByteBuf中,为以下几个步骤。

1.通过Channel的ChannelConfig,获取ByteBuf分配器,用分配器来分配一个ByteBuf,ByteBuf是Netty里的字节数据载体。(一般为直接缓存)

2.将Channel中的数据读取到ByteBuf。

3.数据读完之后,调用pipeline.fireChannelRead(byteBuf)从HeadContext节点开始传播事件至整个ChannelPipeline

-NioMessageUnsafe的读操作很简单,就是调用JDK的accept()方法,新建立一条连接。

  • NioByteUnsafe的写。NioByteUnsafe中的写有两个方法,一个是write,一个是flush。

write是将数据添加到Netty的缓冲区,实际将字节流写到TCP缓冲区中的方法是flush,最终会委托到NioSocketChannel的doWrite方法。

-NioMessageUnsafe的写没有太大意义,

NioByteUnsafe的读

* 实现类为AbstractNioByteChannel的read方法,在事件循环loop中监听到read事件后会调用

为核心功能:JDK的SelectableChannel的字节数据读取到Netty的ByteBuf中,为以下几个步骤。

1.通过Channel的ChannelConfig,获取ByteBuf分配器,用分配器来分配一个ByteBuf,ByteBuf是Netty里的字节数据载体。(一般为直接缓存)

2.将Channel中的数据读取到ByteBuf。

3.数据读完之后,调用pipeline.fireChannelRead(byteBuf)从HeadContext节点开始传播事件至整个ChannelPipeline

write方法写队列

1.调用assertEventLoop确保该方法的调用在Reactor线程中。

2.调用filterOutboundMessage()方法,将待写入的对象过滤,把非ByteBuf对象和FileRegion过滤,把所有的非直接内存转换成直接内存DirectBuffer。

3.估算出需要写入的ByteBuf的size。

4.调用ChannelOutboundBuffer的addMessage(msg,size,promise)方法。加入待发送链表,Entry里包含了待写出ByteBuf及消息回调promise,

flush 方法 刷新写队列

不管调用channel.flush(),还是调用ctx.flush(),最终都会落地到Pipeline中的head节点,最终到AbstractUnsafe的flush()

  1. 组装获取待刷新的链表,调整后获取如下的flushedEntry指针
  2. 获得第一个需要flush的节点的数据
  3. 获得自旋锁的迭代次数(默认),即最多出来16个结点,如果还没处理完,退出选好,添加后续刷新的任务到IO线程即可
  4. 采用自旋方式将ByteBuf写出JDK NIO的Channel
  5. 一个节点的数据已经写入完毕,接下来就需要删除该节点。指针的移动,和entry的释放置空

ChannelFuture

(3)回调(钩子函数)

一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。

表示调用者,在后续期望被执行的功能,这也是异步的基础

(4)Future

*JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,

所以 Netty提供了它自己的实现ChannelFuture继承Future,用于在执行异步操作的时候使用。

* ChannelFuture主要提供能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(future),将会在对应的操作完成时被调用

然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。

简而言之 ,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。

* 每个 Netty Channel 的出站 I/O 操作都将返回一个 ChannelFuture;也就是说,它们都不会阻塞。正如我们前面所提到过的一样,Netty 完全是异步和事件驱动的。

Channel channel = new NioSocketChannel()

ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1" , 25));

future.addListener(new ChannelFutureListener() {

@Override

public void operationComplete(ChannelFuture future) {

if (future.isSuccess()) {

ByteBuf buffer = Unpooled.copiedBuffer("Hello" , Charset.defaultCharset ());

ChannelFuture wf = future.channel().writeAndFlush(buffer);

// ...

} else {

Throwable cause = future.cause();

cause.printStackTrace();

}

}

});

* ChannelFutureListener 添加到处理器链中即将作为参数传递给 ChannelOutboundHandler 的方法的ChannelPromise。

public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) {
if (!f.isSuccess()) {
f.cause().printStackTrace();
f.channel().close();
}
}
});
}
}

* 监听器的触发时机

  • 通过调用 ChannelPromise 上的 setSuccess()和 setFailure()方法,可以使一个操作的状态在 ChannelHandler 的方法返回给其调用者时便即刻被感知到。

  • 在Channel的一些方法内也会调用ChannelPromise 上的 setSuccess()和 setFailure()方法,例如在NioSocketChannel的shutdownOutput()里

  • 在ChannelHandler 的一些方法内也会

ByteBuf

(1)功能

使用不同的读索引和写索引来控制数据访问;

使用内存的不同方式------基于堆字节数组和直接缓冲区;

通过 CompositeByteBuf 生成多个 ByteBuf 的聚合视图;减少复制的消耗

数据访问方法------搜索、切片以及复制;

读、写、获取和设置 API;

ByteBufAllocator 池化和引用计数。

池化减少内存碎片。此实现使用了一种称为jemalloc的技术

  1. api

* 容量API

capacity()表示ByteBuf底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不同的底层实现机制有不同的计算方式,

maxCapacity()表示ByteBuf底层最大能够占用多少字节的内存

readableBytes()与isReadable():readableBytes()表示ByteBuf当前可读的字节数,它的值等于writerIndex-readerIndex,如果两者相等,则不可读,isReadable()方法返回false。

writableBytes()、isWritable()):writableBytes()表示ByteBuf当前可写的字节数,它的值等于capacity-writerIndex,如果两者相等,则表示不可写,isWritable()返回false,

但并不代表不能往ByteBuf写数据了。如果发现往ByteBuf写数据写不进去,Netty会自动扩容ByteBuf,直到底层的内存大小为maxCapacity,

maxWritableBytes()就表示可写的最大字节数,它的值等于maxCapacitywriterIndex。

* 读写指针相关的API

readerIndex()与readerIndex(int) 前者表示返回当前的读指针readerIndex,后者表示设置读指针。

writeIndex()与writeIndex(int) 前者表示返回当前的写指针writerIndex,后者表示设置写指针。

markReaderIndex()与resetReaderIndex() 前者表示把当前的读指针保存起来,后者表示把当前的读指针恢复到之前保存的值。下面两段代码是等价的。

* 读写API

writeBytes(byte\[\] src)与buffer.readBytes(byte\[\] dst) :writeBytes()表示把字节数组src里的数据全部写到ByteBuf,而readBytes()表示把ByteBuf里的数据全部读取到dst

writeByte(byte b)与buffer.readByte():writeByte()表示往ByteBuf中写一字节,而buffer.readByte()表示从 ByteBuf中读取一字节,

类似的API还有writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble(),

以及readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble()

读写API类似的API还有getBytes()、getByte()与setBytes()、setByte()系列,唯一的区别就是get、set不会改变读写指针,而read、write会改变读写指针

release()与retain():由于Netty一般使用了堆外内存,而堆外内存是不被JVM直接管理的。也就是说,申请到的内存无法被垃圾回收器直接回收,所以需要我们手动回收

Netty的ByteBuf是通过引用计数的方式管理的,如果一个ByteBuf没有地方被引用到,则需要手动回收底层内存。在默认情况下,当创建完一个

ByteBuf时,它的引用为1,然后每次调用retain()方法,它的引用就加一,release()方法的原理是将引用计数减一,减完之后如果发现引用计数为0,

则直接回收ByteBuf底层的内存。

当向ByteBuf中写数据的时候,如果发现容量不足,则进行扩容,直到扩容到maxCapacity,超过这个数,就抛出异常。

* slice()、duplicate()、copy():三者的返回值分别是一个新的ByteBuf对象。

slice()方法从原始ByteBuf中截取一段,这段数据是从readerIndex到writeIndex的,同时,返回的新的ByteBuf的最大容量maxCapacity为原始ByteBuf的readableBytes()。

duplicate()方法把整个ByteBuf都截取出来,包括所有的数据、指针信息。

slice()和duplicate()底层内存及引用计数与原始ByteBuf共享,也就是说,经过slice()方法或者duplicate()方法返回的ByteBuf调用write系列方法都会影响到原始ByteBuf,

但是它们都维持着与原始ByteBuf相同的内存引用计数和不同的读写指针。不会复制数据,它们只是通过改变读写指针来改变读写的行为

retainedSlice()与retainedDuplicate():相信读者应该已经猜到这两个API的作用了,它们的作用是在截取内存片段的同时,增加内存的引用计数

copy()会直接从原始ByteBuf中复制所有的信息,包括读写指针及底层对应的数据,因此,往copy()方法返回的ByteBuf中写数据不会影响原始ByteBuf。

  1. 实现思路

ByteBuf完全是netty自己独立的一套字节缓存设计,当需要与原生JDK进行交互式,会先把此缓存转为JDK ByteBuff后在处理

public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {

private final ByteBufAllocator alloc ;
byte \[\] array ;
private ByteBuffer tmpNioBuf ;

protected UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
super (maxCapacity);
this .alloc = alloc;
// 设置为 byte\[\] array = new byteinitialCapacity;
setArray(allocateArray(initialCapacity));
// 设置 this.readerIndex = 0;this.writerIndex = 0;
setIndex(0, 0);
}

// 方法都是基于底层的 array 进行操作
public ByteBuf setBytes(int index, byte \[\] src, int srcIndex, int length) {
checkSrcIndex(index, length, srcIndex, src.length );
System.arraycopy (src, srcIndex, array , index, length);
return this ;
}

// 如果是跟 ByteBuffer 相关的方法,需要对底层缓存源进行包装为 ByteBuffer 在进行处理
private ByteBuffer internalNioBuffer() {
ByteBuffer tmpNioBuf = this .tmpNioBuf ;
if (tmpNioBuf == null ) {
this .tmpNioBuf = tmpNioBuf = ByteBuffer.wrap (array );
}
return tmpNioBuf;
}

public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
ensureAccessible();
try {
return in.read((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length));
} catch (ClosedChannelException ignored) {
return -1;
}
}

}

EventLoop和线程模型

概述

* EventLoop 接口

运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编程上的构造通常被称为事件循环

EventExecutor

* EventLoop 特点

  • 一个 EventLoopGroup 包含一个或者多个 EventLoop;

EventLoopGroup 负责为每个新创建的 Channel 从池中分配一个 EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布;

Netty通过判断NioEventLoopGroup中的NioEventLoop的个数是否是2的幂来创建不同的线程选择器,

  • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;这个线程由底层的线程池executor提供

默认是由ThreadPerTaskExecutor创建的,ThreadPerTaskExecutor每次执行execute(对于EventLoop 来说只会调用一次)的时候都会创建一个FastThreadLocalThread的线程实体。

  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;

同时任务(Runnable 或者 Callable)可以直接提交给 EventLoop 实现,以立即执行或者有序调度执行

事件/任务的执行顺序 事件和任务是以先进先出(FIFO)的顺序执行的。这样可以通过保证字节内容总是按正确的顺序被处理,消除潜在的数据损坏的可能性

  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop(即一个线程);netty Channel存在一个标志boolean registered,多次出错会报错

  • 一个 EventLoop 可能会被分配给一个或多个 Channel。EventLoop可以通过selectedKeys上的k.attachment()方法获取对应的AbstractNioChannel

这种情况下对于所有相关联的 Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。

然而,在一些无状态的上下文中,它仍然可以被用于在多个 Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件

* 线程模型

  • 在以前的版本中所使用的线程模型只保证了入站(之前称为上游)事件(如read)会在所谓的 I/O 线程中执行

所有的出站(下游)事件(如write)都由调用线程处理,其可能是 I/O 线程也可能是别的线程。开始看起来这似乎是个好主意,但是已经被发现是有问题的,

因为需要在 ChannelHandler 中对出站事件进行仔细的同步。简而言之,不可能保证多个线程不会在同一时刻尝试访问出站事件。

例如,如果你通过在不同的线程中调用 Channel.write();

或者

当出站事件触发了入站事件时,将会导致另一个负面影响。当 Channel.write()方法导致异常时,需要生成并触发一个 exceptionCaught 事件。

但是在 Netty 3 的模型中,由于这是一个入站事件,需要在调用线程中执行代码,然后将事件移交给 I/O 线程去执行,然而这将带来额外的上下文切换。

  • Netty 4 中所采用的线程模型,通过在同一个线程中处理某个给定的 EventLoop 中所产生的所有事件,解决了这个问题。

这提供了一个更加简单的执行体系架构,并且消除了在多个ChannelHandler 中进行同步的需要(除了任何可能需要在多个 Channel 中共享的)

  • 在一般的设置中,使用两个EventLoopGroup,一个用于处理accept事件,一个处理一个新建的socketChannle的所有事件(包括此IO事件、处理器链的执行)

  • 在默认情况下,Netty在启动的时候会开启两倍CPU核数个NIO线程,

EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于接收连接请求的组
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理连接的数据读写的组
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class )

//...

* EventLoop 的执行逻辑

  • 如果(当前)调用线程正是支撑 EventLoop 的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。

当 EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。这也就解释了任何的 Thread 是如何与 Channel 直接交互而无需在 ChannelHandler 中进行额外同步的。

即任务使用eventLoop().execute(task)来执行的任务都会按序执行,即线程安全

  • 注意,每个 EventLoop 都有它自已的任务队列,独立于任何其他的 EventLoop。这是 Netty 线程模型的关键组成部分

  • "永远不要将一个长时间运行的任务放入到执行队列中,因为它将阻塞需要在同一线程上执行的任何其他任务。"如

果必须要进行阻塞调用或者执行长时间运行的任务,我们建议使用一个专门的EventExecutor但有时可能需要与那些使用阻塞 API 的遗留代码进行交互。

对于这种情况,ChannelPipeline 有一些接受一个 EventExecutorGroup 的方法addFirst(EventExecutorGroup group, String name, ChannelHandler handler)。

如果一个事件被传递给一个自定义的 EventExecutor-Group,它将被包含在这个 EventExecutorGroup 中的某个 EventExecutor 所处理,

从而与Channel 本身的 EventLoop 时隔离的。对于这种用例,Netty 提供了一个叫 DefaultEventExecutorGroup 的默认实现。

//AbstractChannelHandlerContext
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelActive();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelActive();
}
});
}
}

* 对于耗时的操作,我们需要把这些耗时的操作都丢到业务线程池中去处理

NioEventLoop 的实现

任务队列

* 一个EventLoop可以看做是一个线程执行器

  • 它也实现了java并发包下的executorSeevice接口,由自己的实现逻辑。

一个channel先要交于执行器去支持,那么需要把自己注册到指定的EventLoop(或EventLoopGroup)即可

  • EventLoop 本身只由一个线程驱动,其处理了一个 此Channel 的注册进来所有 I/O 事件,并且在该EventLoop 的整个生命周期内都不会改变。

  • .在默认情况下,NioEventLoopGroup会创建两倍CPU核数个NioEventLoop

* NioEventLoop的创建,最关键的其实就是两部分:实现run方法,这个底层的线程会执行,重写父类的newTaskQueue创建一个MPSC队列

  • run方法需要处理channel上的事件,已经处理MPSC队列上的任务

  • Netty的MPSC队列直接使用的JCTools,可以说Netty的高性能,很大程度上功劳要归功于这个工具包,感兴趣的读者可以了解一下

* 执行NioEventLoop.execute(),会执行父类的SingleThreadEventExecutor的方法

//SingleThreadEventExecutor
@Override
public void execute(Runnable task) {
boolean inEventLoop = inEventLoop();
// 由于 execute 方法是 public ,因此可能被用户代码使用。比如,我们
// 经常使用 ctx.executor().execute(...) ,所以,这里又进行了一次外部线程
// 判断逻辑,确保执行 task 不会遇到线程安全问题
if (inEventLoop) {
addTask(task);
} else {
//Netty 会判断 Reactor 线程有没有被启动。如果没有被启动,则调用 doStartThread 方法启动工作线程。
// 即会调用 NioEventLoop run 方法
startThread();
//taskQueue.offer(task); 加入工作队列
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}

if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}

run方法整体流程

@Override
protected void run() {
for (;;) {

//select IO 事件
select(wakenUp.getAndSet(false ));
// 处理 IO 事件
processSelectedKeys();
// 执行队列上的任务
runAllTasks();

//...
}
}

* select操作也是一个for循环

  • 在for循环第一步中,如果发现当前的定时任务队列中有任务的截止时间快到了(<=0.5ms),就跳出循环。

此外,跳出之前,如果发现目前为止还没有进行过select操作(if (selectCnt ==0)),那么就调用一次selectNow(),该方法会立即返回,不会阻塞。

  • select轮询过程中发现有任务加入,中断本次轮询。

  • 说明Netty任务队列里的队列为空,并且所有定时任务的延迟时间还未到(大于0.5ms)。于是,进行一次阻塞select操作,截止到第一个定时任务的截止时间

如果其他线程支持NioEventLoop.execute(task)会执行会调用wakeup方法来唤醒

  • 每一次select线程醒来之后,都会进行计数+1,当空轮询的次数超过一定阈值(默认是512)的时候,就开始重建Selector

如果select醒来的时间小于阻塞设置的超时时间,或者由其他线程唤醒,那么此计数会重置为1

* 存在事件就绪了

  • Selector在调用select()族方法的时候,如果有IO事件发生,就会往里面的两个成员变量中塞相应的SelectionKey,即相当于往HashSet中添加元素

既然Netty通过反射将JDK中的两个成员变量替换掉为自己实现的set对象,即SelectedSelectionKeySet

优化点在于底层使用数组来实现,这样Netty只需要O(1)的时间复杂度就能将SelectionKey塞到set中去,

而JDK底层使用的HashSet put的时间复杂度最少是O(1),最差是O(n),使用数组替换掉HashSet还有一个好处是遍历的时候非常高效。

  • 每满256次次连接断开,就会将SelectedKeys的内部数组全部清空,方便JVM垃圾回收,然后调用selectAgain重新填装SelectionKeys数组

  • 每个SelectionKey上都绑定了Netty类AbstractChannel对象作为attachment,在处理每个SelectionKey的时候,都可以找到AbstractChannel

* 执行任务

  • 添加任务的场景
  1. 执行NioEventLoop的execute(task)方法时候,都会把任务进行入队
  2. 在连接器链执行调用Channel的各类方法。等方法时,如果判断当前是IO本线程那么会执行方法,否会创建一个任务执行此方法,任务入队
  • 添加定时任务的场景

存在一个scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>();优先级队列存储定时任务,在执行NioEventLoop的scheduled(task)任务时

  1. 如果判断当前是IO本线程那么会直接把任务加入scheduledTaskQueue
  2. 如果是在外部线程调用schedule方法,Netty会将添加定时任务这个逻辑封装成一个普通的task,这个task的任务是一个添加"添加定时任务"的任务,而不是添加定时任务,

所以其实就退回到第二种场景。这样,对PriorityQueue的访问就变成单线程,即只有Reactor线程会访问,因此,不存在多线程并发问题。

注:先比较任务的截止时间;在截止时间相同的情况下,再比较ID,即任务添加的顺序;如果ID再相同,就抛出Error。

这样,在执行定时任务的时候,就能保证截止时间最近的任务先执行。

* IO线程开始执行任务runAllTask(timeoutNanos),timeoutNanos表示最多执行多长时间

  • 将快到期的定时任务转移到MPSC Queue里面。

  • 基于参数的timeoutNanos计算一下本轮任务方法最多可以执行到什么时候。

  • 开始执行任务taskQueue.poll()

执行task的run方法,忽略任何异常,将已运行任务runTasks加一。接着,每隔0x3F(64)个任务,判断当前时间是否超过本次Reactor任务循环的截止时间。

如果超过,那就停止本轮任务的执行;如果没有超过,那就继续执行。最后,如果任务全部执行完毕,则记录下最后一次任务执行时间。

客户端连接接入流程解析

不管是boss线程还是worker线程,所做的事情均分为以下3个步骤。

1.轮询注册在Selector上的IO事件。

2.处理IO事件。

3.执行异步Task。

对于boss线程来说,第一步轮询出来的基本都是ACCEPT事件,表示有新的连接;而worker线程轮询出来的基本都是read或write事件,表示网络的读写事件。

新连接接入的总体流程

简单来说,新连接的接入流程可以分为3个过程。

1.检测到有新连接。

2.将新连接注册到worker线程。

3.注册新连接的读事件。

检测到有新连接

* 当调用bind方法启动服务端之后,服务端的Channel,即NioServerSocketChannel,已经注册到boss Reactorboss线程组,

Reactor线程不断检测是否有新的事件,直到检测出有ACCEPT事件发生

* 轮询到SelectionKey.OP_ACCEPT事件,即表明有新连接进入,此时将调用Channel的Unsafe来进行实际的操作:

  • 创建NioSocketChannel,封装底层NIO SocketChannel,包括Pipeline、Unsafe等

  • 设置该通道为非阻塞模式

  • 调用NioServerSocketChannel的pipeline.fireChannelRead来拦截执行这个新channel,即ServerBootstrapAcceptor处理器

  1. 添加用户自定义childHandler
  2. 设置ChannelOption和ChannelAttr
  3. 绑定Reactor线程。使用childGroup.register(child)注册到工作线程

将NioSocketChannel绑定到Reactor工作线程的Selector上,这样,后续该NioSocketChannel所有的事件都由绑定的Reactor线程的Selector来轮询。

配置自定义Handler,且在自己的管道传播ChannelRegistered事件

注册读事件

引导Boostrap

option()和childOption方法

option()方法可以给服务端Channel设置一些TCP参数,最常见的就是so_backlog,设置如下。

这个设置表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,则可以适当调大这个参数。

childOption()方法

childOption()方法可以给每个连接都设置一些TCP参数。上述代码中设置了两种TCP参数,其中:

● ChannelOption.SO_KEEPALIVE表示是否开启TCP底层心跳机制,true表示开启。

● ChannelOption.TCP_NODELAY表示是否开启Nagle算法,true表示关闭,false表示开启。通俗地说,如果要求高实时性,有数据发送时就马上发送,就设置为关闭;

如果需要减少发送次数,减少网络交互,就设置为开启。

整体流程及基本原理

public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于接收连接请求的组
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理连接的数据读写的组
final AttributeKey<Integer> id = new AttributeKey<Integer>("ID" );
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class )//ServerChannel 的处理器
.childHandler(new ChannelInitializer<SocketChannel>() {// 每一新建的 SocketChannel 的处理器,设置用于处理子 Channel I/O 及数据的 ChannelInboundHandler
// 一旦 Channel 被注册到了它的 EventLoop 之后,就会调用你的
//initChannel() 版本。在该方法返回之后, ChannelInitializer 的实例将会从 ChannelPipeline 中移除它自己。
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 添加多个处理数据的业务 handler
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE ));
}
})
// 设置 tcp 参数 或者 .attr()
// 这些选项将会通过 bind() 方法设置到 Channel 。在 bind() 方法
// 被调用之后,设置或者改变 ChannelOption 都不会有任何的效果
.option(ChannelOption.SO_BACKLOG , 128)
.childOption(ChannelOption.SO_KEEPALIVE , true ) // 设置连接保活
.attr(id, 123456);

// 绑定端口 并初始化服务端 Channel (设置Channel的Option与Attr) ,开始接收连接
// 1 、创建 NioServerSocketChannel ,在器构造方法内

// - id是Netty中每条Channel的唯一标识,类似Snowflake算法,通过机器号、进程号、时间戳、随机数等方式生成。
// - unsafe = newUnsafe(); 提供的方法最终调用的是NioServerSocketChannel或者其父类中对应的方法
// - pipeline = newChannelPipeline();
// - 底层 SctpServerChannel.open();
// - this.readInterestOp = SelectionKey.OP_ACCEPT;
// - ch.configureBlocking(false);
// 2 、为此 NioServerSocketChannel 添加一个内部的处理器 ChannelInitializer
// 主要实现了 initChannel 方法, 在 addLast 里面会创建 context 加入 NioServerSocketChannel 的拦截器链,同时会立即执行此 initChannel
// 会给 bossGroup 里的随机挑选一个执行器 EventLoop 加入 ServerBootstrapAcceptor 处理器
// p.addLast(new ChannelInitializer<Channel>() {
// @Override
// public void initChannel(final Channel ch) throws Exception {
// final ChannelPipeline pipeline = ch.pipeline();
// ChannelHandler handler = config.handler();
// if (handler != null) {
// pipeline.addLast(handler);
// }
//
// ch.eventLoop().execute(new Runnable() {
// @Override
// public void run() {
// pipeline.addLast(new ServerBootstrapAcceptor(
// ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
// }
// });
// }
// });
// ServerBootstrapAcceptor 处理器主要实现了 public void channelRead(ChannelHandlerContext ctx, Object msg) ;
// msg 为每一个连接创建 SocketChannel ,会为此 SocketChannel 加入配置的 childHandler ,并注册到 workerGroup
// 若指定了子 EventLoop 组,那么此 SocketChannel 较交于子 EventLoop 组处理
// 2 、会给 bossGroup 里的随机挑选一个执行器 EventLoop ,使用 ChannelFuture 来封装处理 AbstractUnsafe.register0 任务
// - 执行 selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), SelectionKey.OP_ACCEPT | 0 , this);

会把 NioServerSocketChannel对象当作attachment绑定到JDK的Selector上
// - 执行处理器链的 pipeline.fireChannelRegistered(); pipeline.fireChannelActive();
// 3 当第2步后(存在 ChannelFuture 添加监听器,钩子的内容 如doBind0 ), 执行doBind0
// - 会执行拦截处理器链的 bind(localAddress, promise);

处理器链头的HeadContext实现了bind为NioMessageUnsafe的bind()方法会实际绑定端口和传播fireChannelActive事件
// 4 、支持自动通知的任务接口 ChannelPromise ,它是 Future 的子接口,底层关联一个具体的 Channel ,和一个状态变量 result 以及关联监听器,提供触发监听器的方法
// 因此 NioSctpChannel 其内就组合了这个 ChannelPromise ,在 NioSctpChannel 的各个生命周期方法里都会触发 ChannelPromise 对应的方法
// 从而触发用户注入的监听器,
ChannelFuture f = b.bind(port ).sync();

// 等待服务器 socket 关闭
f.channel().closeFuture().sync();
} finally {
// 关闭事件循环组 EventLoopGroup ,释放资源 , 关闭 EventLoopGroup ,它将处理任何挂起的事件和任务
//shutdownGracefully() 方法也是一个异步的操作,所以你需要阻塞等待直到它完成,或者向
// 所返回的 Future 注册一个监听器以在关闭完成时获得通知
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

}
}

编码器 和解码器

如果将消息看作是对于特定的应用程序具有具体含义的结构化的字节序列---它的数据。那

么编码器是将消息转换为适合于传输的格式(最有可能的就是字节流);而对应的解码器则是将

网络字节流转换回应用程序的消息格式。因此,编码器操作出站数据,而解码器处理入站数据。

粘包与拆包

* 为什么会粘包

首先你得了解一下TCP/IP协议,在用户数据量非常小的情况下,比如1字节,该TCP数据包的有效载荷非常低,传递100字节的数据,需要100次TCP传送、100次ACK,

在应用及时性要求不高的情况下,将这100个有效数据拼接成一个数据包,就会缩短到一个TCP数据包,以及一个ACK,提高了有效载荷,也节省了带宽。

在非极端情况下,有可能两个数据包拼接成一个数据包,也有可能一个半的数据包拼接成一个数据包,也有可能两个半的数据包拼接成一个数据包。

* 为什么要拆包

拆包和粘包是相对的,一端粘了包,另外一端就需要将粘过的包拆开。举个例子,发送端将三个数据包粘成两个数据包发送到接收端,接收端就需要根据应用协议将两个数据包重新组装

成三个数据包。还有一种情况就是用户数据包超过了mss(最大报文长度),那么这个数据包在发送的时候必须拆分成几个数据包,接收端收到这些数据包之后需要将这些数据包粘合之

后再拆开。

解码器

* 解码器是负责将入站数据从一种格式转换到另一种格式的。这些类覆盖了两个不同的用例:

n 将字节解码为消息------ByteToMessageDecoder 和ReplayingDecoder;

n 将一种消息类型解码为另一种------MessageToMessageDecoder。

* ByteToMessageDecoder接口抽象方法

  • decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out)

这是你必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的 ByteBuf,以及一个用来添加解码结果的 List。

对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该 List,或者该 ByteBuf 中没有更多可读取的字节时为止。

然后,如果该 List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个 ChannelInboundHandler

  • decodeLast(ChannelHandlerContext ctx,ByteBuf in,List<Object> out)

Netty提供的这个默认实现只是简单地调用了decode()方法。当Channel的状态变为非活动时,这个方法将会被调用一次。可以重写该方法以提供特殊的处理

* 什么时候会用到解码器呢?很简单:每当需要为 ChannelPipeline 中的下一个 ChannelInboundHandler 转换入站数据时会用到。此外,得益于 ChannelPipeline 的设计,可以将

多个解码器链接在一起,以实现任意复杂的转换逻辑,----因为解码器本身就是一个InboundHandler

* 基本原理如下(ByteToMessageDecoder )

在编码器基类的channelRead0(ChannelHandlerContext ctx, Objetct msg)

  • 存在一个本地缓存--累加字节器,用于存储待加工的数据

首次的情况下,直接将累加器的指针指向新读取的数据msg;

否则调用累加器把当次read时间的字节数据累加数据至字节容器,为什么要累加,因为上次解码后,后续一些字节,是不能抛弃的

  • 将累加的数据传递给业务进行拆包,里面会调用子类的encode抽象方法进行业务拆包完成之后,如果发现并没有拆到一个完整的数据包,这个时候又分成两种情况。

1.一种是拆包器什么数据也没读取,可能数据还不够业务拆包器处理,通过break关键字跳出循环。

2.另一种是拆包器已读取部分数据,说明解码器仍然在工作,继续解码。

3.把解码后的实体加入CodecOutputList out集合中

  • 及时清理字节容器

业务拆包完成之后,只是从字节容器中取走了数据,但是这部分空间对于字节容器来说依然被保留着,而字节容器每次累加字节数据的时候都是将字节数据追加到尾部,

如果不对字节容器做清理,那么时间一长就会出现OOM问题

  1. 如果字节容器当前已无数据可读取,则直接销毁字节容器,并且标注当前字节容器一次数据也没读取。
  2. 如果连续读取16次(discardAfterReads的默认值),字节容器中仍然有未被业务拆包器读取的数据,那么就做一次压缩,把有效数据段整体移到容器首部。
  • 如果CodecOutputList out集合不为空,遍历每一个业务数据包,然后调用fireChannelRead将拆到的业务数据包都传递到后续的Handler。

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {

//....
ByteBuf cumulation ;
private Cumulator cumulator = MERGE_CUMULATOR;
private boolean decodeWasNull ;
private boolean first ;

//IO 可读事件到达后,就会触发此方法(多次)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null ;
if (first ) {
cumulation = data;
} else {
// 如果缓存的 cumulation 还有剩余字节,这个是需要保存的,新的 channelRead 进来的字节会拼接再其后面
cumulation = cumulator .cumulate(ctx.alloc(), cumulation , data);
}
// 调用子类抽象 decode(ctx, in, out) --见下ToIntegerDecoder ; ,对缓存的字节进行编码,把编码结果加入 out 集合中
callDecode(ctx, cumulation , out);
} catch (DecoderException e) {
throw e;
} catch (Throwable t) {
throw new DecoderException(t);
} finally {
// 如果本次编码的内容,刚刚(或者部分)把 cumulation 内容都消耗了,那么就进行内存释放
if (cumulation != null && !cumulation .isReadable()) {
numReads = 0;
cumulation .release();
cumulation = null ;
} else if (++numReads >= discardAfterReads) {
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
// 如果存在编码成功的 out 结果,那么就把 out 传递给处理器链的下一个 ctx.fireChannelRead(outi)
fireChannelRead (ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}

//...

}

*/
public class ToIntegerDecoder extends ByteToMessageDecoder {

private static final int MAX_FRAME_SIZE = 1024;

@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in,
List<Object> out) throws Exception {
// 进行缓大小的限制,设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一
// TooLongFrameException (随后会被 ChannelHandler.exceptionCaught() 方法捕
// 获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如 HTTP )可能允许你
// 返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。
if (in.readableBytes() > MAX_FRAME_SIZE ) {
in.skipBytes(readable);
throw new TooLongFrameException("Frame too big!" );
}
if (in.readableBytes() >= 4) {
out.add(in.readInt());
}
}
}

编码器

* 解码器的功能正好相反。Netty 提供了一组类,用于帮助你编写具有以下功能的编码器:

n MessageToByteEncoder将消息编码为字节;

n MessageToMessageEncoder将消息编码为消息

* MessageToByteEncoder

  • encode(ChannelHandlerContext ctx,I msg,ByteBuf out)

encode()方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为 ByteBuf 的(类型为 I 的)出站消息。

该 ByteBuf 随后将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler

* 基本原理

public abstract class MessageToByteEncoderxx<I> extends ChannelOutboundHandlerAdapter {

private final TypeParameterMatcher matcher ;
private final boolean preferDirect ;

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null ;
try {
// 泛型 I 表示原类型,这里需要校验一下
if (acceptOutboundMessage(msg)) {
I cast = (I) msg;
// 创建一个空的 ByteBuf
buf = allocateBuffer(ctx, cast, preferDirect );
try {
// 调用子类的方法,把原对象转为字节后,加入这个空的 ByteBuf
encode(ctx, cast, buf);
} finally {
ReferenceCountUtil.release (cast);
}

// 无论空的 ByteBuf 是否有内容了,都需要触发处理器链的下一个
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER , promise);
}
buf = null ;
} else {
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null ) {
buf.release();
}
}
}

}

public class ShortToByteEncoder extends MessageToByteEncoder<Short> {
@Override
public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out)
throws Exception {
out.writeShort(msg);
}
}

基于分隔符的解码器

描 述

DelimiterBasedFrameDecoder 使用任何由用户提供的分隔符来提取帧的通用解码器

LineBasedFrameDecoder 提取由行尾符(\n 或者\r\n)分隔的帧的解码器。这个解码

器比 DelimiterBasedFrameDecoder 更快

原理和编码器类似

*/
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel>
{
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
pipeline.addLast(new FrameHandler());
}

public static final class FrameHandler
extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelRead0(ChannelHandlerContext ctx,
ByteBuf msg) throws Exception {
// Do something with the data extracted from the frame
}
}
}

基于长度的解码器

描 述

FixedLengthFrameDecoder 提取在调用构造函数时指定的定长帧

LengthFieldBasedFrameDecoder 根据编码进帧头部中的长度值提取帧;该字段的偏移量以及长度在构造函数中指定

如下图为new LengthFieldBasedFrameDecoder(64 * 1024, 0, 2));

HTTP 相关处理器

HTTP 解码器

* HttpServerCodec 将字节解码为 HttpRequest、HttpContent 和 LastHttpContent。并将 HttpRequest、HttpContent 和 LastHttpContent 编码为字节

  • 其内封装了HttpServerRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize),new HttpServerResponseEncoder());

  • HttpRequestEncoder 将HttpRequest、HttpContent 和 LastHttpContent 消息编码为字节

HttpResponseEncoder 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节

HttpRequestDecoder 将字节解码为HttpRequest、HttpContent 和 LastHttpContent 消息

HttpResponseDecoder 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息

* HttpObjectAggregator 将一个 HttpMessage 和跟随它的多个 HttpContent 聚合为单个 FullHttpRequest 或者 FullHttpResponse(取决于它是被用来处理请求还是响应)。安装了这个之后,

ChannelPipeline 中的下一个 ChannelHandler 将只会收到完整的 HTTP 请求或响应

用于空闲连接以及超时的 ChannelHandler

名 称 描 述

* IdleStateHandler 当连接空闲时间太长时,将会触发一个 IdleStateEvent 事件。然后,

你可以通过在你的 ChannelInboundHandler 中重写 userEventTriggered()方法来处理该 IdleStateEvent 事件

ReadTimeoutHandler 如果在指定的时间间隔内没有收到任何的入站数据,则抛出一个 ReadTimeoutException 并关闭对应的 Channel。

可以通过重写你的ChannelHandler 中的 exceptionCaught()方法来检测该 ReadTimeoutException

WriteTimeoutHandler 如果在指定的时间间隔内没有任何出站数据写入,则抛出一个 WriteTimeoutException 并关闭对应的 Channel 。

可以通过重写你的ChannelHandler 的 exceptionCaught()方法检测该 WriteTimeoutException

* IdleStateHandle基本原理

在每次channelReadComplete后记录最后一次读的时间。后台有一个定时任务(空闲时间),任务到期启动时就会判断当前时间到最后一次读的时间的时间

是否超过指定的空闲时间,是的话就触发userEventTriggered以传递IdleStateEvent事件

*/
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel>
{
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(
new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS ));
pipeline.addLast(new HeartbeatHandler());
}

public static final class HeartbeatHandler
extends ChannelInboundHandlerAdapter {
// 发送到远程节点的心跳消息
private static final ByteBuf HEARTBEAT_SEQUENCE =
Unpooled.unreleasableBuffer (Unpooled.copiedBuffer (
"HEARTBEAT" , CharsetUtil.ISO_8859_1 ));
@Override
public void userEventTriggered(ChannelHandlerContext ctx,
Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
// 发送心跳消息,添加监听在发送失败时关闭该连接
ctx.writeAndFlush(HEARTBEAT_SEQUENCE .duplicate())
.addListener(
ChannelFutureListener.CLOSE_ON_FAILURE );
} else {
// 不是 IdleStateEvent 事件,所以将它传递给下一个 ChannelInboundHandler
super .userEventTriggered(ctx, evt);
}
}
}
}

其他处理器

零拷贝

NIO 的零拷贝特性,这种特性消除了将文件

的内容从文件系统移动到网络栈的复制过程。所有的这一切都发生在 Netty 的核心中,所以应用

程序所有需要做的就是使用一个 FileRegion 接口的实现

*/
public class FileRegionWriteHandler extends ChannelInboundHandlerAdapter {
private static final Channel CHANNEL_FROM_SOMEWHERE = new NioSocketChannel();
private static final File FILE_FROM_SOMEWHERE = new File("" );

@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
File file = FILE_FROM_SOMEWHERE ; //get reference from somewhere
Channel channel = CHANNEL_FROM_SOMEWHERE ; //get reference from somewhere
//...
FileInputStream in = new FileInputStream(file);
FileRegion region = new DefaultFileRegion(
in.getChannel(), 0, file.length());
channel.writeAndFlush(region).addListener(
new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future)
throws Exception {
if (!future.isSuccess()) {
Throwable cause = future.cause();
// Do something
}
}
});
}
}

ChunkedWriteHandler

* 需要将数据从文件系统复制到用户内存中时,可以使用 ChunkedWriteHandler,它支持异步写大型数据流,而又不会导致大量的内存消耗。

* 关键是 interface ChunkedInput<B>,其中类型参数 B 是 readChunk()方法返回的类型。Netty 预置了该接口的 4 个实现,

ChunkedFile 从文件中逐块获取数据,当你的平台不支持零拷贝或者你需要转换数据时使用

ChunkedNioFile 和 ChunkedFile 类似,只是它使用了 FileChannel

ChunkedStream 从 InputStream 中逐块传输内容

ChunkedNioStream 从 ReadableByteChannel 中逐块传输内容

* Channel 的状态变为活动的时,WriteStreamHandler 将会逐块地把来自文件中的数据作为 ChunkedStream 写入。数据在传输之前将会由 SslHandler 加密。

*/
public class ChunkedWriteHandlerInitializer
extends ChannelInitializer<Channel> {
private final File file ;
private final SslContext sslCtx ;
public ChunkedWriteHandlerInitializer(File file, SslContext sslCtx) {
this .file = file;
this .sslCtx = sslCtx;
}

@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new SslHandler(sslCtx .newEngine(ch.alloc())));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new WriteStreamHandler());
}

public final class WriteStreamHandler
extends ChannelInboundHandlerAdapter {

@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
super .channelActive(ctx);
ctx.writeAndFlush(
new ChunkedStream(new FileInputStream(file )));
}
}
}

WebSocket

基本原理

WebSocket规范以及它的实现代表了对一种更加有效的解决方案的尝试。简单地说,

WebSocket提供了"在一个单个的TCP连接上提供双向的通信......结合WebSocket API......它为网

页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。"

也就是说,WebSocket 在客户端和服务器之间提供了真正的双向数据交换。我们不会深入地

描述太多的内部细节,但是我们还是应该提到,尽管最早的实现仅限于文本数据,但是现在已经

不是问题了;WebSocket 现在可以用于传输任意类型的数据,很像普通的套接字。

使用WebSocket的应用程序将始终以HTTP/S作为开始,

然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后。

我们的应用程序将采用下面的约定:如果被请求的 URL 以/ws 结尾,那么我们将会把该协议升级为 WebSocket;否则,服务器将使用基本的 HTTP/S。

在连接已经升级完成之后,所有数据都将会使用 WebSocket 进行传输。

当 WebSocket 协议升级完成之后,WebSocketServerProtocolHandler 将会把 HttpRequestDecoder 替换为 WebSocketFrameDecoder,把 HttpResponseEncoder 替换为

WebSocketFrameEncoder。为了性能最大化,它将移除任何不再被 WebSocket 连接所需要的ChannelHandler

代码

ChatServer引导

*/
public class ChatServer {
private final ChannelGroup channelGroup =
new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE );
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel ;

public ChannelFuture start(InetSocketAddress address) {
ServerBootstrap bootstrap = new ServerBootstrap();
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(
final ChannelGroup group) {
return new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {

// 使用用户标识作为channalid,例如用户名
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new HttpRequestHandler("/ws" ));
// 按照 WebSocket 规范的要求,处理 WebSocket 升级握手、
//PingWebSocketFrame PongWebSocketFrame
//CloseWebSocketFrame
pipeline.addLast(new WebSocketServerProtocolHandler("/ws" ));
pipeline.addLast(new TextWebSocketFrameHandler(group));
}
};

}

public void destroy() {
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 (args0);
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();
}
}

HttpRequestHandler握手升级处理器

*/
public class HttpRequestHandler extends 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())) {
//request.retain() 对内部的 ByteBuf content 的计数 +1 ,因为在父类方法中 finall 会及时的调用 ReferenceCountUtil.release(msg);
// 以免并真正释放了
ctx.fireChannelRead(request.retain());
} else {
if (HttpHeaders.is100ContinueExpected (request)) {
// 如果客户端发送了 HTTP 1.1 HTTP 头信息 Expect: 100-continue ,那么 HttpRequestHandler 将会发送一个 100 Continue 响应
send100Continue (ctx);
}
RandomAccessFile file = new RandomAccessFile(INDEX , "r" );
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) {
response.headers().set(
HttpHeaders.Names.CONTENT_LENGTH , file.length());
response.headers().set( HttpHeaders.Names.CONNECTION ,
HttpHeaders.Values.KEEP_ALIVE );
}
ctx.write(response);
if (ctx.pipeline().get(SslHandler.class ) == null ) {
ctx.write(new DefaultFileRegion(
file.getChannel(), 0, file.length()));
} else {
ctx.write(new ChunkedNioFile(file.getChannel()));
}
// 冲刷所有之前写入的消息。
ChannelFuture future = ctx.writeAndFlush(
LastHttpContent.EMPTY_LAST_CONTENT );
if (!keepAlive) {
// 如果没有请
// keep-alive ,那么 HttpRequestHandler 将会添加一个 ChannelFutureListener
// 到最后一次写出动作的 ChannelFuture ,并关闭该连接。
future.addListener(ChannelFutureListener.CLOSE );
}
}
}

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();
}
}

TextWebSocketFrameHandler WebSocket 消息处理

*/
public class TextWebSocketFrameHandler
extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final ChannelGroup group ;

public TextWebSocketFrameHandler(ChannelGroup group) {
this .group = group;
}

// 如果该事件表示握手成功,则从该 Channelipeline 中移除 HttpRequestHandler
// 因为将不会接收到任何 HTTP 消息了
@Override
public void userEventTriggered(ChannelHandlerContext ctx,
Object evt) throws Exception {
if (evt == WebSocketServerProtocolHandler
.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE ) {
ctx.pipeline().remove(HttpRequestHandler.class );
group .writeAndFlush(new TextWebSocketFrame(
"Client " + ctx.channel() + " joined" ));
group .add(ctx.channel());
} else {
super .userEventTriggered(ctx, evt);
}
}

@Override
public void channelRead0(ChannelHandlerContext ctx,
TextWebSocketFrame msg) throws Exception {
group .writeAndFlush(msg.retain());
}
}

ChannelGroup客户端连接处理组

public class DefaultChannelGroup extends AbstractSet<Channel> implements ChannelGroup {

private static final AtomicInteger nextId = new AtomicInteger();
private final String name ;
private final EventExecutor executor ;
private final ConcurrentMap<ChannelId, Channel> serverChannels = PlatformDependent.newConcurrentHashMap ();
private final ConcurrentMap<ChannelId, Channel> nonServerChannels = PlatformDependent.newConcurrentHashMap ();
private final ChannelFutureListener remover = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
remove(future.channel());
}
};
private final boolean stayClosed ;
private volatile boolean closed ;

public DefaultChannelGroupx(String name, EventExecutor executor, boolean stayClosed) {
if (name == null ) {
throw new NullPointerException("name" );
}
this .name = name;
this .executor = executor;
this .stayClosed = stayClosed;
}

@Override
public String name() {
return name ;
}

@Override
public Channel find(ChannelId id) {
Channel c = nonServerChannels .get(id);
if (c != null ) {
return c;
} else {
return serverChannels .get(id);
}
}

@Override
public boolean add(Channel channel) {
ConcurrentMap<ChannelId, Channel> map =
channel instanceof ServerChannel? serverChannels : nonServerChannels ;

boolean added = map.putIfAbsent(channel.id(), channel) == null ;
if (added) {
channel.closeFuture().addListener(remover );
}

if (stayClosed && closed ) {
channel.close();
}

return added;
}

@Override
public ChannelGroupFuture writeAndFlush(Object message, ChannelMatcher matcher, boolean voidPromise) {
if (message == null ) {
throw new NullPointerException("message" );
}

final ChannelGroupFuture future;
if (voidPromise) {
for (Channel c: nonServerChannels .values()) {
if (matcher.matches(c)) {
c.writeAndFlush(safeDuplicate(message), c.voidPromise());
}
}
future = voidFuture;
} else {
Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size());
for (Channel c: nonServerChannels .values()) {
if (matcher.matches(c)) {
futures.put(c, c.writeAndFlush(safeDuplicate(message)));
}
}
future = new DefaultChannelGroupFuture(this , futures, executor );
}
ReferenceCountUtil.release (message);
return future;
}

}

相关推荐
明月_清风12 分钟前
算法时间复杂度:给小白的一堂"算快慢"课
后端·算法
zhuodedao15 分钟前
Spring AI + MCP 文件工具未调用问题复盘:为什么初始化成功却没有写入文件?
java·debug·agent·springai·mcp
tsqtsqtsq030921 分钟前
鸿蒙系统应用市场更新功能详解与开发适配指南
服务器·harmonyos
Java小白笔记26 分钟前
Java中大数据实时归集与指标汇总方案
java·大数据·开发语言
Nturmoils28 分钟前
内网穿透原来这么简单:Natapp 从注册到公网访问完整教程
后端
随遇而安zx28 分钟前
【地基篇】---Java 8 JVM 知识大纲
java·开发语言·jvm
雾隐隐o29 分钟前
Docker 镜像归档与容器管理
java·docker·eureka
Meta3934 分钟前
Java八股文之Spring Boot 中解决 MySQL 和 Elasticsearch (ES) 的数据一致性问题
java·spring boot·mysql