22.Netty源码之解码器
highlight: arduino-light
抽象解码类
https://mp.weixin.qq.com/s/526p5f9fgtZu7yYq5j7LiQ
解码器
Netty 常用解码器类型:
- ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象;
- MessageToMessageDecoder 将一种消息类型解码为另外一种消息类型。
自定义一次解码器ByteToMessageDecoder解码器,如果读到的字节大小为4,那么认为读取到了1个完整的数据包。
java class VersionDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { //此处不需要while循环 if( in.readableBytes()>=4 ){ out.add(in.readInt()); } } }
自定义二次解码器,用于将String转换为Integer
java class StringToIntegerDecoder extends MessageToMessageDecoder<String> { @Override public void decode(ChannelHandlerContext ctx, String message,List<Object> out) throws Exception { out.add(message.length()); } }
此时使用一次解码器+二次解码器完成了Byte到String、String到Integer的转换。
为什么要粘包拆包
为什么要粘包
首先你得了解一下TCP/IP协议,在用户数据量非常小的情况下,极端情况下,一个字节,该TCP数据包的有效载荷非常低,传递100字节的数据,需要100次TCP传送,100次ACK,在应用及时性要求不高的情况下,将这100个有效数据拼接成一个数据包,那会缩短到一个TCP数据包,以及一个ack,有效载荷提高了,带宽也节省了。
非极端情况,有可能两个数据包拼接成一个数据包,也有可能一个半的数据包拼接成一个数据包,也有可能两个半的数据包拼接成一个数据包。
为什么要拆包
拆包和粘包是相对的,一端粘了包,另外一端就需要将粘过的包拆开。
举个栗子,发送端将三个数据包粘成两个TCP数据包发送到接收端,接收端就需要根据应用协议将三个数据包拆分成两个数据包
还有一种情况就是用户数据包超过了mss(最大报文长度),那么这个数据包在发送的时候必须拆分成几个数据包,接收端收到之后需要将这些数据包粘合起来之后,再拆开。
拆包的原理
在没有netty的情况下,用户如果自己需要拆包,基本原理就是不断从TCP缓冲区中读取数据,每次读取完都需要判断是否是一个完整的数据包
1.如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的数据包
2.如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,够成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。
netty 中的拆包也是如上这个原理,内部会有一个累加器,每次读取到数据都会不断累加,然后尝试对累加到的数据进行拆包,拆成一个完整的业务数据包,这个基类叫做 ByteToMessageDecoder,下面我们先详细分析下这个类
同样,我们先看下抽象解码类的继承关系图。
解码类是 ChanneInboundHandler 的抽象类实现,操作的是 Inbound 入站数据。
解码器实现的难度要远大于编码器,因为解码器需要考虑拆包/粘包问题。
由于接收方有可能没有接收到完整的消息,所以解码框架需要对入站的数据做缓冲操作,直至获取到完整的消息。

一次解码器ByteToMessageDecoder
ByteToMessageDecoder 中定义了两个累加器
2种累加器
Cumulator
每次将读取到的数据累加。
方式1:默认是内存复制的方式累加.如果内存不够先扩容。MERGE_CUMULATOR
方式2:组合的方式,避免内存复制。
MERGE_CUMULATOR
默认情况下,会使用 MERGE_CUMULATOR。
MERGE_CUMULATOR 的原理是每次都将读取到的数据通过内存拷贝的方式,拼接到一个大的字节容器中,这个字节容器在 ByteToMessageDecoder中叫做 cumulation。
下面我们看一下 MERGE_CUMULATOR 是如何将新读取到的数据累加到字节容器里的
java public static final Cumulator MERGE_CUMULATOR = new Cumulator() { @Override public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) { try { final ByteBuf buffer; if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes() || cumulation.refCnt() > 1 || cumulation.isReadOnly()) { //按需扩容 buffer = expandCumulation(alloc, cumulation, in.readableBytes()); } else { buffer = cumulation; } buffer.writeBytes(in); return buffer; } finally { in.release(); } } };
netty 中ByteBuf的抽象,使得累加非常简单。通过一个简单的api调用 buffer.writeBytes(in);
便将新数据累加到字节容器中,为了防止字节容器大小不够,在累加之前还进行了扩容处理
java static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) { ByteBuf oldCumulation = cumulation; cumulation = alloc.buffer(oldCumulation.readableBytes() + readable); cumulation.writeBytes(oldCumulation); oldCumulation.release(); return cumulation; }
扩容也是一个内存拷贝操作,新增的大小即是新读取数据的大小。
ByteToMessageDecoder:拆包原理
利用NIO进行网络编程时,往往需要将读取到的字节数或者字节缓冲区解码为业务可以使用的POJO对象。
Netty提供了ByteToMessageDecoder抽象工具解码类。
用户的解码器继承ByteToMessageDecoder,只需要实现decode()方法,即可完成ByteBuf到POJO对象的解码。 不过ByteToMessageDecoder没有考虑TCP粘包和组包等场景,读半包需要用户自己处理,因此我们可以继承更高级的解码器进行半包处理。
首先,我们看下ByteToMessageDecoder的子类FixedLengthFrameDecoder定义的方法:
```java public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter { /* channelRead方法是每次从TCP缓冲区读到数据都会调用的方法 触发点在AbstractNioByteChannel的read方法中 里面有个while循环不断读取,读取到一次就触发一次channelRead。
1.累加数据到字节容器cumulation。 2.将累加到的数据的字节容器传递给业务进行业务拆包 3.清理字节容器 4.传递业务数据包给业务解码器处理 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //if开始 判断类型是否匹配 if (msg instanceof ByteBuf) { CodecOutputList out = CodecOutputList.newInstance(); try { ByteBuf data = (ByteBuf) msg; //1.累加数据 //if:当前累加器没有数据,就直接跳过内存拷贝,直接将字节容器的指针指向新读取的数据。 //else:调用累加器累加数据至字节容器 first = cumulation == null; if (first) { //数据累加器 cumulation = data; } else { cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); } //调用decode方法 //2.将累加到的数据传递给业务进行拆包 //将尝试将字节容器的数据拆分成业务数据包塞到业务数据容器out中 callDecode(ctx, cumulation, out); } catch (DecoderException e) { throw e; } } catch (Exception e) { throw new DecoderException(e); } finally {
//何为可读:writerIndex > readerIndex //何为不可读:writerIndex <= readerIndex //不可读说明已经读完了! //如果累加器不等于空 也不可读 //那么执行清理逻辑 if (cumulation != null && !cumulation.isReadable()) { //3.清理字节容器 //业务拆包完成之后,只是从字节容器中取走了数据。 //但是这部分空间对于字节容器来说依然保留着。 //而字节容器每次累加字节数据的时候都是将字节数据追加到尾部 //如果不对字节容器做清理,那么时间一长就会OOM。 //正常情况下,其实每次读取完数据,netty都会在下面这个discardSomeReadBytes方法中 //将字节容器清理 //只不过,当发送端发送数据过快,channelReadComplete可能会很久才被调用一次 //如果一次数据读取完毕之后,可能接收端一边收,发送端一边发。 //这里的读取完毕指的是接收端在某个时间不再接受到数据为止。 //发现仍然没有拆到一个完整的用户数据包,即使该channel的设置为非自动读取 //也会触发一次读取操作 ctx.read(),该操作会重新向selector注册op_read事件 //以便于下一次能读到数据之后拼接成一个完整的数据包 //所以为了防止发送端发送数据过快,netty会在每次读取到一次数据 //业务拆包之后对字节字节容器做清理,清理部分的代码如下 numReads = 0; cumulation.release(); cumulation = null; } else if (++ numReads >= discardAfterReads) { //如果字节容器当前已无数据可读取,直接销毁字节容器 //并且标注一下当前字节容器一次数据也没读取 //如果连续16次,discardAfterReads的默认值为16 //字节容器中仍然有未被业务拆包器读取的数据, //那就做一次压缩,有效数据段整体移到容器首部 numReads = 0; discardSomeReadBytes(); } int size = out.size(); firedChannelRead |= out.insertSinceRecycled(); //4.传递业务数据包给业务解码器处理 //触发channelRead事件 将拆到的业务数据包都传递到后续的handler //这样就可以把一个个完整的业务数据包传递到后续的业务解码器进行解码,随后处理业务逻辑 fireChannelRead(ctx, out, size); out.recycle(); } //if开始对应的else判断类型是否匹配 } else { ctx.fireChannelRead(msg); } } } //frameLength=4,如果先发送2字节再发送2字节 //那么是否存在解码出现异常的情况? //答案:不会,因为有一个死循环 //比如发送方先发送了2字节的数据,然后发送方又发来了2字节 //首先原子累加器累加2字节传入callDecode方法的in,in是累加器cumulation //in.isReadable()判断可读,调用decode方法,decode方法会判断如果不够4字节 直接return跳出死循环 //然后发送方又发来2字节,然后继续累加到原子累加器 //判断可读调用decode方法。
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List
decode() 是用户必须实现的抽象方法,在该方法在调用时需要传入接收的数据 ByteBuf,及用来添加编码后消息的 List。
由于 TCP 粘包问题,ByteBuf 中可能包含多个有效的报文,或者不够一个完整的报文。
Netty 会重复回调 decode() 方法,直到没有解码出新的完整报文可以添加到 List 当中,或者 ByteBuf 没有更多可读取的数据为止。
如果此时 List 的内容不为空,那么会传递给 ChannelPipeline 中的下一个ChannelInboundHandler。触发channelRead方法。
此外 ByteToMessageDecoder 还定义了 decodeLast() 方法。
为什么抽象解码器要比编码器多一个 decodeLast() 方法呢?
因为 decodeLast 在 Channel 关闭后会被调用一次,主要用于处理 ByteBuf 最后剩余的字节数据。
Netty 中 decodeLast 的默认实现只是简单调用了 decode() 方法。如果有特殊的业务需求,则可以通过重写 decodeLast() 方法扩展自定义逻辑。
ByteToMessageDecoder 还有一个抽象子类是 ReplayingDecoder。
它封装了缓冲区的管理,在读取缓冲区数据时,你无须再对字节长度进行检查。因为如果没有足够长度的字节数据,ReplayingDecoder 将终止解码操作。
ReplayingDecoder 的性能相比直接使用 ByteToMessageDecoder 要慢,大部分情况下并不推荐使用 ReplayingDecoder。
二次解码器MessageToMessageDecoder
MessageToMessageDecoder实际上是Nety的二次解码器,从SocketChannel读取到的TCP数据报是ByteBuffer,先将解码为Java对象,再二次解码为POJO对象,因此称之为二次解码器。 以HTTP+XML协议栈为例,第一次解码是将字节数组解码成HttpRequest对象,然后对HttpRequest消息中的消息体字符串进行二次解码,将XML格式的字符串解码为POJO对象。 由于二次解码器是将一个POJO解码为另一个POJO,一般不涉及半包处理。
MessageToMessageDecoder 与 ByteToMessageDecoder 作用类似。
都是将一种消息类型的编码成另外一种消息类型。
与 ByteToMessageDecoder 不同的是 MessageToMessageDecoder 并不会对数据报文进行缓存,它主要用作转换消息模型。
比较推荐的做法是使用 ByteToMessageDecoder 解析 TCP 协议,解决拆包/粘包问题。解析得到有效的 ByteBuf 数据,然后传递给后续的 MessageToMessageDecoder 做数据对象的转换,具体流程如下图所示。

三种常用的解码器
FixedLengthFrameDecoder
DelimiterBasedFrameDecoder
LengthFieldBasedFrameDecoder
固定长度:FixedLengthFrameDecoder
public class FixedLengthFrameDecoder extends ByteToMessageDecoder { private final int frameLength; public FixedLengthFrameDecoder(int frameLength) { checkPositive(frameLength, "frameLength"); this.frameLength = frameLength; } @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); } } protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { //判断读取到的数据是否小于定义数据的固定长度 if (in.readableBytes() < frameLength) { //小于不处理 return null; } else { //否则只处理frameLength个长度的数据 return in.readRetainedSlice(frameLength); } } }
通信协议实战★
在之前通信协议设计中我们提到了协议的基本要素并给出了一个较为通用的协议示例。
下面我们通过 Netty 的编辑码框架实现该协议的解码器,加深我们对 Netty 编解码框架的理解。
其实dubbo和rocketMq都是这种方式。
在实现协议编码器之前,我们首先需要清楚一个问题:如何判断 ByteBuf 是否存在完整的报文?
最常用的做法就是通过读取消息长度 dataLength 进行判断。
如果 ByteBuf 的可读数据长度小于 dataLength,说明 ByteBuf 还不够获取一个完整的报文。
在该协议前面的消息头部分包含了魔数、协议版本号、数据长度等固定字段,共 14 个字节。
固定字段长度和数据长度可以作为我们判断消息完整性的依据,具体编码器实现逻辑示例如下:
java /* +---------------------------------------------------------------+ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | +---------------------------------------------------------------+ | 状态 1byte | 保留字段 4byte | 数据长度 4byte | +---------------------------------------------------------------+ | 数据内容 (长度不定) | +---------------------------------------------------------------+ */ @Override public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // 判断 ByteBuf 可读取字节 if (in.readableBytes() < 14) { return; } in.markReaderIndex(); // 标记 ByteBuf 读指针位置 in.skipBytes(2); // 跳过魔数 in.skipBytes(1); // 跳过协议版本号 byte serializeType = in.readByte(); in.skipBytes(1); // 跳过报文类型 in.skipBytes(1); // 跳过状态字段 in.skipBytes(4); // 跳过保留字段 int dataLength = in.readInt(); if (in.readableBytes() < dataLength) { in.resetReaderIndex(); // 重置 ByteBuf 读指针位置 return; } byte[] data = new byte[dataLength]; in.readBytes(data); SerializeService serializeService = getSerializeServiceByType(serializeType); Object obj = serializeService.deserialize(data); if (obj != null) { out.add(obj); } }
总结
Netty 提供了一组 ChannelHandler 实现的抽象类,在项目开发中基于这些抽象类实现自定义的编解码器具备较好的可扩展性,最后我们通过具体示例协议的实战加深了对编解码器的理解。
当然 Netty 在编解码方面所做的工作远不止于此。它还提供了丰富的开箱即用的编解码器,下节课我们便一起探索实用的编解码技巧。
相关文章:
22.Netty源码之解码器
highlight: arduino-light 抽象解码类 https://mp.weixin.qq.com/s/526p5f9fgtZu7yYq5j7LiQ 解码器 Netty 常用解码器类型: ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象;MessageToMessageDecoder 将一种消息类型解码为另外一种消息类…...
R语言【Tidyverse、Tidymodel】的机器学习方法
机器学习已经成为继理论、实验和数值计算之后的科研“第四范式”,是发现新规律,总结和分析实验结果的利器。机器学习涉及的理论和方法繁多,编程相当复杂,一直是阻碍机器学习大范围应用的主要困难之一,由此诞生了Python…...
vscode 第一个文件夹在上一层文件夹同行,怎么处理
我的是这样的 打开终端特别麻烦 解决方法就是 打开vscode里边的首选项 进入设置 把Compact Folders下边对勾给勾掉...
[JavaScript游戏开发] 绘制冰宫宝藏地图、人物鼠标点击移动、障碍检测
系列文章目录 第一章 2D二维地图绘制、人物移动、障碍检测 第二章 跟随人物二维动态地图绘制、自动寻径、小地图显示(人物红点显示) 第三章 绘制冰宫宝藏地图、人物鼠标点击移动、障碍检测 第四章 绘制Q版地图、键盘上下左右地图场景切换 文章目录 系列文章目录前言一、本章节…...
【NLP概念源和流】 01-稀疏文档表示(第 1/20 部分)
一、介绍 自然语言处理(NLP)是计算方法的应用,不仅可以从文本中提取信息,还可以在其上对不同的应用程序进行建模。所有基于语言的文本都有系统的结构或规则,通常被称为形态学,例如“跳跃”的过去时总是“跳跃”。对于人类来说,这种形态学的理解是显而易见的。 在这篇介…...
服务器运行python程序的使用说明
服务器的使用与说明 文章目录 服务器的使用与说明1.登录2.Python的使用2.1 服务器已安装python32.2 往自己的用户目录安装python31.首先下载安装包2.解压缩3.编译与安装 2.3 新建环境变量2.4 测试 3 创建PBS作业并提交 1.登录 windowsr打开运行命令窗口,在运行框中…...
8.2一日总结
1.记录更新: untracked: 未追踪(新增的文件) unmodefied: 未修改 modefied: 已修改 staged: 已暂存 2、添加指定文件到暂存区: git add 文件名 gi…...
JavaScript(四)DOM及CSS操作
1、DOM简介 DocumentType: Html的声明标签 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Docume…...
window中,关闭java占用端口的进程
查看端口被占用的情况 netstat -ano|findstr "端口号"使用Tasklist查看对于 PID 的进程名 tasklist|findstr "PID号"通过 taskkill 命令方式结束进程 taskkill /f /t /im Pid...
【Python】PySpark 数据计算 ⑤ ( RDD#sortBy方法 - 排序 RDD 中的元素 )
文章目录 一、RDD#sortBy 方法1、RDD#sortBy 语法简介2、RDD#sortBy 传入的函数参数分析 二、代码示例 - RDD#sortBy 示例1、需求分析2、代码示例3、执行结果 一、RDD#sortBy 方法 1、RDD#sortBy 语法简介 RDD#sortBy 方法 用于 按照 指定的 键 对 RDD 中的元素进行排序 , 该方…...
Elasticsearch官方测试数据导入
一、数据准备 百度网盘链接 链接:https://pan.baidu.com/s/1rPZBvH-J0367yQDg9qHiwQ?pwd7n5n 提取码:7n5n文档格式 {"index":{"_id":"1"}} {"account_number":1,"balance":39225,"firstnam…...
uniapp项目的pdf文件下载与打开查看
最近写的uniapp项目需要新增一个pdf下载和打开查看功能,摸索了半天终于写了出来,现分享出来供有需要的同行参考,欢迎指正 async function DownloadSignature() {//请求后端接口,返回值为一个url地址let resawait req.flow.flowDo…...
DeepVO 论文阅读
论文信息 题目:DeepVO Towards End-to-End Visual Odometry with Deep Recurrent Convolutional Neural Networks 作者:Sen Wang, Ronald Clark, Hongkai Wen and Niki Trigoni 代码地址:http://senwang.gitlab.io/DeepVO/ (原作者并没有开源…...
HOT71-字符串解码
leetcode原题链接: 字符串解码 题目描述 给定一个经过编码的字符串,返回它解码后的字符串。 编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。你可以认为输入字符串总是有效的;输…...
redis-server进程无法关闭终极解决方案
先使用命令查看6379端口情况: sudo lsof -i :6379 发现redis进程在占用,redis-server进程无论什么手段都杀不死,使用kill -9 pid杀掉pid后又卷土重来,最后找到了下面这个命令 sudo /etc/init.d/redis-server stop ok,…...
(5)将固件加载到没有ArduPilot固件的主板上
文章目录 前言 5.1 下载驱动程序和烧录工具 5.2 下载ArduPilot固件 5.3 使用测试版和开发版 5.3.1 测试版 5.3.2 最新开发版本 5.4 将固件上传到自动驾驶仪 5.5 替代方法 5.6 将固件加载到带有外部闪存的主板上 前言 ArduPilot 的最新版本(Copter-3.6, Pl…...
wpf画刷学习1
在这2篇博文有提到wpf画刷, https://blog.csdn.net/bcbobo21cn/article/details/109699703 https://blog.csdn.net/bcbobo21cn/article/details/107133703 下面单独学习一下画刷; wpf有五种画刷,也可以自定义画刷,画刷的基类都…...
Opencv C++实现yolov5部署onnx模型完成目标检测
代码分析: 头文件 #include <fstream> //文件 #include <sstream> //流 #include <iostream> #include <opencv2/dnn.hpp> //深度学习模块-仅提供推理功能 #include <opencv2/imgproc.hpp> //图像处理模块 #include &l…...
django bootstrap html实现左右布局,带折叠按钮,左侧可折叠隐藏
一、实现的效果 在django项目中,需要使用bootstrap 实现一个左右分布的布局,左侧区域可以折叠隐藏起来,使得右侧的显示区域变大。(为了区分区域,左右加了配色,不好看的修改颜色即可) 点击折叠按钮,左侧区域隐藏,右侧区域铺满: 二、实现思路 1、使用col-md属性,让左…...
Mapping温度分布验证选择数据记录仪时需要考虑的13件事
01 什么是温度分布验证? 温度分布验证是通过在规定的研究时间内测量定义区域内的多个点来确定特定温度控制环境或过程(如冷冻柜、冰箱、培养箱、稳定室、仓库或高压灭菌器)的温度分布的过程。温度分布验证的目标是确定每个测量点之间的差异&…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...
招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...
