IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> netty -> 正文阅读

[网络协议]netty

如何理解netty

Netty 是一个异步的、基于事件驱动的网络应用框架Netty

netty的异步还是基于多路复用的,并没有实现真正意义上的异步IO

事件驱动的Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式

异步:Netty中的I/O操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture,调用者并不能立刻获得结果,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。

netty

  • 使用简单:封装了 NIO 的很多细节,使用更简单,解决了传统NIO的epollbug

  • 使用了主从Reacor线程模型,实现了IO多路复用

  • 自带各种编解码器handler解决 TCP 粘包/拆包问题,还可以通过 ChannelHandler 对通信框架进行灵活地扩展。

  • 还有适用于不同场景的各种高性能序列化传输协议,

  • 支持零拷贝,减少不必要的内存拷贝,实现了更高效率的传输。

为什么使用Netty

为什么不用NIO呢?

NIO的类库和API繁杂,学习成本高,要熟悉多线程对开发者要求高

epollbug会导致空轮询

fd是一个表示连接的文件描述符,这个连接已经关闭,但是在epoll里面还存在,导致epollwait方法一直触发,因为epollwait会等待io事件的触发,然后selector被唤醒一直轮询多个通道但是都没有消息处理最终导致CPU百分之百

Netty的解决策略:

\1) 设定一个轮询时间来记录,如果小于这个轮询时间并且连续发生512次次空轮询就认为发生了epollbug

\2) 将问题Selector上注册的Channel转移到新建的Selector上;

\3) 老的问题Selector关闭,使用新建的Selector替换。

  • Netty 应用场景

    1. 作为 RPC 框架的网络通信工具 :使用 Netty 作为基础通信组件
    2. 实现一个自己的 HTTP 服务器 :使用 Netty 作为基础通信组件
    3. 实现一个即时通讯系统 :使用 Netty 作为基础通信组件
    4. 实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。
  • 解决 TCP 传输问题,如粘包、半包,因为本质还是依靠tcp传输的

Reactor 模式

就是一个或者多个输入同时传递给服务处理器,然后服务处理器又将他们分派给对应线程处理,就是监听事件收到后分发,IO多路复用就是这种思想

Reactor 模式主要由 Reactor 和Acceptor和handler几个核心部分组成,它俩负责的事情如下:

Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件

  • Acceptor 对象的作用是获取连接;
  • Handler 对象的作用是处理具体事件;

根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现:

  • 单 Reactor 单线程

  • Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;

    如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;

    如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;

    但是一个线程就会出现一个事件没执行完其他事件就没法执行,消息积压而且无法同时处理消息建立和分发操作,导致效率低下

    Redis虽然也是采用这种方案但是它C语言编写的是基于内存操作的,性能较快

  • 单 Reactor 多线程

  • Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;

    如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;

    如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;

    但是这次handler不再负责业务处理,他会把业务处理的工作交给子线程,子线程处理完了再交给它发给客户端,它只负责数据的接受和发送。通过引入多线程充分利用CPU多核提高了执行效率。但是这里存在多线程竞争的情况所以要给共享资源加上互斥锁

    此外:单 Reactor」的模式还有个问题,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方,所以引入了主从 Reactor 多线程

  • 主从 Reactor 多线程

  • Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,这次分发给的不是handler和acceptor,而是主Reactor 分发给从Reactor 和acceptor,然后从Reactor 通过 select 监听事件,从Reacotr创建一个 Handler 用于处理连接的响应事件。主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理,处理完了子线程直接发给客户端

  • netty也基于这种方式实现,一个BOSSgroup充当主Reacotor,然后workgroud轮询bossgroup发过来的业务处理事件充当从Reacotr

img

还有一种proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,而Reactor是应用进程来做的。所以其他IO接受数据的时候都需要从内核态拷贝到用户态,但是异步IO是唯一不需要的,但是linux对异步IO不完善

Netty 核心组件有哪些?分别有什么作用?

Bootstrap、ServerBootstrap
Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。ServerBootstrap 设置 Channel 属性有optionchildOption两个方法,option 主要负责设置 Boss 线程组,而 childOption 对应的是 Worker 线程组。

事件循环组 EventLoopGroup

当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。

EventLoopGroup 里面又 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全),类似一个线程池,内部维护了一组eventloop。

