禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 基于Netty连接池泄露问题了解客户端启动源码
    • 来聊聊Netty使用不当导致的并发波动问题
    • 关于使用Netty业务处理器ChannelHanlder的一些注意事项
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析与调优策略
    • Linux下Netty实现高性能UDP服务
    • netty源码编译跑通简记
    • 基于Netty服务端快速了解核心组件
    • 用Netty快速落地一个客户端程序
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • 来聊聊Netty几个开箱即用的处理器框架
    • 聊聊Netty中几个重要的生命周期
    • Netty的几种IO模式的实现与切换
    • 聊聊Netty异常传播链与最佳实践
    • 从Netty的ByteBuf中学习高并发场景下的内存优化艺术
    • 聊聊Netty客户端断线重连的设计与实现
    • 基于Netty源码学习那些并发技巧
    • Netty连接可靠性Idle监测连环问
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解与编解码器最佳实践
    • Netty解码器源码解析
      • 写在文章开头
      • 解码器源码解析
        • 解码器父类ByteToMessageDecoder源码分析
        • FixedLengthFrameDecoder源码分析
        • LineBasedFrameDecoder源码分析
        • DelimiterBasedFrameDecoder源码分析
        • LengthFieldBasedFrameDecoder源码分析
      • 参考文献
    • Netty Reactor模型常见知识点小结
  • 消息队列

  • Nginx

  • 中间件
  • Netty
sharkchili
2025-02-18
目录

Netty解码器源码解析

# 写在文章开头

# 解码器源码解析

# 解码器父类ByteToMessageDecoder源码分析

ByteToMessageDecoder是所有解码器的父类,要想了解Netty解码器的整体流程,我们可以从它的channelRead方法入手,源码逻辑比较清晰,它实现了解码操作各个工作的核心工序,并将解码逻辑交由下层自类实现,这里笔者先大概介绍一下核心流程:

  1. 判断msg是否是ByteBuf,如果不是则直接调用fireChannelRead将msg传播,如果是则进入步骤2。
  2. 初始化一个解码器结果存放列表out 。
  3. 判断累加器是否为空,并将结果赋值给first。
  4. 如果为空,则初始化累加器。
  5. 如果非空,则将msg追加到累加器中。
  6. 执行callDecode完成数据包解码。
  7. 将解码结果传播到下文业务处理器中,并置空out列表。
@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //如果msg是ByteBuf则进入逻辑
        if (msg instanceof ByteBuf) {
        	//创建一个存放解码结果的列表
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                ByteBuf data = (ByteBuf) msg;
                //将累加器是否为空的布尔值赋值给first 
                first = cumulation == null;
                //如果first为null,说明本次是第一次累加,则直接将cumulation设置为data,反之将data累加到cumulation中
                if (first) {
                    cumulation = data;
                } else {                
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                //调用解码器进行解析
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Throwable t) {
                throw new DecoderException(t);
            } finally {
				//略
				//获取解码输出结果
                int size = out.size();
                decodeWasNull = !out.insertSinceRecycled();
                //将解析到的byteBuf向下传播
                fireChannelRead(ctx, out, size);
                //回收out列表,将每一个索引位置清空
                out.recycle();
            }
        } else {
        	//不是bytebuf的则直接向下传播
            ctx.fireChannelRead(msg);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

我们不妨对上述整体流程进行详尽分析,上文提到累加操作的代码:cumulator.cumulate(ctx.alloc(), cumulation, data);查看cumulator的定义我们可以得到这样一段代码:

private Cumulator cumulator = MERGE_CUMULATOR;
1

查看MERGE_CUMULATOR实现,其整体步骤比较清晰:

  1. 查看累加器空间是否足够写入in。
  2. 若不够则进行扩容,容量扩为当前累加器的容量加in的大小,进入步骤4。
  3. 反之buffer直接设置为累加器,进入步骤4。
  4. 将in写入buffer中。
  5. 释放in。
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
        @Override
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            ByteBuf buffer;
            //写的空间不足或者 cumulation.refCnt() > 1则对cumulation进行扩容
            if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
                    || cumulation.refCnt() > 1) {
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
            } else {
               //反之buffer就等于累加器
                buffer = cumulation;
            }
            // 将数据写入buffer
            buffer.writeBytes(in);
            // 释放in
            in.release();
            return buffer;
        }
    };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

