netty学习(3)

在netty的各种类型的channel中,都会包含一个pipeline,字面意思是管道,我们可以理解为一条流水线工艺,流水线工艺有起点,有结束,中间还有各种各样的流水线关卡,一件物品,在流水线起点开始处理,经过各个流水线关卡的加工,最终到流水线结束
对应到netty里面,流水线的开始就是HeadContxt,流水线的结束就是TailConext,HeadContxt中调用Unsafe做具体的操作,TailConext中用于向用户抛出pipeline中未处理异常以及对未处理消息的警告,

我们已经知道在服务端处理新连接的pipeline中,已经自动添加了一个pipeline处理器 ServerBootstrapAcceptor, 并已经将用户代码中设置的一系列的参数传入了构造函数,接下来,我们就来看下ServerBootstrapAcceptor

hdfs我们顺带提取出netty的几大基本组件,先总结如下

Channel
ChannelConfig
ChannelId
Unsafe
Pipeline
ChannelHander

1.boos reactor线程轮询到有新的连接进入
2.通过封装jdk底层的channel创建 NioSocketChannel以及一系列的netty核心组件
3.将该条连接通过chooser,选择一条worker reactor线程绑定上去
4.注册读事件,开始新连接的读写

pipeline 初始化
pipeline 添加节点
pipeline 删除节点

pipeline中的每个节点是一个ChannelHandlerContext对象,每个context节点保存了它包裹的执行器 ChannelHandler 执行操作所需要的上下文,其实就是pipeline,因为pipeline包含了channel的引用,可以拿到所有的context信息
默认情况下,一条pipeline会有两个节点,head和tail,后面的文章我们具体分析这两个特殊的节点,今天我们重点放在pipeline

首先,用一个spliter将来源TCP数据包拆包,然后将拆出来的包进行decoder,传入业务处理器BusinessHandler,业务处理完encoder,输出

整个pipeline结构如下

head spliter decoder pussinessHander encoder tail

ChannelInboundHandler,处理inBound事件,最典型的就是读取数据流,加工处理;还有一种类型的Handler是 ChannelOutboundHandler, 处理outBound事件,比如当调用writeAndFlush()类方法时,就会经过该种类型的handler
不管是哪种类型的handler,其外层对象 ChannelHandlerContext 之间都是通过双向链表连接,而区分一个 ChannelHandlerContext到底是in还是out,在添加节点的时候我们就可以看到netty是怎么处理的

1.检查是否有重复handler
netty使用一个成员变量added标识一个channel是否已经添加,
如果当前要添加的Handler是非共享的,并且已经添加过,那就抛出异常,否则,标识该handler已经添加
由此可见,一个Handler如果是sharable的,就可以无限次被添加到pipeline中,我们客户端代码如果要让一个Handler被共用,只需要加一个@Sharable标注即可
而如果Handler是sharable的,一般就通过spring的注入的方式使用,不需要每次都new 一个

isSharable() 方法正是通过该Handler对应的类是否标注@Sharable来实现的