这个group默认构造2n线程数,如果不是2

这里group为了追求效率

Group里面调取eventloop是通过轮询机制实现,如果线程数也就是eventloop不是2的n次方采用取模,是2的n次方就采用逻辑与运算达到和取模一样的效果并且效率更高

image.png

EventLoop

每个 EventLoop 线程都维护一个 Selector 选择器和任务队列 taskQueue。它主要负责处理 I/O 事件、普通任务和定时任务。NioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。

每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个eventloop。

并且NioEventLoop 通过核心方法 select() 不断轮询注册的 I/O 事件。当没有 I/O 事件产生时,为了避免 NioEventLoop 线程一直循环空转,在获取 I/O 事件或者异步任务时需要阻塞线程,等待 I/O 事件就绪或者异步任务产生后才唤醒线程。

NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:

定时任务用的 ScheduleTaskQueue ,如果是 Reactor 线程内部调用,因为是串行执行,所以不会有线程安全问题。如果是外部线程添加定时任务,我们发现 Netty 把添加定时任务的操作又再次封装成一个任务交由 Mpsc Queue处理

普通任务用的 taskQueue,底层是基于Mpsc队列实现,通过这个队列保证了channel的线程安全

Mpsc Queue 可以保证多个生产者同时访问队列是线程安全的,而且同一时刻只允许一个消费者从队列中读取数据。Netty Reactor 线程中任务队列 taskQueue 必须满足多个生产者可以同时提交任务

Selector

& 可以用作逻辑与的运算符,表示逻辑与(and)

监听事件,管理注册到Selector中的channel,实现多路复用器,

早期这个操作系统调用的名字是select,但是性能低下,后来渐渐演化成了Linux下的epoll

Channel

channel和pipeline区别?

把channel理解为目的地,你要向目的地发送消息的话,会经过管道pipeline ,这个管道上有重重阻碍,就是handler,channelpipeline,类似于一个中间件,在中间帮你进行拦截和过滤

一种连接到网络套接字或能进行读、写、连接和绑定等I/O操作的组件。

channel为用户提供:

  1. 通道当前的状态(例如它是打开?还是已连接?)
  2. channel的配置参数(例如接收缓冲区的大小)
  3. channel支持的IO操作(例如读、写、连接和绑定),以及处理与channel相关联的所有IO事件和请求的ChannelPipeline。

通道类型:

NioServerSocketChannel: 异步非阻塞的服务器端 TCP Socket 连接。

OioSocketChannel: 同步阻塞的客户端 TCP Socket 连接。

常用的就是这两个通道类型,因为是异步非阻塞的。所以是首选。

它除了包括基本的 I/O 操作,如 bind()connect()read()write() 等。

最主要的是它内部的pipeline和handler,才是通信的关键

ChannelHandler 和 ChannelPipeline

总结:

一 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式

pipeline相当于handler的容器。初始化channel时,把channelHandler按顺序装在pipeline中,就可以实现按序执行channelHandler了。ChannelPipeline并不是直接管理ChannelHandler,而是通过ChannelHandlerContext来间接管理

pipeline是结构是一个带有head与tail指针的双向链表,其中的节点为handlercontext,handlercontext关联了一个handler,每个handler将当前handler的处理结果传递给下一个handler

我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler

  • 当有入站(Inbound)操作时,会从head开始向后调用inhandler,直到handler不是处理Inbound操作为止
  • 当有出站(Outbound)操作时,会从tail开始向前调用outhandler,直到handler不是处理Outbound操作为止

ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。

ChannelHandlerContext

用于在Handler中获取pipeline对象,或者channel对象,进行读写等操作

保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象

ChannelFuture

Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。

因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

通过channelFuture.sync()方法,阻塞主线程,同步处理结果

并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel

writeAndFlush

  • writeAndFlush 属于出站操作,它是从 Pipeline 的 Tail 节点开始进行事件传播,一直向前传播到 Head 节点。不管在 write 还是 flush 过程,Head 节点都中扮演着重要的角色。
  • write 方法并没有将数据写入 Socket 缓冲区,只是将数据写入到 ChannelOutboundBuffer 缓存中,ChannelOutboundBuffer 缓存内部是由单向链表实现的。
  • flush 方法才最终将数据写入到 Socket 缓冲区。

