Netty一文搞懂——核心原理篇<随手笔记>

在这里插入图片描述

1.server端工作原理

1.1.原理

server端启动时绑定本地某个端口,将自己NIOServerSocketChannel注册到某个bossNIOEventLoopselector上。

server端包含一个BossNIOEventGroup和一个WorkerNioEventaLoopGroupNioEventaLoopGroup相当于一个事件循环组,这个事件循环组里包含多个事件循环NioEventLoop,每个NioEventLoop包含一个selector和一个事件循环线程

每个BoosNioEventLoop循环执行的任务包含三步:

  1. 轮询read、write事件
  2. 处理I/O任务,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
  3. 处理任务队列中的任务,runAllTasks

1.2.启动流程

public class NettyServer {
	public static void main(String[] args) {
		NioEventLoopGroup bossGroup = new NioEventLoopGroup();
		NioEventLoopGroup workerGroup = new NioEventLoopGroup();
		ServerBootstrap serverBootstrap = new ServerBootstrap();
		serverBootstrap
			.group(boosGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.childHandler(new ChannelInitializer<NioSocketChannel>() {
				protected void initchannel(NioSocketChannel ch) {
				}
			});
		serverBootstrap.bind(8000);
	}
}
  1. 首先创建了两个NioEventLoopGroup,这两个对象可以看做是传统IO编程模型的两大线程组,bossGroup表示监听端口,accept新连接的线程组,workerGroup表示处理每一条连接的数据读写的线程组。
  2. 接下来创建了一个引导类ServerBootstrap,这个类将引导我们进服务端的启动工作,直接new出来。
  3. 通过.group(boosGroup, workerGroup)给引导类配置两大线程组,此引导类的线程模型也就定型了。
  4. 然后指定服务端的I/O模型为NIO,我们通过.channel(NioServerSocketChannel.class)来制定IO模型。
  5. 最后我们调用childHandler()方法给这个引导类创建一个channelInitialilzer,这里主要就是定义后续每条连接的数据读写、业务处理逻辑。channelInitiater这个类中的泛型参数NioSocketChannel是Netty对NIO类型的连接的抽象,而NioServerSocketChannel也是对NIO类型的连接的抽象,NioServerSocketChannel和NioSocketChannel的概念可以喝BIO编程模型中的serverSocket以及Socket两个概念对应上。

创建一个引导类,然后给它指定线程模型、IO模型、连续读写处理逻辑,绑定端口之后服务就启动起来了。

2.client端工作原理

2.1.原理

client端启动时connect到server建立NioSocketChannel,并注册到某个NioEventLoop的Selector上。

client端只包含一个NioEventLoopGroup,每个EventLoop循环执行的任务包含三步:

  1. 轮询connect、read、write事件
  2. 处理I/O任务,即connect、read、write事件,在NioSocketChannel上建立连接。可读、可写事件发生时进行处理
  3. 处理非I/O任务,runAllTasks

2.2.启动流程

@Slf4j
public class NettyClient {
    public static void main(String[] args) {
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        
                    }
                });
        bootstrap.connect("ip", 80).addListener(future -> {
            if (future.isSuccess()) {
                log.info("connect success.");
            } else {
                log.info("connect fail.");
            }
        });
    }
}
  1. 首先与服务端的启动一样,需要给它指定线程模型,驱动着连接的数据读写
  2. 然后指定IO模型为NioSocketChannel,表示IO模型为NIO
  3. 接着给引导类指定一个handler,这里主要就是定义连接的业务处理逻辑
  4. 配置完线程模型、IO模型、业务处理逻辑之后,调用connect方法进行连接,可以看到connect方法有两个参数,第一个参数可以填写IP或者域名,第二个参数填写的是端口号,由于方法返回的是Future,所以我们通过addListener方法监听结果。

3.ByteBuf

是一个节点容器,里面包含三部分内容:

  1. 已经丢弃的数据,这部分数据是无效的
  2. 可读字节,这部分数据是ByteBuf的主体
  3. 可写字节