2.创建节点
netty使用一个 FastThreadLocal(后面的文章会细说)变量来缓存Handler的类和默认名称的映射关系,在生成name的时候,首先查看缓存中有没有生成过默认name(简单类名#0),如果没有生成,就调用generateName0()生成默认name,然后加入缓存

context0()方法链表遍历每一个 ChannelHandlerContext,只要发现某个context的名字与待添加的name相同,就返回该context,最后抛出异常,可以看到,这个其实是一个线性搜索的过程
如果context0(name) != null 成立,说明现有的context里面已经有了一个默认name,那么就从 简单类名#1 往上一直找,直到找到一个唯一的name,比如简单类名#3
如果用户代码在添加Handler的时候指定了一个name,那么要做到事仅仅为检查一下是否有重复

处理完name之后,就进入到创建context的过程,由前面的调用链得知,group为null,因此childExecutor(group)也返回null

3.添加节点
说白了,其实就是一个双向链表的插入操作

4.回调用户方法
pipeline删除节点
netty 有个最大的特性之一就是Handler可插拔,做到动态编织pipeline,比如在首次建立连接的时候,需要通过进行权限认证,在认证通过之后,就可以将此context移除,下次pipeline在传播事件的时候就就不会调用到权限认证处理器

remove操作相比add简单不少,分为三个步骤:

1.找到待删除的节点
2.调整双向链表指针删除
3.回调用户函数

1.找到待删除的节点

2.调整双向链表指针删除
1.以新连接创建为例,新连接创建的过程中创建channel,而在创建channel的过程中创建了该channel对应的pipeline,创建完pipeline之后,自动给该pipeline添加了两个节点,即ChannelHandlerContext,ChannelHandlerContext中有用pipeline和channel所有的上下文信息。
2.pipeline是双向个链表结构,添加和删除节点均只需要调整链表结构
3.pipeline中的每个节点包着具体的处理器ChannelHandler,节点根据ChannelHandler的类型是ChannelInboundHandler还是ChannelOutboundHandler来判断该节点属于in还是out或者两者都是

pipeline是伴随Channel的存在而存在的,交互信息通过它进行传递,我们可以addLast(或者addFirst)多个handler,第一个参数是名字,无具体要求,如果填写null,系统会自动命名。

handler()和childHandler()的主要区别是,handler()是发生在初始化的时候,childHandler()是发生在客户端连接之后。

ChannelInitializer是一个抽象类,不能直接使用
ChannelInitializer的主要目的是为程序员提供了一个简单的工具,用于在某个Channel注册到EventLoop后,对这个Channel执行一些初始化操作。ChannelInitializer虽然会在一开始会被注册到Channel相关的pipeline里,但是在初始化完成之后,ChannelInitializer会将自己从pipeline中移除,不会影响后续的操作。

使用场景:

a. 在ServerBootstrap初始化时,为监听端口accept事件的Channel添加ServerBootstrapAcceptor

b. 在有新链接进入时,为监听客户端read/write事件的Channel添加用户自定义的ChannelHandler

ChannelPipeline和ChannelHandler机制类似于Servlet和Filter过滤器,这类拦截器实际上是职责链模式的一种变形,主要是为了方便事件的拦截和用户业务逻辑的定制。Netty的Channel过滤器实现原理与Servlet Filter机制一致,它将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipeline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便地通过新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。

ChannelPipeline是ChannelHandler的容器,它负责ChannelHandler的管理和事件拦截与调度。

ChannelPipeline的事件处理
一个消息被ChannelPipeline的ChannelHandler链拦截和处理的全过程:

(1)底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead事件,由I/O线程NioEventLoop调用ChannelPipeline的fireChannelRead(Object msg)方法,将消息(ByteBuf)传输到ChannelPipeline中;

(2)消息依次被HeadHandler、ChannelHandler1、ChannelHandler2……TailHandler拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递;

(3)调用ChannelHandlerContext的write方法发送消息,消息从TailHandler开始,途经ChannelHandlerN……ChannelHandler1、HeadHandler,最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回。

Netty中的事件分为inbound事件和outbound事件。inbound事件通常由I/O线程触发,例如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等。

触发inbound事件的方法如下。

(1)ChannelHandlerContext.fireChannelRegistered():Channel注册事件;

(2)ChannelHandlerContext.fireChannelActive():TCP链路建立成功,Channel激活事件;

(3)ChannelHandlerContext.fireChannelRead(Object):读事件;

(4)ChannelHandlerContext.fireChannelReadComplete():读操作完成通知事件;

(5)ChannelHandlerContext.fireExceptionCaught(Throwable):异常通知事件;

(6)ChannelHandlerContext.fireUserEventTriggered(Object):用户自定义事件;

(7)ChannelHandlerContext.fireChannelWritabilityChanged():Channel的可写状态变化通知事件;

(8)ChannelHandlerContext.fireChannelInactive():TCP连接关闭,链路不可用通知事件。

Outbound事件通常是由用户主动发起的网络I/O操作,例如用户发起的连接操作、绑定操作、消息发送等操作,它对应图17-1的右半部分。

触发outbound事件的方法如下:

(1)ChannelHandlerContext.bind(SocketAddress, ChannelPromise):绑定本地地址事件;

(2)ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise):连接服务端事件;