Netty 服务端和客户端的启动过程了解么?

首先你创建了两个 NioEventLoopGroup 对象实例:bossGroup 和 workerGroup。

  • bossGroup : 用于处理客户端的 TCP 连接请求。
  • workerGroup :负责每一条连接的具体读写数据的处理逻辑,真正负责 I/O 读写操作,交由对应的 Handler 处理。

一般情况下我们会指定 bossGroup 的 线程数为 1(并发连接量不大的时候) ,workGroup 的线程数量为 CPU 核心数 *2 ,如果不指定默认的话就是2n

接下来 我们创建了一个服务端启动引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作。

通过 .group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型。

通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO

通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了服务端消息的业务处理逻辑 HelloServerHandler 对象

调用 ServerBootstrap 类的 bind()方法绑定端口

客户端:

1.创建一个 NioEventLoopGroup 对象实例

2.创建客户端启动的引导类是 Bootstrap

3.通过 .group() 方法给引导类 Bootstrap 配置一个线程组

4.通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO

5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了客户端消息的业务处理逻辑 HelloClientHandler 对象

6.调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:

  • inetHost : ip 地址
  • inetPort : 端口号
  • connect 方法返回的是一个 Future 类型的对象
  • 这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息,或者用sync阻塞主线程同步获得处理结果

ByteBuf

ByteBuf实现了两个接口,分别是ReferenceCounted和Comparable。Comparable是JDK自带的接口,表示该类之间是可以进行比较的。而ReferenceCounted表示的是对象的引用统计。当一个ReferenceCounted被实例化之后,其引用count=1,每次调用retain() 方法,就会增加count,调用release() 方法又会减少count。当count减为0之后,对象将会被释放,如果试图访问被释放过后的对象,则会报访问异常。ByteBuf是一个可以比较的,可以计算引用次数的对象。他提供了序列或者随机的byte访问机制。

传统IO

java语言本身不具备磁盘读写能力,要调用磁盘就要从用户态切换到内核态调用操作系统的本地方法,然后读取到系统缓冲区,再在用户态堆里面分配一块java缓冲区,再从系统缓冲区拷贝到java缓冲区,数据写入的时候就从java缓冲区再拷贝回去,做了一个不必要数据复制,因而效率不会很高

ByteBuf 有多种实现类,每种都有不同的特性,下图是 ByteBuf 的家族图谱,可以划分为三个不同的维度:Heap/DirectPooled/UnpooledUnsafe/非 Unsafe

直接内存(Direct Memory)

在 buffer 方法中使用了ByteBuffer.allocateDirect,就是说分配了一个直接内存,这个方法调用了之后表示在操作系统中划出了一个为 1M 大小的缓冲区供当前使用。这块区域对于Java程序来说是可以直接访问的,java程序可以使用,计算机系统也可以进行使用是一块共享的区域。

内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。提升复制速度(io效率)

JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请

Heap/Direct 就是堆内和堆外内存。Heap 指的是在 JVM 堆内分配,底层依赖的是字节数据;Direct 则是堆外内存,不受 JVM 限制,分配方式依赖 JDK 底层的 ByteBuffer。

Pooled/Unpooled 表示池化还是非池化内存。Pooled 是从预先分配好的内存中取出,使用完可以放回 ByteBuf 内存池,等待下一次分配。而 Unpooled 是直接调用系统 API 去申请内存,确保能够被 JVM GC 管理回收。

Unsafe/非 Unsafe 的区别在于操作方式是否安全。 Unsafe 表示每次调用 JDK 的 Unsafe 对象操作物理内存,依赖 offset + index 的方式操作数据。非 Unsafe 则不需要依赖 JDK 的 Unsafe 对象,直接通过数组下标的方式操作数据。

内存分配的角度来看,ByteBuf 可以分为堆内存 HeapByteBuf 和堆外内存 DirectByteBuf。

堆内存 HeapByteBuf 虽然有GC垃圾回收机制使得回收效率较高但是会增加两次拷贝操作。

堆外内存DirectByteBuf可以减少内核到用户空间和用户空间到内核两次拷贝,但是回收效率比较低

并且为了避免堆外内存的频繁创建和销毁,Netty 提供了池化类型的 PooledDirectByteBuf