这三段数据被两个指针给划分出来:读指针、写指针

本质:

  • 引用一段内存,此内存可以是堆内也可以是堆外的,然后用引用计数来控制这段内存是否需要被释放,使用读写指针来控制对ByteBuf的读写,可以理解为外观模式的一种使用
  • 基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法,要注意read或write与get/set的区别
  • 多个ByteBuf可以引用计数来控制内存的释放,遵循谁retain()谁release()的原则

ByteBuf和ByteBuffer的区别:

  1. 可扩展到用户定义的buffer类型
  2. 通过内置的复合buffer类型实现透明的零拷贝(zero-copy)
  3. 容量可以根据需要扩展
  4. 切换读写 模式不需要调用ByteBuffer.flip()方法
  5. 读写采用不同的索引
  6. 支持方法链接调用
  7. 支持引用计数

4.池技术–线程池等

4.1.ByteBuf和设计模式

  1. ByteBufAllocator抽象工厂模式

    在Netty的世界里ByteBuf实例通常应该由ByteBufAllocator来创建

  2. CompositeByteBuf组合模式

    CompositeByteBuf可以让我们把多个ByteBuf当成一个Buf来处理,ByteBufAllocator提供了compositeBuffer()工厂方法来创建 compositeByteBuf。它的实现使用了组合模式。

  3. ReadOnlyByteBuf装饰器模式

    ReadOnlyByteBuf使用装饰器模式把一个ByteBuf变为只读,ReadOnlyByteBuf通过调用unpooled.unmodifiableBuffer(ByteBuf)方法获得。
    ReadOnlyByteBuf->Ab

  4. ByteBufInputStream适配器模式

    ByteBufInputStream使用适配器模式使我们可以把ByteBuf当做Java的InputStream来使用,同理,ByteBufOutputStream允许我们把ByteBuf当做PutputStream来使用。

  5. ByteBuf工厂方法模式

    一般不直接通过构造函数来创建ByteBuf实例,而是通过Allocator来创建。从装饰器模式可以看出另一种获得ByteBuf的方式是调用ByteBuf的工厂方法,如:

    • ByteBuf # duplicate()
    • ByteBuf # slice()

4.2.channelHandler

channelHandler在只会对感兴趣的时间进行拦截处理,servlet的Filter过滤器负责对IO事件或者IO操作进行拦截和处理,他可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。pipeline和channelHandler他们通过责任链接设计模式来组织代码逻辑,并且支持逻辑的动态添加和删除。

channelHandler有两大子接口:

  • channelInBoundhandler,是处理读数据的逻辑
  • channelOutBoundHandler,是处理写数据的逻辑
    这两个接口分别对应的默认实现,ChannelInBoundHandlerAdapter和ChannelOutBoundHandlerAdapter他们分别实现了两大接口的所有功能,默认情况下会把读写事件传播到下一个handler。

事件的传播:
abstractChannel直接调用了pipeline的write()方法,因为write是个output事件,所以DefaultChannelPipeline直接找到tail部分的context调用其write()方法。

5.NioEventLoop

NioEventLoop除了处理Io事件还有主要:

  1. 非IO操作的系统Task
  2. 定时任务

非IO操作和IO操作各占默认值50%,底层使用Selector(多路复用器)

Selector BUG出现的原因:selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,cpu使用率100%
Netty的解决方法:

  • selectorselect操作周期进行统计,每完成一次空selector操作进行一次计数
  • 若咋某个周期内连接发生N次空轮询,则触发epoll死循环bug
  • 重建selector,判断是否是其他线程发起的重建请求,若不是则将原socketChannel从旧的selector上去除注册,重新注册到新的selector上,并将原来的selector关闭

6.Netty内存池和对象池

6.1.基本概念

内存池: 指为了实现内存池的功能,设计一个内存结构chunk,其内部管理者一个大块的连接内存区域,将这个内存区域切分均等的大小,每一个大小称之为一个page。将从内存池中申请内存的动作映射为从chunk中申请一定数量page。为了方便计算和申请page,chunk内部采用完全二叉树的方式对page进行管理。