(3)ChannelHandlerContext.write(Object, ChannelPromise):发送事件;

(4)ChannelHandlerContext.flush():刷新事件;

(5)ChannelHandlerContext.read():读事件;

(6)ChannelHandlerContext.disconnect(ChannelPromise):断开连接事件;

(7)ChannelHandlerContext.close(ChannelPromise):关闭当前Channel事件。

自定义拦截器
ChannelPipeline通过ChannelHandler接口来实现事件的拦截和处理,由于ChannelHandler中的事件种类繁多,不同的ChannelHandler可能只需要关心其中的某一个或者几个事件,所以,通常ChannelHandler只需要继承ChannelHandlerAdapter类覆盖自己关心的方法即可。

下面的例子展示了拦截Channel Active事件,打印TCP链路建立成功日志,代码如下:
public class MyInboundHandler extends ChannelHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println(“TCP connected!”);
ctx.fireChannelActive();
}
}

构建pipeline
事实上,用户不需要自己创建pipeline,因为使用ServerBootstrap或者Bootstrap启动服务端或者客户端时,Netty会为每个Channel连接创建一个独立的pipeline。对于使用者而言,只需要将自定义的拦截器加入到pipeline中即可。

pipeline = ch.pipeline();
pipeline.addLast(“decoder”, new MyProtocolDecoder());
pipeline.addLast(“encoder”, new MyProtocolEncoder());
对于类似编解码这样的ChannelHandler,它存在先后顺序,例如MessageToMessageDecoder,在它之前往往需要有ByteToMessageDecoder将ByteBuf解码为对象,然后对对象做二次解码得到最终的POJO对象。

ChannelPipeline的主要特性
ChannelPipeline支持运行态动态的添加或者删除ChannelHandler,在某些场景下这个特性非常实用。例如当业务高峰期需要对系统做拥塞保护时,就可以根据当前的系统时间进行判断,如果处于业务高峰期,则动态地将系统拥塞保护ChannelHandler添加到当前的ChannelPipeline中,当高峰期过去之后,就可以动态删除拥塞保护ChannelHandler了。

ChannelPipeline是线程安全的,这意味着N个业务线程可以并发地操作ChannelPipeline而不存在多线程并发问题。但是,ChannelHandler却不是线程安全的,这意味着尽管ChannelPipeline是线程安全的,但是用户仍然需要自己保证ChannelHandler的线程安全。

ChannelPipeline的代码相对比较简单,它实际上是一个ChannelHandler的容器,内部维护了一个ChannelHandler的链表和迭代器,可以方便地实现ChannelHandler查找、添加、替换和删除。

ChannelPipeline对ChannelHandler的管理
ChannelPipeline是ChannelHandler的管理容器,负责ChannelHandler的查询、添加、替换和删除,它与Map等容器的实现非常类似。

由于ChannelPipeline支持运行期动态修改,在调用类似addBefore(ChannelHandlerInvoker invoker, String baseName, final String name, ChannelHandler handler)方法时,存在两种潜在的多线程并发访问场景。

I/O线程和用户业务线程的并发访问;
用户多个线程之间的并发访问。
为了保证ChannelPipeline的线程安全性,需要通过线程安全容器或者锁来保证并发访问的安全,此处Netty直接使用了synchronized关键字,保证同步块内的所有操作的原子性。首先根据baseName获取它对应的DefaultChannelHandlerContext,ChannelPipeline维护了ChannelHandler名和ChannelHandlerContext实例的映射关系。 新增的ChannelHandler名进行重复性校验,如果已经有同名的ChannelHandler存在,则不允许覆盖,抛出IllegalArgumentException("Duplicate handler name: " + name)异常。校验通过之后,使用新增的ChannelHandler等参数构造一个新的DefaultChannelHandlerContext实例。将新创建的DefaultChannelHandlerContext添加到当前的pipeline中(首先需要对添加的ChannelHandlerContext做重复性校验,如果ChannelHandler不是可以在多个ChannelPipeline中共享的,且已经被添加到ChannelPipeline中,则抛出ChannelPipelineException异常。),加入成功之后,缓存ChannelHandlerContext,发送新增ChannelHandlerContext通知消息。