Netty 提前申请一块连续内存作为 ByteBuf 内存池,如果有堆外内存申请的需求直接从内存池里获取即可,使用完之后必须重新放回内存池,否则会造成严重的内存泄漏

为什么需要 DirectByteBuffer

主要是零拷贝

首先介绍:继承于MappedByteBuffer

  • Position:从哪个位置开始映射,字节数的位置;
  • Size:从position开始向后多少个字节;

DirectByteBuffer是在堆外内存分配的空间,堆里面只存那块内存的引用,避免了堆外堆内的来回拷贝

其次,回答为什么需要 Buffer 。

缓冲区就是在内存中预留出指定大小的存储空间,然后对输入/输出(简称i/o)进行数据的临时存储,这部分区域就称为缓冲区 也叫Buffer

我们知道 GC 会管理内存,大致上可以这么认为,其主要做两件事:

  • 回收不被引用的对象;
  • 整理内存空间(比如将有效的对象整理到一起);
  • 如果在堆上操作会造成IO写入地址不完整,因为会发生垃圾回收,而JNI本地方法接口规定了不完整地址不能生效,当然也可以在IO期间选择禁止GC,但是IO时间长容易内存溢出。所以设定了缓冲区。

堆外内存垃圾回收怎么实现的?

DirectByteBuffer是通过虚引用(Phantom Reference)来实现堆外内存的释放的。

堆外内存的回收其实依赖于我们的GC机制

首先,我们要知道在java层面和我们在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,它在堆里面记录了这块内存的基地址以及大小,那么既然和GC也有关,那就是GC能通过操作DirectByteBuffer对象来间接操作对应的堆外内存了。

DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference其实主要是用来跟踪对象何时被回收的,它不能影响GC决策。

GC过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到一个队列里,在GC完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。

操作系统不能直接使用JVM堆内存进行 I/O 的读写?

如果在JVM 内部执行 I/O 操作时,必须将数据拷贝到堆外内存,才能执行系统调用。 VM语言都会存在的问题,那么为什么操作系统不能直接使用JVM堆内存进行 I/O 的读写呢?

主要有两点原因:第一,操作系统并不感知 JVM 的堆内存,而且 JVM 的内存布局与操作系统所分配的是不一样的,操作系统并不会按照 JVM 的行为来读写数据。第二,同一个对象的内存地址随着 JVM GC 的执行可能会随时发生变化,例如 JVM GC 的过程中会通过压缩来减少内存碎片,这就涉及对象移动的问题了。

零拷贝

「内核缓冲区」实际上是**磁盘?速缓存(****PageCache)**PageCache 的优点主要是两个:

缓存最近被访问的数据;

预读功能;

2 次 DMA 拷贝都是依赖硬件来完成,不需要 CPU 参与,零拷贝是一个广义的概念,可以认为只要能够减少不必要的 CPU 拷贝,都可以理解为是零拷贝。

netty零拷贝和java零拷贝不一样,是基于应用层面的

1.Netty 在进行 I/O 操作时都是使用的堆外内存,可以避免数据从 JVM 堆内存到堆外内存的拷贝。

2.Composite Buf

CompositeByteBuf 可以理解为一个虚拟的 Buffer 对象,它是由多个 ByteBuf 组合而成,但是在 CompositeByteBuf 内部保存着每个 ByteBuf 的引用关系,从逻辑上构成一个整体。

CompositeByteBuf 内部维护了一个 Components 数组。在每个 Component 中存放着不同的 ByteBuf,各个 ByteBuf 独立维护自己的读写索引,而 CompositeByteBuf 自身也会单独维护一个读写索引。

Drawing 3.png

传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,比如有一个size1大小的buffer和一个size2大小的buffer,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组size1和size2中的数据分别拷贝到新的数组中,这里涉及到两次CPU拷贝操作。但是netty只需要通过读写索引找到可读字节和可写字节就能找到size1和size2并且逻辑上将他们组合到一起。

wrapper

wrappedBuffer 同时也是创建 CompositeByteBuf 对象的另一种推荐做法。

wrappedBuffer 方法可以将不同的数据源的一个或者多个数据包装成一个大的 ByteBuf 对象,其中数据源的类型包括 byte[]、ByteBuf、ByteBuffer。包装的过程中不会发生数据拷贝操作,包装后生成的 ByteBuf 对象和原始 ByteBuf 对象是共享底层的 byte 数组。