再来看看callDecode方法,它是实际上完成解码操作的地方,可以看到ByteToMessageDecoder已经将大部分核心逻辑都实现了,唯独将decode设置为抽象方法,提供子类实现,整体核心逻辑也比较清晰:

  1. 判断传入的byteBuf是否可读,若可读则进入循环。
  2. 判断当前解码列表是否有数据,若有则说明前几次的循环已经完成了一次完整的数据包解码操作,直接调用fireChannelRead将数据和列表传到下一个解码器中,并将out列表清空。反之进入步骤3。
  3. 调用decode完成解码操作,该方法是抽象方法,不同的解码器有着不同的实现,总之每个抽象类完成解码操作之后,都会将解码的结果存到out列表中。
  4. 判断本次循环是否有解码操作,若还未完成则循环跳到步骤2,反之结束循环。
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
        	//如果ByteBuf 可读,则进入循环
            while (in.isReadable()) {
            	//获取解码列表的元素个数
                int outSize = out.size();
				//如果解码列表大于0,则说明之前循环执行过程中完成了byteBuf解码操作,则将解码列表传给下一个解码器,并将out列表清空
                if (outSize > 0) {
                    fireChannelRead(ctx, out, outSize);
                    out.clear();

                    
                    if (ctx.isRemoved()) {
                        break;
                    }
                    outSize = 0;
                }
				//获取in的可读字节数
                int oldInputLength = in.readableBytes();
                //调用子类的decode方法将in解析成需要的数据存到out列表中
                decode(ctx, in, out);

                
                if (ctx.isRemoved()) {
                    break;
                }
				//经过解码后的out列表还是和原来的长度一样则说明本次循环没有完成一次完整的解码操作,则进入if逻辑中
                if (outSize == out.size()) {
                	//如果oldInputLength和和in的可读长度一样,说明in的数据因为某些原因无法解码直接退出循环
                    if (oldInputLength == in.readableBytes()) {
                        break;
                    } else {
                    //走到这个分支说明本次循环有进行解码工作,但还是没有完全完成,跳到下一次循环进行
                        continue;
                    }
                }

                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                            ".decode() did not read anything but decoded a message.");
                }

                if (isSingleDecode()) {
                    break;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable cause) {
            throw new DecoderException(cause);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# FixedLengthFrameDecoder源码分析

由上文我们知道了ByteToMessageDecoder将decode设置为抽象方法提供子类实现,FixedLengthFrameDecoder就是其中一个子类,它是基于固定长度的解码器,这一点我们在上文的示例代码中就已经做了示范。

在正式阅读源码之前,我们不妨看看FixedLengthFrameDecoder源码中作者所给的注释,可以看到作者给了一个非常直观的代码示例,大意是说假如我们收到了4个分段的数据包,每个包的内容分别是:A、BC、DEFG、HI 。

假如我们使用FixedLengthFrameDecoder作为解码器,并将长度设置为3,那么最终业务处理器得到的数据包将是:ABC、DEF、GHI。

/**
 * A decoder that splits the received {@link ByteBuf}s by the fixed number
 * of bytes. For example, if you received the following four fragmented packets:
 * <pre>
 * +---+----+------+----+
 * | A | BC | DEFG | HI |
 * +---+----+------+----+
 * </pre>
 * A {@link FixedLengthFrameDecoder}{@code (3)} will decode them into the
 * following three packets with the fixed length:
 * <pre>
 * +-----+-----+-----+
 * | ABC | DEF | GHI |
 * +-----+-----+-----+
 * </pre>
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

基于这个示例我们回到源码查看它是如何做到这一点的,它对于decode的实现,显示调用自己实现的decode生成一个decoded ,如果不为空则存到out列表中。

@Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }
1
2
3
4
5
6
7

我们步入查看一下decode的逻辑,它会判断ByteBuf 可读长度是否小于固定长度,若小于固定长度则说明无法切割直接返回null,反之调用readRetainedSlice进行切割解码。

protected Object decode(
            @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
            //可读长度小于切割长度,直接返回null
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
        //调用readRetainedSlice进行拆包
            return in.readRetainedSlice(frameLength);
        }
    }
1
2
3
4
5
6
7
8
9
10

查看readRetainedSlice方法,它是AbstractByteBuf的提供的默认实现,逻辑也很简单,通过入参的length截取得到一个slice ,随后更新当前的byteBuf的写索引并将slice 返回。

@Override
    public ByteBuf readRetainedSlice(int length) {
    //基于length切割出一个完整的ByteBuf,记录当前读取的字节,然后直接返回
        ByteBuf slice = retainedSlice(readerIndex, length);
        readerIndex += length;
        return slice;
    }
1
2
3
4
5
6
7

# LineBasedFrameDecoder源码分析

LineBasedFrameDecoder是一个基于换行符实现的解码器,通过注释我们也可以知道,它进行拆包时会工具\n或者\r\n这两种换行符进行拆包的。

/**
 * A decoder that splits the received {@link ByteBuf}s on line endings.
 * <p>
 * Both {@code "\n"} and {@code "\r\n"} are handled.
 * For a more general delimiter-based decoder, see {@link DelimiterBasedFrameDecoder}.
 */
1
2
3
4
5
6

查看源码入口,可以看到逻辑和FixedLengthFrameDecoder差不多,这里就不多赘述了,直接查看decode的实现。

@Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }
1
2
3
4
5
6
7