ChannelPipeline的inbound事件
当发生某个I/O事件的时候,例如链路建立、链路关闭、读取操作完成等,都会产生一个事件,事件在pipeline中得到传播和处理,它是事件处理的总入口。由于网络I/O相关的事件有限,因此Netty对这些事件进行了统一抽象,Netty自身和用户的ChannelHandler会对感兴趣的事件进行拦截和处理。

pipeline中以fireXXX命名的方法都是从IO线程流向用户业务Handler的inbound事件,它们的实现因功能而异,但是处理步骤类似,总结如下。

(1)调用HeadHandler对应的fireXXX方法;

(2)执行事件相关的逻辑操作。

以fireChannelActive方法为例,调用head.fireChannelActive()之后,判断当前的Channel配置是否自动读取,如果为真则调用Channel的read方法

DefaultChannelPipeline

@Override
public ChannelPipeline fireChannelActive() {
    head.fireChannelActive();

    if (channel.config().isAutoRead()) {
        channel.read();
    }

    return this;
}


ChannelPipeline的outbound事件

由用户线程或者代码发起的I/O操作被称为outbound事件,事实上inbound和outbound是Netty自身根据事件在pipeline中的流向抽象出来的术语,在其他NIO框架中并没有这个概念。

Pipeline本身并不直接进行I/O操作,最终都是由Unsafe和Channel来实现真正的I/O操作的。Pipeline负责将I/O事件通过HeadHandler进行调度和传播,最终调用Unsafe的I/O方法进行I/O操作。最终由TailHandler调用Unsafe的connect方法发起真正的连接,pipeline仅仅负责事件的调度。

InetAddress表示IP地址 表示互联网协议(IP)地址
SocketAddress表示不依赖于具体协议的套接字地址抽象,InetSocketAddress表示IP地址的套接字地址,包含IP地址和端口号。 不带任何协议附件的 Socket Address

executorService.scheduleAtFixedRate
ExecutorService是Java提供的线程池

ScheduleExecutorService接口中有四个重要的方法,其中scheduleAtFixedRate和scheduleWithFixedDelay在实现定时程序时比较方便。
1.按指定频率周期执行某个任务。

初始化延迟0ms开始执行,每隔100ms重新执行一次任务。
2.按指定频率间隔执行某个任务。
初始化时延时0ms开始执行,本次执行结束后延迟100ms开始下次执行。
3.周期定时执行某个任务。

TimeUnit是java.util.concurrent包下面的一个类,TimeUnit提供了可读性更好的线程暂停操作,通常用来替换Thread.sleep(),在很长一段时间里Thread的sleep()方法作为暂停线程的标准方式,几乎所有Java程序员都熟悉它,事实上sleep方法本身也很常用而且出现在很多面试中。如果你已经使用过Thread.sleep(),当然我确信你这样做过,那么你一定熟知它是一个静态方法,暂停线程时它不会释放锁,该方法会抛出InterrupttedException异常(如果有线程中断了当前线程)。但是我们很多人并没有注意的一个潜在的问题就是它的可读性。Thread.sleep()是一个重载方法,可以接收长整型毫秒和长整型的纳秒参数,这样对程序员造成的一个问题就是很难知道到底当前线程是睡眠了多少秒、分、小时或者天。
Thread.sleep(4601000);
,不容易猜出当前线程将等待4分钟。TimeUnit类解决了这个问题,通过指定DAYS、HOURS、MINUTES,SECONDS、MILLISECONDS和NANOSECONDS。
TimeUnit.MINUTES.sleep(4); // sleeping for 4 minutes