对象池: 指recycler整个对象池的核心实现由ThreadLocal和stack及wrakOrderQueue构成,接着来看stack和wrakOrderQueue的具体实现,最后概括整体实现。

6.2.设计核心

  1. stack相当于是一个缓存,同一个线程内的使用和回收都将使用一个stack
  2. 每个线程都会有一个自己对应的stack,如果回收的线程不是stack的线程,将元素放入到Queue中
  3. 所有的Queue组合成一个链表,stack可以从这些链表中回收元素(实现了多线程之间共享回收的实例)

7.心跳和空闲检测

连接假死:

在某一端(服务端或者客户端)看来,底层的TCP连接已经断开了,但是应用程序并没有捕获到,因此会认为这条连接仍然是存在的,从TCP层面来说,只有收到四次握手数据包或者一个RST数据包,连接的状态才表示已断开。

导致的问题:

  1. 对于服务端每条连接都耗费CPU和内存资源,大量假死的连接会耗光服务器的资源
  2. 对于客户端,假死会造成发送数据超时,影响用户体验

链接假死的原因:

  1. 应用线程出现线程堵塞,无法进行数据的读写
  2. 客户端或服务端出现网络相关的故障
  3. 公网丢包

7.1.服务端空闲检测

如果能一直收到客户端发来的数据,则此条连接是活的,因此服务端对于连接假死的应对策略是空闲检测;

简化一下,服务端只需检测一段时间内是否收到客户端发来的数据即可,Netty自带的IdleStateHandler就实现了此功能。
IdleStateHandler构造器有四个参数:

  1. 表示读空闲时间:指的是在这段时间内没有数据读到,则连接假死
  2. 写空闲时间:指的是在这段时间内没有数据写到,则连接假死
  3. 读写空闲时间:如没有产生读或写,则连接假死
  4. 时间单位

7.2.客户端定时心跳

服务端在一段时间内没有收到客户端的数据有两种情况:连接假死;非假死的确没有数据;所以我们要排出第二种情况的假死,定期向服务端发送心跳。

实现了每隔五秒向服务端发送一个心跳数据包,这个时间段通常要比服务端的空间检测时间的一半要短一些,我们这直接定义为空闲检测时间的三分之一,主要是为了排除公网偶发的秒级抖动。

为了排除是否是因为服务端在非假死状态下确实没有发送数据,服务端也要定期发送心跳检测到客户端。

8.拆包粘包理论与解决

TCP是个“流协议”,所谓流就是灭幼界限的一串数据。TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实际情况进行包的拆分,所以在业务上认为一个完整的包可能被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包的问题。

解决方法:

  1. 封装自己的包协议:包=包内容(4byte)+包内容
  2. 对于粘包问题先读出包头即包体长度n,然后再读取长度为n的包内容,这样数据包之间的边界就清楚了
  3. 对于断包问题先读出包头即包体长度n,由于此次读取的缓冲区长度小于n,这时候就需要先缓存这部分的内容,等待下次read事件来时拼接起来形成完整的数据包。

8.1.Netty自带的拆包器

  1. 固定长度的拆包器FixedlengthFrameDecoder

    如果应用层协议非常简单,每个数据包的长度都是固定的,比如100,那么只需要把这个拆包器加到pipeline中,netty会把一个长度为100的数据包(ByteBuf)传递到下一个channelhandler.

  2. 分隔符拆包器DelimiterBasedFrameDecode

    从字面意思来看,发送断发送数据包的时候每个数据包之间以换行符作为分隔,接收端通过DelimiterBasedFrameDecode将粘过的ByteBuf拆分成一个完整的应用层数据包。

  3. 行拆包器LinebasedFrameDecode

    LinebasedFrameDecode是行拆包器的通用版本,只不过我们可以自定义分隔符

  4. 基于长度域拆包器lengthFieldbasedFrameDecoder

    这种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值