decode的逻辑比较长,为了让读者更好的理解源码,笔者这里才用分段叙述的方式进行讲解,首先代码会查找换行符的位置,findEndOfLine查找换行符的方式比较特殊,考虑到换行符有\r\n或者\n,所有需要考虑下面3种情况:

  1. 查询数据中有没有\n,若存在进入步骤2,若没有直接返回-1。
  2. 如果\n前面是\r则返回\r的位置,反之进入步骤3。
  3. 反之返回\n的位置。
final int eol = findEndOfLine(buffer);
1

完成eol计算之后,我们继续查看后续逻辑,来说说第1种情况,这里先说明一下discarding是丢弃标识,默认情况下为false,当它为true时则说明本次读取的数据包大于可读取的最大长度,需要丢弃。 下面这段代码即说明非丢弃模式下且存在换行符,它进行了这样的操作:

  1. 判断读取的长度length 是否大于最大长度maxLength,若大于则说明该数据包超长,需要跳过,故更新读索引,将读索引值设置为换行符索引后面的位置,若不大于则进入步骤2。
  2. 来到步骤2这一步则说明存在换行符且长度合法,判断stripDelimiter是否为true,若为true则进入步骤3,若为false进入步骤4。
  3. 截取的数据不需要包含换行符,读取时跳过换行符,然后将数据返回。
  4. 截取的数据需要包含换行符,连着数据以及换行符一并截取并返回。

自此我们将情况1的流程都讲解完成了,对应的核心代码部分如下:

