netty权威指南学习

什么是nio?

nio 本质就是多路复用,内核告诉你那些句柄可读可写

jdk的selector就是对操作系统的io多路复用的一个封装,在linux中就是对epoll的封装;epoll的本质就是让内核直接处理句柄,不需要再复制到用户空间,这个要整理不能简单介绍

================>传送门(后续整理)


同步与异步
阻塞和非阻塞

非nio的io复用的使用:

客户端:

打开socketchannel 设置 socketchannel为非阻塞 异步链接server ,向reactor注册多路复用器,reactor线程创建selector启动线程,轮询就绪的key 异步读请求到bytebuffer (异步写请求到socketchannel)

服务端:

打开serversocketchannel 绑定端口,Reactor创建selector 将serversocketchannel注册到selector selector轮询key。判断是否是新接入,新接入设置为accept。不是新接入的可读状态 异步请求到byuteBuffer(异步写butebuffer到socketchannel)

如果是netty不用这么麻烦(首先启动bootstrapt 注入两个线程组,创建channel为nioServerSocketChannel,然后配置nioserverSocketChannel的tcp参数,设置为backlog,最后绑定iohandler 处理类(包含处理类,编解码,记录日志等),上面配置完成以后,绑定监听端口即可)

tcp拆包粘包:

如上几种情况,第二种粘了,第三种拆了

产生原因:1.进行了mss大小的tcp分段  2.超mtu进行ip分段 3.程序写的数据超过缓冲期大小

解决策略:

1.定长消息

2.在包尾部进行分割符

换行:LineBasedFrameDecoder
分隔符:DelimiterBasedFrameDecoder 
定长(固定长度):FixLengthFrameFecoder 

3.在表头表名消息的总长度

netty解决粘包的第一种方法:LineBasedFrameDecoder解决:

(通过addLast()添加编解码器)

(服务端)

