闪电侠netty小册阅读总结

前言

Netty 入门与实战:仿写微信 IM 即时通讯系统阅读总结
Netty结构中英对照版

编程模型

1.传统IO编程

每个连接创建成功之后都需要一个线程来维护,每个线程包含一个 while 死循环,那么 1w 个连接对应 1w 个线程,继而 1w 个 while 死循环

1
2
3
4
5
* 线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起

* 线程切换效率低下:单机 CPU 核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降

* 数据读写是以字节流为单位

2.NIO编程模型

NIO 编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责

1
2
3
4
5
6
7
* NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,serverSelector负责轮询是否有新的连接,clientSelector负责轮询连接是否有数据可读

* 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等

* clientSelector被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过 clientSelector.select(1)方法可以轮询出来,进而批量处理

* 数据的读写面向 Buffer

Netty编程

1.服务端启动流程

1
2
3
4
5
6
7
8
9
10
11
* 创建一个引导类,然后给他指定线程模型,IO模型,连接读写处理逻辑,绑定端口之后,服务端就启动起来了

* 通过给 bind 方法添加监听器,用以实现自动绑定递增端口

* attr 方法,为每条连接增加属性,相当于给NioServerSocketChannel维护一个map

* handler()用于指定在服务端启动过程中的一些逻辑

* childOption 方法,用于指定处理新连接数据的读写处理逻辑。设置一些TCP底层相关的属性。

* 关于 TCP连接的优化,SO_KEEPALIVE 底层心跳,TCP_NODELAY (是否开启Nagle算法)延迟发送,SO_BACKLOG (临时存放已完成三次握手的请求的队列的最大长度)等待队列

2.客户端启动流程

1
2
3
4
5
6
7
* 与服务端启动类似。调用 connect 方法进行连接,返回Future,异步监听是否连接成功。可以增加

* 重试不在主线程,定时任务是调用 bootstrap.config().group().schedule()

* 重连优化:实现指数退避重连逻辑,位运算。

* option() 方法CONNECT_TIMEOUT_MILLIS 属性

3.客户端与服务端双向通信

1
2
3
4
5
6
7
* initChannel() 方法添加逻辑处理器

* 调用 ch.pipeline().addLast() 方法 添加一个逻辑处理器。其中ch.pipeline() 返回的是和这条连接相关的逻辑处理链,采用了责任链模式

* 建立成功之后,会调用channelActive()方法,读取数据用channelRead()方法

* 客户端与服务端交互的二进制数据载体为 ByteBuf,ByteBuf 通过连接的内存管理器创建,字节数据填充到 ByteBuf 之后才能写到对端

4.ByteBuf

1
2
3
* 基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法

* 多个 ByteBuf 可以引用同一段内存,这段内存可以是堆内也可以是堆外的。通过引用计数来控制内存的释放,遵循谁 retain() 谁 release() 的原则

5.客户端与服务端通信协议编解码。扩展阅读RPC 消息协议

1
2
3
4
5
6
7
自定义通信协议
* 4字节魔数校验
* 版本号,预留字段,用于协议升级
* 序列化算法,表示序列化方式
* 1字节指令,每一种指令都会有相应的处理逻辑
* 4字节数据长度,用于拆包粘包
* N字节数据内容

6.实现客户端与服务端收发消息

1
2
* channel 的 attr() 绑定属性来设置某些状态,获取某些状态,不需要额外的 map 来维持。
* hannel.attr(Attributes.LOGIN).set(true)绑定登陆标识

7.pipeline 与 channelHandler

1
2
* ChannelPipeline 是一个双向链表结构,他和 Channel 之间是一对一的关系。
* ChannelPipeline 里面每个节点都是一个 ChannelHandlerContext 对象,这个对象能够拿到和 Channel 相关的所有的上下文信息,然后这个对象包着一个重要的对象,那就是逻辑处理器ChannelHandler。

8.构建客户端与服务端 pipeline

1
2
3
* 基于 SimpleChannelInboundHandler,不再需要强转,不再有冗长乏味的 if else 逻辑,不需要手动传递对象。(对比ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter理解)

* 基于 MessageToByteEncoder,我们可以实现自定义编码,而不用关心 ByteBuf 的创建,内存释放。

9.拆包粘包理论与解决方案

1
2
3
4
5
6
7
8
* 1. 固定长度的拆包器 FixedLengthFrameDecoder
2. 行拆包器 LineBasedFrameDecoder
3. 分隔符拆包器 DelimiterBasedFrameDecoder
4. 基于长度域拆包器 LengthFieldBasedFrameDecoder

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

* 扩展LengthFieldBasedFrameDecoder 通过魔数校验尽早屏蔽非本协议的客户端

10.channelHandler 的生命周期

1
2
3
4
5
6
7
* ChannelHandler 启动时回调方法的执行顺序为:handlerAdded() -> channelRegistered() -> channelActive() -> channelRead() -> channelReadComplete()

* ChannelHandler 关闭时回调方法的执行顺序为:channelInactive() -> channelUnregistered() -> handlerRemoved()

* channelActive() 与 channelInActive()统计单机的增减连接数

* channelReadComplete()调用ctx.channel().flush() 批量刷新提升性能

11.使用 channelHandler 的热插拔实现客户端身份校验

1
2
3
4
5
* 如果有很多业务逻辑的 handler 都要进行某些相同的操作,我们完全可以抽取出一个 handler 来单独处理

* ctx.pipeline().remove(this)移除自身。通过 ChannelHandler 的热插拔机制来实现动态删除逻辑

* handlerRemoved()通知移除事件

12.客户端互聊原理与实现

1
2
3
4
5
6
7
* channel.attr(Attributes.SESSION).set(session) 绑定session

* channel.attr(Attributes.SESSION).set(null) 删除 session

* channel.attr(Attributes.SESSION).get() 拿到 session

* 通过Map实例化userId -> channel 的映射

13.群聊的发起与通知

1
2
* ChannelGroup channelGroup = new DefaultChannelGroup(ctx.executor());
* ChannelGroup:它可以把多个 chanel 的操作聚合在一起,可以往它里面添加删除 channel,可以进行 channel 的批量读写,关闭等操作

性能优化篇

1.Netty性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
* 共享 handler
1.无状态handler直接单例模式,提高效率,也避免了创建很多小的对象。
2.加上注解标识@ChannelHandler.Sharable,表明该 handler 是可以多个 channel 共享的

* 压缩 handler - 合并编解码器——>MessageToMessageCodec

* 缩短事件传播路径
1.压缩 handler - 合并平行 handler。定义一个通用handler,依然是可以写成一个单例模式的类。内部维护 map,存放指令到各个指令处理器的映射,回调时通过指令找到具体的 handler
2.更改事件传播源。ctx.writeAndFlush()对比ctx.channel().writeAndFlush()

* 减少阻塞主线程的操作
1.对于数据库或者网络等一些耗时操作,丢到业务线程池处理
2.计算耗时,使用回调 Future

2.心跳与空闲检测

1
2
3
* 空闲检测 IdleStateHandler ,连接假死之后会回调 channelIdle() 方法
* 定时心跳 ctx.executor().scheduleAtFixedRate
* 通常空闲检测时间要比发送心跳的时间的两倍要长一些,为了排除偶发的公网抖动,防止误判。

------本文结束感谢阅读------
显示评论
0%