ByteBuf.slice 操作

ByteBuf.slice 和 wrappedBuffer 的逻辑正好相反,ByteBuf.slice 是将一个 ByteBuf 对象切分成多个共享同一个底层存储的 ByteBuf 对象。也就是说虽然逻辑上切分了,但是底层的存储仍然是共享。

通过 slice 切分后都会返回一个新的 ByteBuf 对象,而且新的对象有自己独立的 readerIndex、writerIndex 索引,由于新的 ByteBuf 对象与原始的 ByteBuf 对象数据是共享的,所以通过新的 ByteBuf 对象进行数据操作也会对原始 ByteBuf 对象生效。

对于FileChannel.transferTo的使用

Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。

DMA 引擎从文件中读取数据拷贝到内核态缓冲区之后,由操作系统直接拷贝到 Socket 缓冲区,不再拷贝到用户态缓冲区,所以数据拷贝的次数从之前的 4 次减少到 3 次。

java的零拷贝

读取磁盘数据的时候,之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或网卡

DMA :进行IO数据磁盘与系统内核交互的时候,磁盘缓冲区数据满了以后由DMA去调度资源拷贝到内核中。主要是避免CPU进行大量数据搬运。

传统情况下数据会被拷贝四次,先是DMA控制从磁盘到内核缓冲区,再由CPU负责从内核到用户,CPU再负责从用户到内核socket缓冲区,然后DMA控制从内核socket缓冲区到网卡。用户缓冲区的传输是没必要的,因为并没有对数据加工处理。这个过程会有read和write来调用应用到内核

通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程,因为read会把内核数据拷贝到应用层

mmap() +write()系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。这样的话上面四次拷贝就变成DMA控制磁盘到内核缓冲区,这时候操作系统和应用层共享这个缓冲区,应用进程调用write就可以直接在内核态里面把数据从缓冲区直接通过CPU拷贝到socket缓冲区,再由DMA搬运到网卡。

sendfile

还有一种CPU一次拷贝都不需要的零拷贝,这种需要网卡支持 SG-DMA,DMA搬运磁盘数据到内核缓冲区以后,缓冲区描述符和长度直接传给socket缓冲区,然后SG-DMA直接把磁盘搬运到缓冲区的数据拷贝到网卡上,连socket缓冲区都不需要过去,全程DMA控制,真正意义的零拷贝

如果不支持SG-DMA,就还是把数据拷贝到 socket 缓冲区,但是不需要read和write这两个系统调度了

PageCache 的优点主要是两个:

  • 缓存最近被访问的数据;
  • 预读功能;
  • 所以,针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。

Netty 支持哪些常用的解码器?

固定长度解码器 FixedLengthFrameDecoder

特殊分隔符解码器 DelimiterBasedFrameDecoder

长度域解码器 LengthFieldBasedFrameDecoder

粘包拆包问题:如何获取一个完整的网络包

消息长度固定

每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。当发送方的数据小于固定长度时,则需要空位补齐。

消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。

特定分隔符

既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。以下报文根据特定分隔符 \n 按行解析,即可得到 AB、CDEF、GHIJ、K、LM 五条原始报文。

由于在发送报文时尾部需要添加特定分隔符,所以对于分隔符的选择一定要避免和消息体中字符相同,以免冲突。否则可能出现错误的消息拆分。比较推荐的做法是将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符。特定分隔符法在消息协议足够简单的场景下比较高效,例如大名鼎鼎的 Redis 在通信过程中采用的就是换行分隔符。

Base64,顾名思义,就是包括小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/“一共64个字符的字符集,(另加一个“=”,实际是65个字符,至于为什么还会有一个“=”,这个后面再说)。任何符号都可以转换成这个字符集中的字符,这个转换过程就叫做base64编码。

Redis 在通信过程中采用的就是换行分隔符。

消息头+消息内容

消息头用四字节int值存取消息总长度,消息体存数据。接收方接受数据先取出这个总长度值,然后再解析整个消息判断这是不是一个完整的报文。这种方式更加灵活,再消息头还可以放其他东西

常见的序列化方式

