文章目录
所有的Netty服务器都需要一下两部分:
- 至少一个ChannelHandler-该组件实现了服务器对从客户端接收的数据的处理,即业务逻辑
- 引导–这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求端口上。
Channel和业务逻辑
ChannelHandler是一个父接口,良好的和事件挂钩的实现类是ChannelInboundHandlerAdapter,它的实现负责接收并响应事件通知,所有的数据处理逻辑都包含在这些核心抽象的实现中。
其中该接口令人感兴趣的方法是:
- channelRead() --对每个传入的消息都要调用,可以读取用户的数据,还有一个channelRead0方法,通常都是用来处理开发者的用户逻辑。再开一个线程池去处理。
- channelReadComplete() --对通知ChannelInboundHandler最后一次对channelRead的调用时当前批量读取中的最后一条信息
- exceptionCaught() --在读取操作期间。有异常抛出要调用
其中在ChannelHandlerContext和Channel之间,在ChannelPipeLine执行的过程中,如果有操作一些Channel和ChannelHandlerContext重名的方法,可以使用channel,也可以使用ChannelHandlerConntext。
- 前者是从与之绑定的ChannelPipeLine的出站事件的第一个handler开始流动
- 后者是从于之绑定的ChannelPipeline的下一个Handler进行流动,具备更短的消息流事件,有利于提高程序效率
ByteBuf的优点
- 他可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长(类似于JDK的StringBuilder)
- 在读和写这两种模式之间切换不需要调用ByteBuffer的filp方法
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
Heap Buffer(堆缓冲区)
- 优点:因为底层是在分配在jvm上的堆的数组,可以直接访问进行操作。
- 缺点:每次读写数据时,都需要先将数据复制到直接缓冲区再进行网络传输
Direct Buffer(直接缓冲区)
在堆之外直接分配内存空间,直接缓冲区并不会占用堆的容量空间,因为它是由操作系统在本地内存进行的数据分配
- 优点:在使用Socket进行数据传输(即IO)时,性能非常好,因为数据直接位于操作系统的本地内存中,实现了零拷贝,不用拷贝数据到jvm的缓冲区
- 缺点:因为Direct Buffer是在直接在操作系统中的,所以内存空间的分配释放要比堆空间更加复杂,而且速度要慢一些。
提示:
- 对于后端的业务消息的编解码来说,推荐使用HeapByteBuf
- 对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf。
Channel的生命周期
- ChannelUnregistered:Channel已经被创建,但还没被注册到EventLoop
- ChannelRegistered:Channel已经被注册到EventLoop
- ChannelActive:Channel处于活动状态。它现在可以接收和发送数据了
- ChannelInactive:Channel没有连接到远程节点
ChannelHandler的生命周期
- handlerAdded:当把ChannelHandler添加到ChannelPipeline中时被调用
- handlerRemoved:当从ChannelPipeline中移除ChannelHandler时被调用
- exceptionCaught:当处理过程中在ChannelPipeline中有错误时被调用
Handler重要概念:
- Netty的处理器可以分为两类:入站和出站处理器
- 数据处理时常用的各种编解码器本质上都是处理器
- 编解码器:无论我们向网络中写入的数据是什么类型(int、char、String、二进制等),数据在网络中传递时,其都是以字节流的形式呈现的;将数据由原本的形式转换为字节流的操作称为编码(encode),将数据由字节转换为它原本的格式或者而其他的格式称为解码(decode),编解码统一称为codec
- 编码:本质上是一种出站处理器,因此,编码一定是一种ChannelOutboundHandler
- 解码:本质上是一中入站处理器,因此,解码一定是一种ChannelInboundHandler
- 在Netty中,编码器通常以XXXEncoder命名;解码器通常以XXXDecoder命名;只是通常,不是一定的。
两个重要的ChannelHandler子接口:
- ChannelInboundHandler:处理入站数据以及各种状态变化
- ChannelOutboundHandler:处理出站数据并且允许拦截所有的操作
ChannelPipeline
- ChannelPipeline保存了与Channel相关联的ChannelHandler
- ChannelPipeline可以根据需要,通过添加或者删除ChannelHandler来动态地修改
- ChannelPipeline有着丰富的API用以被调用,以响应入站和出站事件
ChannelHandlerContext接口
ChannelHandlerContext代表了ChannelHandler和ChannelPipeline之间的关联,每当有ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。ChannelHandlerContext的主要功能是管理它所关联的ChannelHandler和在同一个ChannelPipeline中的其他ChannelHandler之间的交互。
ChannelHandler和ChannelHandlerContext的高级用法
-
可以通过调用ChannelHandlerContext上的pipeline()方法来获得到被封闭的ChannelPipe的引用。这使得运行时得以操作ChannelPipeline的ChannelHandler,我们可以运用这一点来实现一些复杂的设计。例如:可以通过将ChannnelHandler添加到ChannelPipeline中来实现动态的协议切换
-
缓存到ChannelHandlerContext的引用以供稍后使用,这可能会发生在任何的ChannelHandler方法之外,甚至来自于不同的线程。
//缓存到ChannelHandlerContext的引用 public class WriteHandler extends ChannelHandlerAdapter { private ChannelHandlerContext ctx; @Override public void handlerAdded(ChannelHandlerContext ctx) { this.ctx = ctx; } public void send(String msg) { ctx.writeAndFlush(msg); } }
-
因为一个ChannelHandler可以从属于多个ChannelPipeline,所以它可以绑定到多个ChannelHandlerContext实例。对于这种用法(指在多个ChannelPipeline中共享同一个ChannelHandler),对应的ChannelHandler必须要使用@Sharable注解标注;否则,会出现异常
@ChannelHandler.Sharable public class SharableHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.out.println("Channel read message" + msg); ctx.fireChannelRead(msg); } }
异常处理
-
处理入站异常 需要重写ChannelInboundHandler的下列方法
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception //ChannelHandler.exceptionCaught的默认实现是简单地将当前异常转发给ChannelPipeline中的下一个ChannelHandler //如果异常到达了ChannelPipe的尾端,他将会被记录为未被处理 //要想定义自定义的处理逻辑,需要重写exceptionCaught方法。然后需要决定是否将该异常传播出去
-
处理出站异常:①每个出站操作都会返回一个ChannelFuture。注册到ChannelFuture的ChannelFutureListener将在操作完成时被通知该操作是成功了还是出错了。②几乎所有的ChannelOutboundHandler上的方法都会传入一个ChannelPromise的实例。作为ChannelFuture的子类,ChannelPromise也可以被分配用于异步通知的监听器。但是,ChannelPromise还具有提供立即通知的可写方法:
ChannelPromise setSuccess() ChannelPromise serFailure()
EventLoop接口
运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编程上的构造统称为事件循环。
EventLoopGroup负责为每个新创建的Channel分配一个EventLoop。在当前实现中,使用顺序循环的方式进行分配已获得一个均衡的分布,并且相同的EventLoop会被分配给多个channel,一旦一个Channel分配给一个EventLoop,它将在它的整个生命周期中都使用这个EventLoop。
Reactor模式
角色构成:
- Handle(句柄或是描述符):本质上表示是一种资源,是由操作系统提供的;该资源用于表示一个个的事件,比如说文件描述符,或是针对网络编程中的Socket描述符,事件可以来自外部,也可以来自内部;外部事件比如说来自客户端的连接请求,connect,disconnect,客户端发过来数据等;内部事件比如说操作系统产生的定时器时间等,它本质上就是一个文件描述符。Handle是事件本身的发源地,即一种状态值,让操作系统标注
- Synchronous Event Demultiplexer(同步事件分离器):它本身是一个系统调用,用于等待事件的发生(事件可能是多个,也可能是一个)。调用方在调用它的时候会被阻塞,一直阻塞到同步事件分离器上有事件产生为止。对于Linux来说,同步事件分离器指的就是常营的IO多路复用机制,比如说select、poll、epoll等。在Java的nio中,对应的同步事件分离器是selector,对应的阻塞方法是select方法
- Event Handler(事件处理器):本身由多个回调方法构成,这些回调方法构成了与应用相关的对于某个事件的反馈机制。Netty相比于Java NIO来说,事件处理器这个机制是完善了,它为我们开发者提供了大量的回调方法,供我们在特定事件产生时实现响应的回调方法进行业务逻辑的处理
- Concrete Event Handler (具体事件处理器):是事件处理器的实现。它本身实现了事件处理器所提供的各个回调方法,从而实现了特定于业务的逻辑。它本质上我们所编写的一个个Handler实现。
- Initiation Dispatcher (初始分发器):实际上就是Reactor角色,它本身定义了一些规范,这些规范用于控制事件的调度方式,同时又提供了引用进行事件处理器的注册、删除等设施,它本身是整个事件处理器的核心所在,Initiation Dispatcher会通过同步事件分离器来等待事件的发生,一旦事件发生,Initiation Dispatcher首先会分离出每一个事件,然后调用事件处理器,最后调用相关的回调方法来处理这些事件
Reactor模式的流程
- 当应用向Initiation Dispatcher注册具体的事件处理器时。应用会标识出该事件处理器希望Initiation Dispatcher在某个事件发生时向其通知的该事件,该事件与Handle关联
- Initiation Dispatcher会要求每个Event Handler向其传递内部的Handle。该handle向操作系统标识了事件处理器。
- 当所有事件处理器注册完毕后,应用会调用handle_events方法来启动Initiation Dispatcher 的事件循环。这时,Initiation Dispatcher会将每个注册的事件管理器的Handle合并起来,并使用同步事件分离器等待这些事件的发生。比如说,TCP协议层会使用select同步事件分离器操作来等待客户端发送的数据到达连接的socket handle上。这类似于selector的select方法在等待事件发生。
- 当与某个事件对应的Handle变成ready状态时(比如说,TCP socket变为等待读状态时),同步事件分离器就会通知Initiation Dispatcher。
- Initiation Dispatcher 会触发事件处理器的回调方法。从而响应这个处于ready状态的Handle。当事件发生时,Initation Dispatcher会将事件激活的Handle作为key来寻找并分发恰当的事件处理器回调方法。
- Initiation Dispatcher会回调事件处理器的handle_events回调方法来执行特定于应用的功能(开发者自定义的回调方法),从而响应这个事件,所发生的事件类型可以作为该方法的参数并被该方法内部使用来执行额外的特定于服务的分离与分发
ChannelPipeline
channelPipeline是一个容器,他里面存放的是一个个ChannelHandlerContext,ChannelhandlerContext里面维护着一个与之对应的ChannelHandler。
所以,ChannelHandlerContext它是连接了ChannelHandler和ChannelPipeline的一个桥梁和纽带
Netty线程模型
- 一个EventLoopGroup当中含有多个EventLoop
- 一个EventLoop在它的整个生命周期当中都只会和与唯一一个Thread进行绑定(源码SingleThreadEventExecutor)
- 所有EventLoop所处理的各种I/O时间都将在它所关联的那个Thread上进行处理
- 一个Channel在它的整个生命周期中只会注册到一个EventLoop
- 一个EventLoop在运行过程当中,会分配给一个或者多个Channel
重要的结论: 在Netty中,Channel的实现一定是线程安全的;基于此,我们可以存储一个Channel的引用,并且在需要向远程端点发送数据时,通过这个引用来调用Channel相应的方法,即便当时有很多线程都在使用它也不会出现多线程问题;而且,消息一定会按照顺序发送出去。
业务线程池的两种实现方式
- 在ChnanelHandler的回调方法中国,使用自己定义的业务线程池,这样就可以实现异步调用。
- 借助于ChannelPipelined的addLast方法把线程池和自定义的Handler 传进去。
Netty中的观察者模式
JDK提供的Future只能通过手工方式检查执行结果,而这个操作是会阻塞的;
Netty对ChannelFuture进行了增强,通过ChannelFutureListener以回调的方式来获取执行结果,去掉了手工检查阻塞的操作;值得注意的是:ChannelFutureListener的operationComplete方法是由I/O线程执行的,因此要注意的是不要在这里执行耗时操作,需要通过另外的线程或线程池。
可以自定义ChannelFuture接口来自定义实现自己需要的ChannelFuture。
Netty中的模板方法
SimpleChannelInboundHandler继承自ChannelInboundHandlerAdapter,它主要重写了channelRead方法,在重现的channelRead方法中,其中有一个方法是channelRead0方法在里面作为钩子挂着,等待开发者去实现。且该方法对消息有引用的操作,看源码可以知道
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
客户端和服务端的实战问题
客户端一般是一个EventLoop来进行连接然后启动Bootstrap就可以了。而服务端可以有很多层,会有服务端连接另外一个服务端发送数据这种情况。这时可以使用一种方法,让服务端(此时可以当做是客户端)使用自己的一个eventLoop作为一个线程去连接另外一个服务端。
伪代码如下:
public void channelActive(ChannelHandlerContext ctx){
Bootstrap bootstrap = ````;
bootstrap.channel(NioSocketChannel.class).handler(new YourJopHandler()).group(ctx.channel()eventLoop);
bootstrap.connect();
}
Netty中的并发启发:
把原子性操作放在死循环里,直到更新成功才退出,保持原子性并且更新成功。这就是所谓的自旋锁。
AtomicIntegerFieldUpdater要点总结:
- 更新器更新的必须使int类型变量,不能是其包装类型。
- 更新器更新的必须使volatile类型变量,确保线程之间共享变量时的立即可见性(主要是volatile把修饰的关键字强制性地刷新到了主存,方便了其他线程去主存中取这个值到自己的工作内存中时可以确保这个值是最新的)
TCP粘包和拆包
用定义协议来进行转化可以得知,Netty接收信息时,一切都是ByteBuf,通过对ByteBuf的转换来定义handler来进行协议转换。如下
/**
* @author chenqiting
*/
public class PersonProtocol {
private int length;
private byte[] content;
public PersonProtocol(int length, byte[] content) {
this.length = length;
this.content = content;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public void setContent(byte[] content) {
this.content = content;
}
public byte[] getContent() {
return content;
}
}