一:bootstrap.group(boss,work).channel(NioserversocketChannel.class).OPTION( tcp参数).childHandler(子处理器

二:子处理器(pipeline).addLast(LineBasedFrameDecoder编解码).addLast(StringDecoder).addLast(真正的业务处理器handler)

三:真正的处理器子处理器handler需要继承ChannelHandlerAdaptr父类实现 channelActive、channelRead,exceptionCaught方法

(客户端)

与服务端类似:bootstrap.group().channel().option().handler(new ChannerlInitializer(){

实现initChannel(socketChannel)方法{

ch.pipeline.addLast(new LineBasedFrameDecoder()).addLast(StringDecoder()); addLast(客户端处理类)}

})

:如此解决的问题:不会出现粘包的问题

为什么可以解决该问题:

原理很简单LineBasedFrameDecoder的作用是依次遍历bytebuf的可读字节,读到/N /R/N 换行符的时候结束,从可读索引到结束位置的区间字节组成一行

而StringDecoder将接受的对象转成字符串然后调用后来的handler,这两个东西就是做按行切换的的文本解码器

另外的处理方法:DelimiterBasedFrameDecoder 和 FixLengthFrameFecoder 这两种的实现第一个是自动完成以分隔符做结束标志的消息的解码

第二个是自动完成对定长消息的解码

什么是netty的编解码?

当远程调用的时候,需要把本地的java对象编码为字节数组或者bytebuffer对象,当远程读取到bytebuffer对象或者字节数组的时候,需要将其解码位发送时的java对象

java序列化:仅仅是java编解码技术的一种

为什么rpc框架很少使用jdk的serializable序列化:第一个缺点:因为rpc通常用到的是调用非java对象,而jdk的serializable只是适用于java语言;第二个缺点:因为序列化的后码流很大,占用网络传输空间大;第三个缺点,序列化的性能比二进制编码的性能差

编解码技术二:google的protbuf

特点:结构化数据存储格式(xml json) ;高校的编解码技术;语言无关;支持java.c python

使用二进制编码有益于网络传输

序列化和反序列化


字节数

Thrift:

弱势:必须先确定好对象的数据结构。当数据结构发生变化了,需要重新定义idl(接口描述)文件

优点:适用于大型数据的交换及存储的工具。性能远优于json和xml

thrift主要由5部分组成:

1.语言系统及idl编译器,由用户给定额idl文件生成相应的接口代码

2.Tprotocol:Rpc的协议层,选择多种不同的序列化方式。如json和binary

3.TTransport:rpc的传输层,可以选择不同的传输层实现。如socket。NIO。MemoryBuffer

4.Tprocessor:作为协议层和用户提供的服务实现之间的纽带,负责调用服务实现的接口

5.TServer:聚合上面的2.3.4

Thrift支持三种典型的编解码:通用的二进制编解码。压缩的二进制编解码。优化的可选字段压缩编解码

Netty使用Protobuf作为编解码框架实现

优点:跨语言,编解码后消息更小,编解码性能高,支持定义可选和必选字段

特点:支持数据结构化一次到处使用,包括跨语言使用

netty实现protobuf:

前面配置一样,bootstrap.group.channel.option.childHandler(pipeline.addlast(protobuVarine32FrameDecoder(半包处理)).addLast(new protobufEncoder()).addLast(new SubReqServerHandler()))

因为使用了Decoder所以消息直接自动解码,收到的消息可以直接使用;由于使用了Encoder我们做响应的时候不需要手动进行编码

resp构造数据,writeAndFlush可以直接编码发送

客户端:类似,pipeline.addLast(new protobufVarint32FrameDecoder).addLast(new ProtobufDecoder()).addLast(ProtobufVarint32LengthFieldPrepender).addLast(new ProtobufEncoder).addLast(handler)

支持http场景

netty天生的异步事件驱动框架,因此,niotcp协议栈开发的http协议也是异步非阻塞的

WEBSOCKET协议开发

websocket的出现解决了http的弊端,1.http是个半双工的协议,数据只能单一方向传递

2.http消息荣誉繁杂,包含消息头,消息体,换行符等,相比于其余二进制冗杂

照着写一份代码试一下懒得解释了

netty的基本组件及源码学习

ByteBuf:

bytebuf继承类

首先我们数据传输用到缓冲区,使用的最多主要是bytebuffer,8中基本数据类型都用各自的bytebuffer,这个是jdknio类库提供使用的

缺点是
bytebuffer的长度是固定死的,出现了编码的pojo大于bytebuffer的时候就会出现索引越界的异常
2.bytebuffer只有一个标志位置的指针position 读写的时候需要手动调用filp()和rewind()等要正确调用否则会出现异常

光明出现了:Netty提供了新的缓冲区bytebuf数组缓冲区

提供一下几个基本功能:
1.支持7种剧本类型byte数组,bytebuffer
等的读写
2.缓冲区自身的copy和slice
3.设置网络字节序
4.构造缓冲区实例
5.操作位置指针等方法

BYTEBUFFER的实现:如果用的是jdk的bytebuffer,要正确使用filp,如果写入数据后不调用flip的话。读取的内容不正确

会读到从position - capality(当前的limit)的位置
调用flip之后才会读取position到limit的距离

所以得出结论 : flip()做的是将position与初始位置交换,limit移动到position的位置

bytebuf的实现
采用了readIndex和writeIndex两个指针

初始状态
前面是可读数据,后面是可写数据

clear以后数据都两个指针都回到0位置

Bytebuffer进行扩容,他不能自动扩容,put数据的时候会判断剩余空间是否足够,如果不够的话会进行重新开辟新的空间,将老数据移入,并清理老的bytebuffer对象;而netty提供的新的bytebuf会自动扩容不需要人为进行手动扩容

clear:将读写指针移会到初始位置

Mark+Reset:

bytebuffer用的是这两个,主要的用处是mark将当前指针记录,reset将指针恢复为备份在mark中的值

bytebuf因为有两个指针,所以对应的mark和reset有四个,分别是读mark读reset写mark写reset

已知:nio的socketchannel进行网络读写。操作的对象是bytebuffer。
但是:netty所用的是bytebuf他不能操作
所以:要在接口层进行两者互换,

详解bytebuffer与bytebuf转换:

1.nioBuffer()将bytebuf转换成bytebuffer,两者公用一个缓冲区内容,对bytebuffer的读写操作,不会修改原bytebuf的动态拓展操作

2.niobuffer(int index , int length)也是将bytebuf转换成buffer,是在起始位置为index,长度为length进行转换

支持随机读写(set / get)

但是问题在set和write的区别是set必须确认长度大小是否符合可写入的大小,不能支持动态扩容,而write是支持动态扩容的


Bytebuf分类
按照内存分配的角度,可以分为两类:堆内存和直接内存

堆内存:
分配及回收速度快,可以被jvm自动回收,但是缺点是io读写的时候要额外做一次内存复制,将堆内存的缓冲区复制到内核的channel中

直接内存
非堆内存,在堆外进行内存分配,他的分配和回收会比堆内存比较慢,但是io操作的时候可以直接从socketchannel中读取

最佳实践:
io通信线程读写缓冲区使用直接内存。后端业务编解码模块使用堆内存

按照内存回收的角度区分bytebuf可以以分为两类,基于对象池的bytebuf 普通的bytebuf

区别在于对象池的bytebuf可以复用bytebuf对象,可以在netty的高负载大并发的冲击下使得gc更加的稳定

Channel是个啥

是jdk的NIO的组成部分,netty重新实现

主要的api还是进行网络io

channel在netty中是网络操作抽象类、他聚合了一组功能,包含但不限于网络的读写客户端发起链接主动关闭链接,获取通信双方饿网络地址等;同时也可以获取eventloop,获取缓冲分配器,和pipeline等

netty用自己的channel主要是要能够跟netty的整体架构融合, 例如io模型,基于channelpipeline的定制模型,以及基于元数据描述配置化的tcp参数等

channel的一些常用api

1.channel read:从channel中读取数据到第一个inbound缓冲区,读取完以后会调用channelReadComplete这样决定是否需要继续读取数据
2.channelFuture write:将数据通过channelpipeline写入到channel中,该操作只是将消息发送到环形数组中,只有当调用到flush才会真正的发送数据
3.channelfuture writeandflush 将write和flush组合
4.channel flush将之前写入到发送环形数组的消息全部写入channel 并发送给通信方
5.channelfure connect 指定服务器地址发起链接请求
6.channelfuture bind 绑定指定的本地socket地址
7.is open:判断是否channel已经打开;isRegistered:判断channel是否注册到evenloop上

8.eventLoop():channel需要注册到evenloop的多路复用器;通过该方法,可以获取到channel注册的evenloop (evenloop本质上是吃力网络读写的reactor线程)

netty网络传播的本质是,当有网络io的操作时候会发生channelPipeline中对应的事件方法;channel在io操作中产生的io事件,驱动事件在channelpieline中传播。由对应的channelhandler进行事件处理。不关心的事件直接忽略。功能等价于aop像切面一样

两个重要的channel介绍一下 nioServerSocketChannel 和 nioSocketChannel

nioServerSocketChannel

对于nioserversocketchannel来说,他的读取操作就是接受客户端的链接,然后创建niosocketChannel对象

nioSocketChannel

1.链接操作 socket.bind + socketchannel.connect;如果链接success将niosocketChannel中的selectionKey设置为OP_CONNECT

什么是ChannelPipeline 和 ChannelHandler

就是一种责任链模式

Netty将channel的数据管道抽象为channelpipeline ,消息在channelPipeline中传递。channelHandler为事件拦截器,被channelpipeline持有(pipeline是handler的容器),用户可以通过新增和删除channelhandler进行不同的业务处理,不需要对已有的handler进行修改。

本章三个概念:channelpipeline channelhandler channelhandlerContext

pipeline的流程图

pipeline的流程图详解

左边一列:数据读取的流程,底层的socketchannel 调用read()读取bytebuf -----> 触发io线程eventloop调用channelpipeline的fireChannelRead的方法。将消息(bytebuf)传输到channelPipeline中 --------->依次被channelhandler(1.2.3.4)一直到tailHandler(任何一个channelhandler都可以拦截和处理当前的消息结束消息传递

右边一列:调用channelContext的write方法,从tailHandler开始,途径channelHandler(N...1)将消息发送到缓冲区中等待刷新,也可以中断流程如编码失败的时候,需要中断消息构造future返回

Netty的事件:inbound事件。outbound事件(参看上面的流程图)这两个事件只是netty根据事件在pipeline中流向抽象出来的术语

inbound事件:(流程图左边)(都是执行的channelHandlerContext的 方法)(从io线程流向用户业务的handler)
1.channel注册 》 channelHandlerContext.fireChannelRegistered()
2.TCP链路建立成功,Channel激活事件
3.读事件
4.读完之后的通知事件
5.异常通知事件
6.Channel的可写状态变化通知事件
7.TCP关闭

outbound事件(流程图右边)(都是执行的channelHandlerContext的 方法)(由用户线程或者代码发起的io操作)
1.bind 绑定本地地址事件
2.链接服务端事件
3.write发送事件
4.flush刷新事件
5.read读事件
6.关闭当前channel事件

pipeline的构成>使用ServerBootStrap 或者 bootStrap启动服务端和客户端 netty为每一个channel都会单独的创建一个独立的pipeline

channelhandler是线程不安全的,channelpipeline是线程安全的,channelhandler支持动态的添加和删除,:使用场景在业务高峰期的时候要对系统进行阻塞保护的时候,就可以动态的增减阻塞的channelhandler

channelpipeline维护了channlerhandler 名和 channelHandlerContext实例的映射关系

ChannelHandler支持注解:
1.sharable 多个channelPipeline公用一个channlehandler
2.skip被注释的方法,直接跳过

解码的处理流程:


messageToMessageDecoder  》 byteTomessageDecoder(都是实现channelHandler接口)

NETTY 提供的半包解码器:

1.lineBaseFrameDecoder
2.DelimiterBasedFrameDecoder
3.LengthFieldBasedFrameDecoder :通过长度区分  与其对应的是LengthFieldPrepender将消息编码为 : 长度字段+原消息

如何区分整包:固定长度。回车换行符。分隔符。指定长度

到此为止前面的问题到底是啥?知道了pipeline 和 channelhandler的联系(容器),但是channelhandlerContext和他们的联系是啥呢?????

以下是通过资料查询这三者的关系:

三者的关系

pipeline是handler的通道,context表示ChannelHandler和ChannelPipeline之间的关系

注意:一个ChannelHandler可以属于多个ChannelPipeline,它也可以绑定多个ChannelHandlerContext实例,但是要使用注解sharable

注意二:handler线程不安全的所以要注意

什么是EventLoop和EventLoopGroup?

netty我们会创建boss和woker线程组带着问题看一下?

netty框架主要的线程就是IO线程

Netty线程模型本质上遵循了Reactor的基础线程模型,但是实现与Reactor存在差异.首先我们回顾一下Reactor的线程模型,这地方我应该整理 传送=======>
https://www.jianshu.com/writer#/notebooks/48158437/notes/79347965

回顾完以后看下netty

虽然netty官方的demo是主从reactor模式但是也是支持netty用单或者多线程的模型

Netty的最佳实践

1.创建两个NioEventLoopGroup用于隔离nio Acceptor 和 nio 的io线程 (理解为一个是链接线程一个是读数据的线程)
2.不要在ChannelHandler中启动用户线程,可以启用线程可以将解码后的消息发送到业务线程
3.解码操作要在nio的解码handler中操作,不要切换到用户线程中完成消息解码
4.如果业务处理简单可以直接在nio线程上完成业务逻辑编排
5.如果业务复杂,不要在nio线程上完成业务,要将解码的消息封装成task交付给业务线程中执行,尽快将nio线程释放,

推荐线程数量计算公式

线程数量 = (线程总时间/瓶颈资源时间) * 瓶颈资源的线程并行数

NioEventLoop:不单单是一个io线程,除了负责io的读写,可以实现定时任务,可以处理系统task。(当io线程和用户线程都要操作网络资源的时候,为防止并发操作导致的锁竞争,将用户线程的操作task放到队列中,有io线程同一处理,实现局部无锁化)

Netty解决了空轮询

空轮询的bug:正常情况下selector的select操作是阻塞的,只有被监听的fd有读写的时候才会被唤醒,但是空轮询的bug是没有 读写的时候也会被唤醒,所以会出现空轮询的异常,

产生的原因:linux2.6内核中, poll和epoll突然中断连接的socket的场景下,会将exenset集合设置为pollup。或者是poerr 。这样就造成了evenset事件集合发生了变化。将selector被唤醒就出现了空轮询

代码如下:

Netty解决空轮询的方式:

1.对selector的select的操作周期进行统计
2.每次出现空轮询都记录数
3.如果再某个周期内出现了n次空轮询则证明出现了空轮询的bug

Netty会通过重建selector进行系统恢复

ChannelFuture是异步获取io操作结果的,分为两种状态uncompleted 和 completed 。再其刚创建的时候处于第一种状态,在其完成之后会处于第二种状态,第三种状态也分为操作成功,失败,取消三种

Netty的架构剖析

Netty的行业应用

多线程编程在netty中的应用

拓展知识:

主流的操作系统提供了线程的实现,主要有三种

内核线程:这种线程有内核来完成线程的切换,内核通过线程调度器来对线程进行调度,并负责将线程任务映射到不同的处理器上

用户线程实现:完全建立在用户空间的线程库上,用户线程的创建,启动,运行完全在用户态中完成,不需要内核的帮助,因此执行性能更改

混合实现:顾名思义

sun-jdk:在内核线程实现
solaris-jdk:可以设置参数选择在哪里实现

Netty的逻辑架构

又上向下 
service
    |
pipeline
    |
Reactor

解释:
Reactor通信调度层:包含ReactornioeventLoop。niosocketChannel , bytebuf,主要负责监听网络的读写和链接操作,负责将网络层数据读取到内存中,例如创建连接,读写事件,并将事件触发到pipeline等

ChannelPipeline职责链:负责动态的编排职责连。职责链选择监听和处理自己关心的事件,可以将外部的消息转换成自己内部的pojo对象,这样上层业务只关心业务处理即可

service业务编排层,对于业务开发人只需要关心业务逻辑制定即可,这种分层架构设计理念实现了nio框架各层之间的解耦,便于上层协议栈的开发和业务逻辑定制

NETTY优化的地方:

1.非阻塞的io库,基于reactor实现,解决同步阻塞io模式下,服务端无法平滑增长客户端的问题
2.tcp接收发送缓冲区用直接内存,避免内存复制
3.通过内存池方式循环利用bytebuf,避免频繁常见和销毁bytebuf
4.环形数组缓冲区实现无锁化并发编程
5.关键资源的单线程串行处理,避免多线程并发带来的所竞争和额外的cpu资源消耗
6.通过引用计数器及时释放不在引用的对象,锁粒度的内存管理降低了gc的频率

netty的可靠性

检测链路的有效性,由于长链接每次发送消息都要创建链路,性能相比于短连接性能更高,netty提供了两种心跳检测(空闲时检测机制)
读空闲超时机制
当连续T周期没有消息可读,触发超时handler,并发送心跳消息,进行链路检测,当n个周期没有收到心跳则关闭链路

写空闲超时机制
当有T个周期没有数据要发的情况下,用户可以基于写空闲超时发送心跳消息,进行链路检测,如果连续n个周期没有收到对方心跳,主动关闭

netty的可制定性

1.责任链模式,channelpipeline基于责任链开发,便于业务逻辑的拦截,定制和拓展
2.基于接口的开发:关键的类库都提供了接口或者抽象类,如果netty无法满足用户需求,可以由用户自定义实现类
3.提供了工厂类,通过重载这些工厂类可以按需创建出用户实现的对象
4.提供大量系统参数,供用户按需设置,增强系统的场景定制性

Dubbo与Netty

流程图:

DUBBO的RPC框架,通过dubbo encode方式将pojo对象编码为dubbo协议的二进制字节流,通过netty client发送给服务提供者,服务提供者的netty server从niosocketChannel读取二进制码流,将bytebuf解码为dubbo请求消息并调用服务提供者,处理完后返回

netty位dubbo提供的了高性能nio框架,主要职责在:
1.提供了异步高性能的nio框架
2.nio客户端和服务端
3.心跳检测能力
4.断链重试机制
5.流量控制
6.dubbo协议的编解码handler

NETTY CLIENT 的主要实现:

nettyhandler 持有了dubbo的自定义的channelhandler ,通过channelhandler 回调dubbo的filter,实现rpc的服务调用

学习高性能
https://www.cnblogs.com/flgb/p/13122281.html

你可能感兴趣的:(netty权威指南学习)