序列化考虑优先级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-81tYP2kw-1642241141544)(image-20211224062529861.png)]

用JDK原生自带的inputstream和outputstream。原理就是序列化的时候加入一些分割符,分割头部数据和对象数据,头部一般用来声明序列化协议和版本,对象数据包括类和属性这些,然后反序列化根据分割符解析出来创建一个新的对象。

  • 序列化对象需实现Serialization接口

  • static属性不能被序列化,序列化保存对象的状态,static属于类状态

  • transient修饰的不能被序列化

    缺点:不能跨语言,只能给java序列化反序列化

    ? 对比当前主流的序列化协议,序列化后数据流太大,导致占网络资源较多

    ? 采用的同步阻塞IO,性能太差

    ? 不安全,用readObject() 方法进行反序列化,实现了 Serializable 接口的对象都实例化。也就是说什么代码都会执行,这样非常危险

json是key-value形式文本型序列化框架,可以用来http和前台web调用

优点就是数据格式简单方便读写,兼容性好。

问题:空间开销大,json没有类型使用java的时候要用反射解决所以导致性能不高因为反射降低性能。rpc用它的时候只能用在数据比较少的情况

kryo目前协议中序列化反序列化性能极高,花费的时间比其他协议都少,序列化后体积小,并且API友好不需要实现序列化接口

但是缺点就是兼容性不好和线程不安全

protobufProtocol Buffers)底层用C++实现的,谷歌开源的序列化协议,也可以跨语言但是要使用工具进行编译成二进制文件,它的消耗主要就是编译过程,本身性能非常好。并且比起其他几种序列化方式安全性更高。性能只比kyro差一点,比json,hessian,java原生化都强,但是兼容性比kyro强,在序列化协议里面我们一般优先考虑安全然后考虑兼容之后才考虑性能,所以它相比之下更加优秀

.Hessian性能比jdk和json高,生成字节数更小而且支持跨语言而且兼容性更好因为跨语言不需要工具编译,dubbo就是用这个

缺点:但是对java里面linked系列不支持,比如linkedhahsmap,linkedhashset,byteshort反序列化回生成int,

  • ? Hessian序列化后的数据要比Kyro序列化后的数据大; 缺乏安全机制,传输没有加密处理

hessian使用更加方便兼容性做的更好,protobuf性能更好一点

简单说下 BIO、NIO 和 AIO

Netty 是如何保持长连接的

什么是长连接?

客户端和服务器之间定期发送的一种特殊的数据包,通知对方自己还在线, 以确保 TCP 连接的有效性。但是由于网络不稳定性,有可能在 TCP 保持长连接的过程中,由于某些突发情况, 例如网线被拔出, 突然掉电等。 会造成服务器和客户端的连接中断。在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话,那么它们是不能在短时间内发现对方已经掉线的。

如何保持长连接?

利用心跳维护长连接信息。

在服务器和客户端之间一定时间内没有数据交互时,即处于 idle 状态时,客户端或服务器会发送一个特殊的数据包给对方,当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。

当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性。

Netty 发送消息有几种方式?
Netty 有两种发送消息的方式:

直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动;
写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。

netty长连接短连接

长连接就是tcp三握四挥建立连接完成一次读写以后以后不会立刻完毕,发送完就关闭就是短连接,通常用长连接来处理资源请求比较频繁的客户端,避免反复三次握手四次挥手造成网络资源浪费

TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,他们是无法发现对方已经掉线的。这种情况叫连接假死,为了解决这个问题, 我们就需要引入 心跳机制

TPS/QPS 很高的 REST 服务中,如果使用的是短连接(即没有开启keep-alive),则很可能发生客户端端口被占满的情形

这是由于短时间内会创建大量TCP 连接,而在 TCP 四次挥手结束后,客户端的端口会处于 TIME_WAIT一段时间(2*MSL),

这期间端口不会被释放,从而导致端口被占满。这种情况下最好使用长连接。

netty设计模式

NioEventLoop 通过核心方法 select() 不断轮询注册的 I/O 事件,Netty 提供了选择策略 SelectStrategy 对象,它用于控制 select 循环行为,SelectStrategy 对象的默认实现就是使用的饿汉式单例

  • CONTINUE,表示需要重试。
  • SELECT,表示需要通过select方法获取就绪的Channel列表,当NioEventLoop中不存在异步任务时,也就是任务队列为空,则返回该策略。