//非丢弃模式
if (!discarding) {
			//存在换行符
            if (eol >= 0) {
                final ByteBuf frame;
                //获取需要读取的长度
                final int length = eol - buffer.readerIndex();
                //获取换行符长度
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
				//如果长度大于最大长度,则调整读索引,设置到换行符后面,读取下一个数据包,并调用fail抛个异常,再返回null
                if (length > maxLength) {
                    buffer.readerIndex(eol + delimLength);
                    fail(ctx, length);
                    return null;
                }
				//走到这说明长度合法,判断读取是否需要包含换行符,若为true则需要跳过,false则包含
                if (stripDelimiter) {
                	//读取数据到frame
                    frame = buffer.readRetainedSlice(length);
                    //跳过换行符
                    buffer.skipBytes(delimLength);
                } else {
                	//读取数据以及换行符到frame中
                    frame = buffer.readRetainedSlice(length + delimLength);
                }

                return frame;
            } else {
               //略
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

情况2则是非丢弃模式下不存在换行符,它的执行步骤为:

  1. 获取可读长度length。
  2. 如果length大于maxLength,若不大于则说明当前数据还未达到规定数据包最大长度,直接返回null,等下一次收报新的数据流再查询一次,反之进入步骤3。
  3. 设置丢弃的长度为length,更新读索引位置到写索引,意为跳过这段字节数据。
  4. 将丢弃模式discarding 设置true。
  5. 判断failFast是否为true,若为true则抛出一个异常。
 if (!discarding) {
            if (eol >= 0) {
               //略
            } else {//非丢弃模式下不存在换行符
            
            //获取可读长度
                final int length = buffer.readableBytes();
                //如果大于最大长度,则设置丢弃长度为length
                if (length > maxLength) {
                    discardedBytes = length;
                    buffer.readerIndex(buffer.writerIndex());
                    discarding = true;
                    if (failFast) {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

因为上一个情况将discarding设置为了true,下一次收到数据流时我们来到了情况3,即丢弃模式且不存在换行符。它的执行步骤为,丢弃长度追加本次读取的可读的字节数,buffer直接跳过本地要读取的数据。

if (!discarding) {
            //略
        } else {
            if (eol >= 0) {
              //略
            } else {
            	//丢弃长度追加本地可读的长度
                discardedBytes += buffer.readableBytes();
                //更新读取位置到本次读取的数据包后面
                buffer.readerIndex(buffer.writerIndex());
            }
            return null;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13

基于上述场景,我们本次读取到了换行符,即在丢弃模式下读取到了换行符,于是来到的情况4,说明本次读取数据尽管存在换行符,但还是需要丢弃,于是代码就会经过这样的执行步骤:

  1. 计算跳过长度加读取长度的长度总和。
  2. 计算换行符长度。
  3. 将读索引更新到换行符后面,意味跳过本次超长数据的读取。
  4. 丢弃模式设置为false。
  5. 如果非快速失败,则抛出一个异常。
if (!discarding) {
           //略
        } else {
            if (eol >= 0) {
            	//计算跳过长度+读取长度的长度总和
                final int length = discardedBytes + eol - buffer.readerIndex();
                //计算换行符长度
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                //将读索引更新到换行符后面
                buffer.readerIndex(eol + delimLength);
                //重置丢弃长度和丢弃模式
                discardedBytes = 0;
                discarding = false;
                //如果非快速失败,则抛出一个异常
                if (!failFast) {
                    fail(ctx, length);
                }
            } else {
               //略
            }
            return null;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

最后我们做个小结完整的代码及注释如下:

protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
		//查找换行符位置
        final int eol = findEndOfLine(buffer);
        //非丢弃模式
        if (!discarding) {
        	//情况1:非丢弃模式且存在换行符
            if (eol >= 0) {
                final ByteBuf frame;
                //读取可读长度以及换行符长度
                final int length = eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                
				//如果要读取的数据大于最大长度,则更新读索引到换行符后面,并抛出一个异常
                if (length > maxLength) {
                    buffer.readerIndex(eol + delimLength);
                    fail(ctx, length);
                    return null;
                }
				//需要跳过换行符,则读取换行符之前的数据
                if (stripDelimiter) {
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                } else {
                	//读取包含换行符的数据
                    frame = buffer.readRetainedSlice(length + delimLength);
                }

                return frame;
            } else {
            	//情况2:非丢弃模式且不包含换行符
            	
            	//先获取读取长度
                final int length = buffer.readableBytes();
                
                //如果大于最大长度,则将模式切换为丢弃模式,丢弃长度为length
                if (length > maxLength) {
                    discardedBytes = length;
                    buffer.readerIndex(buffer.writerIndex());
                    discarding = true;
                    if (failFast) {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
        } else {
        	//情况3:丢弃模式下,存在换行符,则将换行符以及之前的数据全部跳过,完成后将丢弃模式设置为false,重置discardedBytes。
            if (eol >= 0) {
                final int length = discardedBytes + eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                buffer.readerIndex(eol + delimLength);
                discardedBytes = 0;
                discarding = false;
                if (!failFast) {
                    fail(ctx, length);
                }
            } else {
            	//情况4:丢弃模式下不存在换行符,追加丢弃数据长度,并将读索引更新到本次读取数据的末尾,放弃这段长数据的读取
                discardedBytes += buffer.readableBytes();
                buffer.readerIndex(buffer.writerIndex());
            }
            return null;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# DelimiterBasedFrameDecoder源码分析

基于特殊分隔符的解码器和行解码器差不多,只不过我们可以自定义设置进行分割的符号,入口代码套路一致,不多赘述。

 @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }

   
1
2
3
4
5
6
7
8
9

decode代码还是很长,我们还是采用分段的形式来讲解,首先代码会判断lineBasedDecoder 是否不为空,若不为空则用lineBasedDecoder 进行解码,这是为什么呢?

if (lineBasedDecoder != null) {
            return lineBasedDecoder.decode(ctx, buffer);
        }
1
2
3

我们在初始化解码器时,它会判断我们的分隔符,如果是换行符,则将lineBasedDecoder初始化,复用了行解码器。

public DelimiterBasedFrameDecoder(
            int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters) {
       //略
       //如果我们定义的符号是换行符则直接复用行解码器
        if (isLineBased(delimiters) && !isSubclass()) {
            lineBasedDecoder = new LineBasedFrameDecoder(maxFrameLength, stripDelimiter, failFast);
            this.delimiters = null;
        } else {
            //略
        }
        //略
    }
1
2
3
4
5
6
7
8
9
10
11
12

紧接着来到第2大步,遍历所有的分隔符,找到符合存在某个分隔符的最短长度bytebuf。

		int minFrameLength = Integer.MAX_VALUE;
        ByteBuf minDelim = null;
        //遍历分隔符
        for (ByteBuf delim: delimiters) {
        	//找到索引值最小的分隔符,即最小长度byteBuf
            int frameLength = indexOf(buffer, delim);
            //如果长度大于且	小于minFrameLength则更新minFrameLength为当前长度和minDelim 为当前分隔符
            if (frameLength >= 0 && frameLength < minFrameLength) {
                minFrameLength = frameLength;
                minDelim = delim;
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12

于是我们来到了情况1,如果数据中存在分隔符,则进入以下步骤:

  1. 先判断是否是丢弃模式,如果是丢弃模式,则说明之前读取数据流过程中遇到超长数据,本次尽管读取到分隔符,也是一个超长的存在分隔符的数据,需要丢弃,故重置丢弃模式为false,并跳过本次要读取的数据,直接返回null,反之进入步骤2。
  2. 如果读取的长度大于最大长度,则跳过并抛个异常,反之进入步骤3。
  3. 如果读取时需要跳过分隔符则仅读取分隔符之前的数据,反之连着分隔符一起读取。
  4. 返回读取结果。
		//分隔符不为空,存在可读数据
		if (minDelim != null) {
			//获取分隔符长度
            int minDelimLength = minDelim.capacity();
            ByteBuf frame;
            
			//如果当前为丢弃模式,即可说明之前的数据读取过程中存在超长数据,和本次包含分隔符的数据是一个整体,需要一并丢弃
            if (discardingTooLongFrame) {
                //重置丢弃模式为false,并将读索引更新到分隔符后面,跳过这些数据的读取
                discardingTooLongFrame = false;
                buffer.skipBytes(minFrameLength + minDelimLength);
				//重置tooLongFrameLength 
                int tooLongFrameLength = this.tooLongFrameLength;
                this.tooLongFrameLength = 0;
                //如果非快速失败则抛个异常
                if (!failFast) {
                    fail(tooLongFrameLength);
                }
                return null;
            }
			//如果需要读取的长度大于最大长度,则跳过并抛个异常
            if (minFrameLength > maxFrameLength) {
                // Discard read frame.
                buffer.skipBytes(minFrameLength + minDelimLength);
                fail(minFrameLength);
                return null;
            }
            
			//如果读取时需要跳过分隔符则读取分隔符之前的数据并跳过分隔符
            if (stripDelimiter) {
                frame = buffer.readRetainedSlice(minFrameLength);
                buffer.skipBytes(minDelimLength);
            } else {
            //如果需要包含分隔符则将数据和分隔符一起读取
                frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
            }

            return frame;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

了解了情况1,我们再来说说情况2,如果读取的数据不包含分隔符,则进入下面的步骤:

  1. 如果是非丢弃模式,先判断可读数据是否超过最大长度,若超过则开启丢弃模式,告知后续就算找到分隔符和本次读取的数据也是个整体也需要丢弃,然后跳过本次读取的数据。反之进入步骤2。
  2. 如果是丢弃模式,则直接跳过本次读取的数据,并累加丢弃数据的长度tooLongFrameLength 。
	if (minDelim != null) {
            //略
        } else {
        	//不包含分隔符
        	//非丢弃模式
            if (!discardingTooLongFrame) {
                if (buffer.readableBytes() > maxFrameLength) {
                   //设置tooLongFrameLength 为本次可读长度
                    tooLongFrameLength = buffer.readableBytes();
                    //跳过本次可读数据
                    buffer.skipBytes(buffer.readableBytes());
                    //开启丢弃模式
                    discardingTooLongFrame = true;
                    //如果是快速失败则抛个异常
                    if (failFast) {
                        fail(tooLongFrameLength);
                    }
                }
            } else {
                // 丢弃模式下,直接跳过本次读取的数据,并累加丢弃数据的长度
                tooLongFrameLength += buffer.readableBytes();
                buffer.skipBytes(buffer.readableBytes());
            }
            return null;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

自此我们把DelimiterBasedFrameDecoder也给讲完了,来小结一下执行过程:

  1. 判断分隔符是否是换行符,如果是则直接复用行解码器进行解码,反之进入步骤2。
  2. 尝试截取包含某个分隔符的ByteBuf。
  3. 如果存在这样的ByteBuf且是丢弃模式,则将当前这个ByteBuf丢弃,并将丢弃模式设置为false,并将长度累加tooLongFrameLength设置为0,进入下一次的读取,反之按要求截取包含或不包含分隔符的ByteBuf返回。
  4. 如果不存在符合要求的ByteBuf且非丢弃模式,先判断是否超过最大读取长度,若超过直接跳过本次读取并开启丢弃模式,返回null。
  5. 再次读取若还是没有遇到分隔符直接丢弃ByteBuf并累加tooLongFrameLength。

# LengthFieldBasedFrameDecoder源码分析

LengthFieldBasedFrameDecoder算是比较经典且常用的解码器了,它会按照我们提供的偏移量和长度找到描述长度的字段,并按照长度进行读取。

同样的decode源码也是比较长,我们还是以分段的方式来说明,默认情况下discardingTooLongFrame为false,我们这里假设为true,因为前几次获取数据流并解码过程中遇到了超长的数据,需要进行丢弃操作,所以它的执行步骤为:

  1. 如果数据过长,比较可读字节以及bytesToDiscard的最小值,获取实际需要丢弃的大小。
  2. 跳过要丢弃的字节。
  3. 更新bytesToDiscard 的值。
		//如果数据过长
 		if (discardingTooLongFrame) {
 			//获取当前可以跳过的最小值
            long bytesToDiscard = this.bytesToDiscard;
            int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
            //跳过需要丢弃的位置
            in.skipBytes(localBytesToDiscard);
            //bytesToDiscard设置为读取后的字节数
            bytesToDiscard -= localBytesToDiscard;
            this.bytesToDiscard = bytesToDiscard;
			//若需要则抛出个异常
            failIfNecessary(false);
        }
1
2
3
4
5
6
7
8
9
10
11
12
13

然后再判断是否还有可读字节并判断是否小于lengthFieldEndOffset(即描述长度的字段的偏移量),若小于则说明当前数据包可能还未接收完整,直接返回null。

if (in.readableBytes() < lengthFieldEndOffset) {
            return null;
}
1
2
3

走到这里说明可以尝试截取数据包了,它的执行步骤为:

  1. 精确定位到描述长度的字段的位置。
  2. 获取length存放的长度,如果小于0抛出异常,反之进入下一步。
  3. 判断数据包长度是否合法,如果数据包长度小于数据包起始位置到长度字段的位置,则说明当前数据包有问题,则跳过这几个字节,并抛出异常,反之进入步骤4。
  4. 将frameLength (表示数据字段的长度)加上lengthAdjustment (长度调整大小值)和lengthFieldEndOffset(长度字段的偏移量,可以直接理解为长度字段之前的长度)获取实际数据包的长度,如果数据包长度小于数据包起始位置到长度字段的位置,则说明当前数据包有问题,则跳过这几个字节,并抛出异常。
		//获取长度字段的实际索引位置
		int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
		//获取长度字段的值
        long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);
		//如果长度值小于0则跳过这几个字节,并抛出异常
        if (frameLength < 0) {
            in.skipBytes(lengthFieldEndOffset);
            throw new CorruptedFrameException(
                    "negative pre-adjustment length field: " + frameLength);
        }
		//将frameLength 加上lengthAdjustment 和lengthFieldEndOffset获取实际数据包的长度
        frameLength += lengthAdjustment + lengthFieldEndOffset;
		//如果frameLength 小于lengthFieldEndOffset则说明包不完整,直接抛异常
        if (frameLength < lengthFieldEndOffset) {
            in.skipBytes(lengthFieldEndOffset);
            throw new CorruptedFrameException(
                    "Adjusted frame length (" + frameLength + ") is less " +
                    "than lengthFieldEndOffset: " + lengthFieldEndOffset);
        }
		
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

得到正确的数据包长度frameLength 后,进行如下步骤:

  1. 判断长度是否超长,若超长进入步骤2。
  2. 计算frameLength 和可读字节的差discard 。
  3. 如果小于0,则说明超长的部分小于可读字节数,直接跳过即可,反之执行步骤4。
  4. 若大于0,则将丢弃模式设置为true,并将discard赋值给bytesToDiscard ,并跳过可读字节。
  5. 完成跳过步骤后,调用failIfNecessary,若有必要抛出一个异常。
  6. 最后判断frameLength是否小于可读字节,若是说明包还未收完整,直接返回null,下一次收到新数据再次读取并尝试。
		//数据包如果超过最大长度
	if (frameLength > maxFrameLength) {
			//计算超出的部分
            long discard = frameLength - in.readableBytes();
            //将frameLength设置为超长长度存到tooLongFrameLength 中
            tooLongFrameLength = frameLength;
			//若小于0说明frameLength 小于可读字节数,直接跳过即可
            if (discard < 0) {
                // buffer contains more bytes then the frameLength so we can discard all now
                in.skipBytes((int) frameLength);
            } else {
                // 若大于0,则将丢弃模式设置为true,并将discard设置给bytesToDiscard ,并跳过可读字节
                discardingTooLongFrame = true;
                bytesToDiscard = discard;
                in.skipBytes(in.readableBytes());
            }
            failIfNecessary(true);
            return null;
        }

        //如果可读部分小于frameLengthInt 说明包还未受完整,返回null
        int frameLengthInt = (int) frameLength;
        if (in.readableBytes() < frameLengthInt) {
            return null;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

接下来就是完整的拆包部分了,执行步骤为:

  1. 判断要跳过的字节数是否大于计算的数据包长度,若是则跳过并抛异常,反之进入步骤2。
  2. 从起始位置开始跳过initialBytesToStrip设置的字节数。
  3. 计算跳过后实际要截取的长度actualFrameLength 。
  4. 截取数据包得到一个frame 并返回。
//判断要跳过的字节数是否大于计算的数据包长度,若是则跳过并抛异常
if (initialBytesToStrip > frameLengthInt) {
            in.skipBytes(frameLengthInt);
            throw new CorruptedFrameException(
                    "Adjusted frame length (" + frameLength + ") is less " +
                    "than initialBytesToStrip: " + initialBytesToStrip);
        }
        //跳过initialBytesToStrip的字节数
        in.skipBytes(initialBytesToStrip);

        // 计算跳过后实际要截取的长度actualFrameLength 
        int readerIndex = in.readerIndex();
        int actualFrameLength = frameLengthInt - initialBytesToStrip;
        //截取数据包得到一个frame 并返回
        ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
        in.readerIndex(readerIndex + actualFrameLength);
        return frame;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

自此我们将LengthFieldBasedFrameDecoder也分析完成了,来小结一下整体步骤:

  1. 判断是否是丢弃模式,如果是且数据过长,则丢弃localBytesToDiscard个数据。
  2. 丢弃完成后若还是有数据可读,则尝试获取长度字段的位置,并判断当前数据长度是否足以保证读取到长度位置的字段,若不可以则抛异常,反之读取长度的值。
  3. 计算总长度frameLength是否小与数据包起始位置到长度字段位置的长度,如果小于则说明数据有问题直接抛异常,反之进入步骤4。
  4. 基于上述步骤计算的总长度frameLength来到步骤4则判断数据包是否超长,如果超长则丢弃,若小于可读字节数,说明数据包还没读完,等待下一次收到字节流再次从步骤1开始执行,反之按照设置的要求拆包并返回ByteBuf。

# 参考文献

跟闪电侠学 Netty:Netty 即时聊天实战与底层原理:https://book.douban.com/subject/35752082/ (opens new window) Netty学习(五)-DelimiterBasedFrameDecoder:https://blog.csdn.net/a953713428/article/details/68231119 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
Netty如何驾驭TCP流式传输?粘包拆包问题全解与编解码器最佳实践
Netty Reactor模型常见知识点小结

← Netty如何驾驭TCP流式传输?粘包拆包问题全解与编解码器最佳实践 Netty Reactor模型常见知识点小结→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×