钩子程序
Runtime.getRuntime().addShutdownHook(Thread thread)

这里我们需要将一个线程对象传入,作为钩子程序的实现代码。本质上就是在jvm关闭时,执行一个线程。

SimpleChannelInboundHandler
首先看到的是 SimpleChannelInboundHandler 继承自 ChannelInboundHandlerAdapter。

netty发送和接收数据handler处理器 主要是继承 SimpleChannelInboundHandler 和 ChannelInboundHandlerAdapter

其实用这两个抽象类是有讲究的,在客户端的业务Handler继承的是SimpleChannelInboundHandler,而在服务器端继承的是ChannelInboundHandlerAdapter。

最主要的区别就是SimpleChannelInboundHandler在接收到数据后会自动release掉数据占用的Bytebuffer资源(自动调用Bytebuffer.release())。而为何服务器端不能用呢,因为我们想让服务器把客户端请求的数据发送回去,而服务器端有可能在channelRead方法返回前还没有写完数据,因此不能让它自动release。

对于WebSocket而言,数据使用帧进行传输,完整的数据包含多帧。
WebSocket协议允许客户端和服务器随时传输消息,要求他们异步处理接收的消息,而几乎所有的浏览器都支持WebSocket协议,Netty支持WebSocket协议的所有实现,可以在应用中直接使用。
WebSocket定义了六种帧, 对于聊天应用而言,其包含如下帧:CloseWebSocketFrame、PingWebSocketFrame、PongWebSocketFrame、TextWebSocketFrame。

handler处理器 内置 方法
channelActive
通道激活时触发,当客户端connect成功后,服务端就会接收到这个事件,从而可以把客户端的Channel记录下来,供后面复用
channelRead
这个必须用啊,当收到对方发来的数据后,就会触发,参数msg就是发来的信息,可以是基础类型,也可以是序列化的复杂对象。
channelReadComplete
channelRead执行后触发
exceptionCaught
出错是会触发,做一些错误处理

ChannelHandlerContext
每个ChannelHandler通过add方法加入到ChannelPipeline中去的时候,会创建一个对应的ChannelHandlerContext,并且绑定,ChannelPipeline实际维护的是ChannelHandlerContext 的关系

ChannelPipeline
在Channel创建的时候,会同时创建ChannelPipeline
在ChannelPipeline中也会持有Channel的引用
ChannelPipeline会维护一个ChannelHandlerContext的双向链表

JSON.parseObject(String str)与JSONObject.parseObject(String str)的区别
JSONObject json=JSON.parseObject(params); parms:字符串 把字符串转为JSONObject对象

channelUnregistered

channel的方法
localAddress()返回channel绑定的本地地址,
remoteAddress()返回与Channel连接的远程地址
config()返回一个ChannelConfig对象,通过这个对象可以配置Channel相关的参数
metadata()返回一个ChannelMetadata对象,通过这个对象可以查询具体的Channel实现是否支持某种操作。目前ChannelMetadata只有一个方法,hasDisconnect(),用来查询Channel实现是否支持disconnect()操作。
parent()方法返回一个Channel的父Channel。按照Javadoc的说法,视Channel如何被创建而定,Channel可能会有一个父Channel。比如说如果一个SocketChannel是被ServerSocketChannel创建(accept)的,那么调用SocketChannel的parent()方法就会返回这个ServerSocketChannel
eventLoop()方法返回Channel注册到了哪个EventLoop里
pipeline()方法返回Channel的Pipeline
alloc()方法返回与Channel关联的ByteBufAllocator实例
unsafe()方法返回一个Unsafe实例。这个Unsafe实例只供Channel实现内部使用

HttpServlet 类需要两个参数HttpRequest和HttpResponse。
HttpRequest对象的目的是代替浏览器把Http请求发送给web应用,因此,任何浏览器能发送的,HttpRequest都可以接受到。

FullHttpRequest的协议头和Form数据是在一起的,不用分开读,之前分开接收的是DefaultHttpRequest与HttpContent,FullHttpRequest把两者合在一起了,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值