Netty 在创建 Channel 的时候使用的就是工厂方法模式,因为服务端和客户端的 Channel 是不一样的。Netty 将反射和工厂方法模式结合在一起,只使用一个工厂类,然后根据传入的 Class 参数然后利用反射来构建出对应的 Channel,不需要再为每一种 Channel 类型创建一个工厂类。NioServerSocketChannel类的构造函数里通过反射拿到jdk底层的channel,还有tcp配置参数,阻塞模式,pipeline这些

Bootstrap采用了典型的Builder模式构造对象,首先创建一个空实例,然后调用方法设置Bootstrap的必要属性。

把复杂的对象通过一个个简单的对象构造而成

  • 必须调用group方法设置EventLoopGroup实例group,用于后期注册Channel到EventLoop;
  • 调用channel方法设置Channel类型,该方法会初始化ChannelFactory实例,用于后期创建Channel实例;
  • 调用handler设置ChannelInitializer实例,用于添加自定义Handler。

责任链模式

对请求发送者和接收者解耦,让接受请求的对象形成一条链,并且沿着这条链传递请求,直到有一个对象处理它为止。

ChannlPipeline 和 ChannelHandler。ChannlPipeline 内部是由一组 ChannelHandler 实例组成的,内部通过双向链表将不同的 ChannelHandler 链接在一起,ChannelHandlerContext 会默认将处理器上下文信息传递到下一个处理器,也可以指定传到某个处理器上。

观察者模式

被观察者发布消息,观察者订阅消息

addListener 方法会将添加监听器添加到 ChannelFuture 当中,然后channelFuture执行完毕就通知注册了的监听器,进行下一步操作

这里被观察者就是channelfuture,观察者就是addlistener

装饰者模式

装饰器模式是对被装饰类的功能增强,在不修改被装饰类的前提下,能够为被装饰类添加新的功能特性。

wrappedBuffer 同时也是创建 CompositeByteBuf 对象的另一种推荐做法。

wrappedBuffer 方法可以将不同的数据源的一个或者多个数据包装成一个大的 ByteBuf 对象,其中数据源的类型包括 byte[]、ByteBuf、ByteBuffer。包装的过程中不会发生数据拷贝操作,包装后生成的 ByteBuf 对象和原始 ByteBuf 对象是共享底层的 byte 数组。

CompositeByteBuf 就是实现零拷贝的关键

netty线程池作用

线程池隔离

我们知道,如果有复杂且耗时的业务逻辑,推荐的做法是在 ChannelHandler 处理器中自定义新的业务线程池,将耗时的操作提交到业务线程池中执行。建议根据业务逻辑的核心等级拆分出多个业务线程池,如果某类业务逻辑出现异常造成线程池资源耗尽,也不会影响到其他业务逻辑,从而提高应用程序整体可用率。对于 Netty I/O 线程来说,每个 EventLoop 可以与某类业务线程池绑定,避免出现多线程锁竞争。

连接空闲检测 + 心跳检测

TCP KeepAlive 是用于检测连接的死活,而心跳机制则附带一个额外的功能:检测通讯双方的存活状态。两者听起来似乎是一个意思,但实际上却大相径庭。

连接空闲检测就是服务器每隔一段时间检测是否有数据读写,如果一直能收到客户端发来的数据就说明还是活跃状态,如果没有收到也不一定是假死状态,可能是客户端没数据发,但是连接还是健康的,所以需要心跳机检测,客户端定时向服务器发送一次心跳包,如果服务器没有收到就说明客户端已经下线了。心跳包就是为了方式没有数据读写这种情况,它是用来判断连接是否可用的。而keepalive

TCP的断开可能有时候是不能瞬时探知的,要服务端维持一个2h+10*75秒的死链接,keepalive设计初衷清除和回收死亡时间长的连接,不适合实时性高的场合,而且它会先要求连接一定时间内没有活动,周期长,这样其实已经断开很长一段时间,没有及时性。

心跳包就改善了这一点。并且可以防止TCP的死连接问题,避免出现长时间不在线的死链接仍然出现在服务端的管理任务中。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-01-16 13:28:51  更:2022-01-16 13:29:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 11:44:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码