FLV文件主要由两部分组成:Header和Body。

1. Header

header部分记录了flv的类型、版本等信息,是flv的开头,一般都差不多,占9bytes。具体格式如下:

文件类型 3 bytes

“FLV”

版本 1 byte

一般为0x0000 0001 (1)

流信息 1 byte

倒数第一位是1表示有视频,倒数第三位是1表示有音频.

其余备用字段必须为0, 音视频都具备为0x0000 0101 (5)

header长度 4 bytes

整个header的长度,一般为9; 大于9表示下面还有扩展信息 (9)

2. Body

body部分由一个个Tag组成,每个Tag的下面有一块4bytes的空间,用来记录这个tag的长度,这个后置用于逆向读取处理。

2.1.Tag

每个Tag由也是由两部分组成的:Tag Header和Tag Data。Tag Header里存放的是当前Tag的类型、数据区(Tag Data)长度等信息,具体如下:

名称 长度

介绍

Tag类型 1 bytes 8:音频
9:视频
18:脚本 (这里是一些描述信息)
其他:保留
数据区长度 3 bytes 数据区(Tag Data)的长度
时间戳 3 bytes

整数,单位是毫秒。对于脚本型的tag总是0.

相对于FLV文件的第一个TAG时戳。第一个tag的时戳总是0。

——不是时戳增量,rtmp中是时戳增量。

时间戳扩展 1 bytes 将时间戳扩展为4bytes,代表高8位。很少用到
StreamsID 3 bytes 总是0

2.2.Tag Data

数据区根据Tag类型的不同可分为三种,音频数据、视频数据和脚本数据。

2.2.1.音频数据

第一个byte是音频的信息,格式如下:

名称 长度 介绍
音频格式 4 bits 0 = Linear PCM, platform endian
1 = ADPCM
2 = MP3
3 = Linear PCM, little endian
4 = Nellymoser 16-kHz mono
5 = Nellymoser 8-kHz mono
6 = Nellymoser
7 = G.711 A-law logarithmic PCM
8 = G.711 mu-law logarithmic PCM
9 = reserved
10 = AAC
11 = Speex
14 = MP3 8-Khz
15 = Device-specific sound
采样率 2 bits 0 = 5.5-kHz
1 = 11-kHz
2 = 22-kHz
3 = 44-kHz
对于AAC总是3
采样的长度 1 bit 0 = snd8Bit
1 = snd16Bit
压缩过的音频都是16bit
音频类型 1 bit 0 = sndMono
1 = sndStereo
对于AAC总是1

第2byte开始就是音频流数据了。

2.2.2.视频数据

和音频数据一样,第一个byte是视频信息,格式如下:

名称 长度 介绍
帧类型 4 bits 1: keyframe (for AVC, a seekable frame)
2: inter frame (for AVC, a non-seekable frame)
3: disposable inter frame (H.263 only)
4: generated keyframe (reserved for server use only)
5: video info/command frame
编码ID 4 bits 1: JPEG (currently unused)
2: Sorenson H.263
3: Screen video
4: On2 VP6
5: On2 VP6 with alpha channel
6: Screen video version 2
7: AVC

2.2.3脚本数据

脚本Tag一般只有一个,是flv的第一个Tag,用于存放flv的信息,比如duration、audiodatarate、creator、width等等信息。

首先介绍下脚本的数据类型。所有数据都是以数据类型+(数据长度)+数据的格式出现的,数据类型占1byte,数据长度看数据类型是否存在,后面才是数据。

可参考AMF数据格式。
其中数据类型的种类有:

  • 0 = Number type
  • 1 = Boolean type
  • 2 = String type
  • 3 = Object type
  • 4 = MovieClip type
  • 5 = Null type
  • 6 = Undefined type
  • 7 = Reference type
  • 8 = ECMA array type
  • 10 = Strict array type
  • 11 = Date type
  • 12 = Long string type

如果类型为String,后面的2bytes为字符串的长度(Long String是4bytes),再后面才是字符串数据;如果是Number类型,后面的8bytes为Double类型的数据;Boolean类型,后面1byte为Bool类型。

知道了这些后再来看看flv中的脚本,一般开头是0x02,表示String类型,后面的2bytes为字符串长度,一般是0x000a(“onMetaData”的长度),再后面就是字符串“onMetaData”。好像flv格式的文件都有onMetaData标记,在运行ActionScript的时候会用到它。后面跟的是0x08,表示ECMA Array类型,这个和Map比较相似,一个键跟着一个值。键都是String类型的,所以开头的0x02被省略了,直接跟着的是字符串的长度,然后是字符串,再是值的类型,也就是上面介绍的那些了。

文件内存显示:

3. 源码解析

flv的格式还是比较简单的,header部分很简洁,body部分都是由一个个tag,tag的话也就三种,脚本tag一般只有一个的,我想这也是flv能成为在线视频格式的原因吧。

只要了解了格式,我们就可以写个程序来解析flv文件了,下面我们看源码解析:

FLVFile.h

#pragma once
#include "VideoFile.h" /* PACKET_TYPE_... 0x00 */
#define PACKET_TYPE_CHUNK_SIZE 0x01
/* PACKET_TYPE_... 0x02 */
#define PACKET_TYPE_BYTES_READ_REPORT 0x03
#define PACKET_TYPE_CONTROL 0x04
#define PACKET_TYPE_SERVER_BW 0x05
#define PACKET_TYPE_CLIENT_BW 0x06
/* PACKET_TYPE_... 0x07 */
#define PACKET_TYPE_AUDIO 0x08
#define PACKET_TYPE_VIDEO 0x09
/* PACKET_TYPE_... 0x0A */
/* PACKET_TYPE_... 0x0B */
/* PACKET_TYPE_... 0x0C */
/* PACKET_TYPE_... 0x0D */
/* PACKET_TYPE_... 0x0E */
#define PACKET_TYPE_FLEX_STREAM_SEND 0x0F
#define PACKET_TYPE_FLEX_SHARED_OBJECT 0x10
#define PACKET_TYPE_FLEX_MESSAGE 0x11
#define PACKET_TYPE_SCRIPT 0x12
#define PACKET_TYPE_SHARED_OBJECT 0x13
#define PACKET_TYPE_INVOKE 0x14
/* PACKET_TYPE_... 0x15 */
#define PACKET_TYPE_FLASH_VIDEO 0x16 /************************************************************************************************************
header部分记录了flv的类型、版本等信息,是flv的开头,一般都差不多,占9bytes. 具体格式如下;
文件类型    3 bytes "FLV";
版本 1 byte 一般为0x01;
流信息 1 byte 倒数第一位是1表示有视频,倒数第三位是1表示有音频,倒数第二、四位必须为0;
header长度 4 bytes 整个header的长度,一般为9;大于9表示下面还有扩展信息;
************************************************************************************************************/
struct FlvHeader
{
byte Type[];
byte Version;
byte StreamInfo;
byte HeaderSize[];
}; /************************************************************************************************************
每个Tag由也是由两部分组成的:Tag Header和Tag Data;
Tag Header里存放的是当前Tag的类型、数据区(Tag Data)长度等信息;
具体如下; 名称 长度 介绍;
Tag类型 1 bytes 8:音频 \ 9:视频 \ 18:脚本 \ 其他:保留;
数据区长度 3 bytes 在数据区的长度;
时间戳 3 bytes 整数,单位是毫秒 对于脚本型的tag总是0;
时间戳扩展 1 bytes 将时间戳扩展为4bytes,代表高8位 很少用到;
StreamsID 3 bytes 总是0;
************************************************************************************************************/
struct TagHeader
{
byte TagType;
byte DataSize[];
byte Timestamp[];
byte TimeExtend;
byte StreamsID[];
}; class CFLVFile: public CVideoFile
{
public:
CFLVFile();
virtual ~CFLVFile(); virtual bool ParseFile(); protected:
bool ParseFLVHeader();
bool ParseFLVBody(); bool ParseAudioData(int iDataSize);
bool ParseVideoData(int iDataSize);
bool ParseScriptData(int iDataSize);
};

FLVFile.cpp

#include "FLVFile.h"
#include "amf.h" CFLVFile::CFLVFile()
{
} CFLVFile::~CFLVFile()
{
} bool CFLVFile::ParseFile()
{
if (!ParseFLVHeader())
{
return false;
} if (!ParseFLVBody())
{
return false;
} return true;
} bool CFLVFile::ParseFLVHeader()
{
printf("******************************FLV Header******************************\n");
// FLV Header;
{
FlvHeader flvHeader;
int iSize = sizeof(FlvHeader);
if (iSize != fread(&flvHeader, , iSize, m_pFile))
{
printf("Read FLV Header is Error. \n");
return false;
} printf("\t File Type \t : %c %c %c \n", flvHeader.Type[], flvHeader.Type[], flvHeader.Type[]);
printf("\t Version \t : %d \n", flvHeader.Version);
printf("\t Stream Info \t : %d \n", flvHeader.StreamInfo);
printf("\t Header Length \t : %d \n\n", ByteToInt(flvHeader.HeaderSize, sizeof(flvHeader.HeaderSize)));
} return true;
} bool CFLVFile::ParseFLVBody()
{
printf("******************************FLV Body******************************\n"); bool bStop = false;
while (!bStop)
{
printf("\n ************************Tag Header************************ \n"); _getw(m_pFile); TagHeader tagHeader;
int iTagHeaderSize = sizeof(TagHeader);
if (iTagHeaderSize != fread(&tagHeader, , iTagHeaderSize, m_pFile))
{
// 读完了;
printf("\t Read File Finished. \n");
bStop = true;
} printf("\t Tag Type \t : %d \n", tagHeader.TagType);
printf("\t DataSize \t : %d \n", ByteToInt(tagHeader.DataSize, sizeof(tagHeader.DataSize)));
printf("\t Timestamp \t : %d \n", ByteToInt(tagHeader.Timestamp, sizeof(tagHeader.Timestamp)));
printf("\t TimeExtend \t : %d \n", tagHeader.TimeExtend);
printf("\t StreamsID \t : %d \n", ByteToInt(tagHeader.StreamsID, sizeof(tagHeader.StreamsID))); printf(" ************************Tag Body************************ \n");
const int iDataSize = ByteToInt(tagHeader.DataSize, sizeof(tagHeader.DataSize)); switch (tagHeader.TagType)
{
case PACKET_TYPE_AUDIO:
{
// 音频数据;
ParseAudioData(iDataSize);
}
break;
case PACKET_TYPE_VIDEO:
{
// 视频数据;
ParseVideoData(iDataSize);
}
break;
case PACKET_TYPE_SCRIPT:
{
// 类型数据;
ParseScriptData(iDataSize);
}
break;
default:
{
printf("\t Read Tag Body %d bytes. \n\n", iDataSize);
fseek(m_pFile, iDataSize, SEEK_CUR);
}
break;
}
} return true;
} /************************************************************************************************************
第一个字节是音频信息格式,格式如下; 名称 长度 介绍; 音频格式 4 bits
0 = Linear PCM, platform endian
1 = ADPCM
2 = MP3
3 = Linear PCM, little endian
4 = Nellymoser 16-kHz mono
5 = Nellymoser 8-kHz mono
6 = Nellymoser
7 = G.711 A-law logarithmic PCM
8 = G.711 mu-law logarithmic PCM
9 = reserved
10 = AAC
11 = Speex
14 = MP3 8-Khz
15 = Device-specific sound 采样率 2 bits (对于AAC总是3)
0 = 5.5-kHz
1 = 11-kHz
2 = 22-kHz
3 = 44-kHz 采样的长度 1 bit (压缩过的音频总是16bit)
0 = snd8Bit
1 = snd16Bit 音频类型 1 bit (对于AAC总是1)
0 = sndMono
1 = sndStereo 第二个字节开始就是音频流数据了;
************************************************************************************************************/
bool CFLVFile::ParseAudioData(int iDataSize)
{
// 解析音频格式;
byte audioHeader;
fread(&audioHeader, , sizeof(audioHeader), m_pFile); char strAudioInfo[] = { };
{
byte audioFormat = audioHeader >> ;
switch (audioFormat)
{
case :strcat(strAudioInfo, "Linear PCM, platform endian"); break;
case :strcat(strAudioInfo, "ADPCM"); break;
case :strcat(strAudioInfo, "MP3"); break;
case :strcat(strAudioInfo, "Linear PCM, little endian"); break;
case :strcat(strAudioInfo, "Nellymoser 16-kHz mono"); break;
case :strcat(strAudioInfo, "Nellymoser 8-kHz mono"); break;
case :strcat(strAudioInfo, "Nellymoser"); break;
case :strcat(strAudioInfo, "G.711 A-law logarithmic PCM"); break;
case :strcat(strAudioInfo, "G.711 mu-law logarithmic PCM"); break;
case :strcat(strAudioInfo, "reserved"); break;
case :strcat(strAudioInfo, "AAC"); break;
case :strcat(strAudioInfo, "Speex"); break;
case :strcat(strAudioInfo, "MP3 8-Khz"); break;
case :strcat(strAudioInfo, "Device-specific sound"); break;
default:strcat(strAudioInfo, "UNKNOWN"); break;
}
strcat(strAudioInfo, "| ");
} {
byte sampBits = audioHeader << ;
sampBits = sampBits >> ;
switch (sampBits)
{
case :strcat(strAudioInfo, "5.5-kHz"); break;
case :strcat(strAudioInfo, "1-kHz"); break;
case :strcat(strAudioInfo, "22-kHz"); break;
case :strcat(strAudioInfo, "44-kHz"); break;
default:strcat(strAudioInfo, "UNKNOWN"); break;
}
strcat(strAudioInfo, "| ");
} {
byte sampLen = audioHeader << ;
sampLen = sampLen >> ;
switch (sampLen)
{
case :strcat(strAudioInfo, "8Bit"); break;
case :strcat(strAudioInfo, "16Bit"); break;
default:strcat(strAudioInfo, "UNKNOWN"); break;
}
strcat(strAudioInfo, "| ");
} {
byte audioType = audioHeader << ;
audioType = audioType >> ;
switch (audioType)
{
case :strcat(strAudioInfo, "Mono"); break;
case :strcat(strAudioInfo, "Stereo"); break;
default:strcat(strAudioInfo, "UNKNOWN"); break;
}
strcat(strAudioInfo, "| ");
} printf("\t %s audio data: %d bytes \n", strAudioInfo, iDataSize - );
fseek(m_pFile, iDataSize - , SEEK_CUR); return true;
} /************************************************************************************************************
第一个字节是视频信息格式,格式如下; 名称 长度 介绍; 帧类型 4 bits
1: keyframe(for AVC, a seekable frame)
2 : inter frame(for AVC, a non - seekable frame)
3 : disposable inter frame(H.263 only)
4 : generated keyframe(reserved for server use only)
5 : video info / command frame 编码ID 4 bits
1 : JPEG(currently unused)
2 : Sorenson H.263
3 : Screen video
4 : On2 VP6
5 : On2 VP6 with alpha channel
6 : Screen video version 2
7 : AVC 第二个字节开始就是视频流数据了;
************************************************************************************************************/
bool CFLVFile::ParseVideoData(int iDataSize)
{
// 解析音频格式;
byte videoHeader;
fread(&videoHeader, , sizeof(videoHeader), m_pFile); char strVideoInfo[] = { };
{
byte frameType = videoHeader >> ;
switch (frameType)
{
case :strcat(strVideoInfo, "key frame "); break;
case :strcat(strVideoInfo, "inter frame"); break;
case :strcat(strVideoInfo, "disposable inter frame"); break;
case :strcat(strVideoInfo, "generated keyframe"); break;
case :strcat(strVideoInfo, "video info/command frame"); break;
default:strcat(strVideoInfo, "UNKNOWN"); break;
}
strcat(strVideoInfo, "| ");
} {
byte sampBits = videoHeader << ;
sampBits = sampBits >> ;
switch (sampBits)
{
case :strcat(strVideoInfo, "JPEG (currently unused)"); break;
case :strcat(strVideoInfo, "Sorenson H.263"); break;
case :strcat(strVideoInfo, "Screen video"); break;
case :strcat(strVideoInfo, "On2 VP6"); break;
case :strcat(strVideoInfo, "On2 VP6 with alpha channel"); break;
case :strcat(strVideoInfo, "Screen video version 2"); break;
case :strcat(strVideoInfo, "AVC"); break;
default:strcat(strVideoInfo, "UNKNOWN"); break;
}
strcat(strVideoInfo, "| ");
} printf("\t %s audio data: %d bytes \n", strVideoInfo, iDataSize - );
fseek(m_pFile, iDataSize - , SEEK_CUR); return true;
} /************************************************************************************************************
脚本Tag一般只有一个,是flv的第一个Tag;
用于存放flv的信息,比如duration、audiodatarate、creator、width等; 首先介绍下脚本的数据类型;
所有数据都是以数据类型+(数据长度)+数据的格式出现的,数据类型占1byte,数据长度看数据类型是否存在,后面才是数据; AMF数据格式解析;
************************************************************************************************************/
bool CFLVFile::ParseScriptData(int iDataSize)
{
// 解析类型信息;
byte* pData = new byte[iDataSize];
byte* pDataEnd = pData + iDataSize;
fread(pData, , iDataSize, m_pFile); while (pDataEnd - pData > )
{
switch (pData[])
{
case AMF_NUMBER: // 数字(double);
{
//double dVal = AMF_DecodeNumber(pFileFlv);
}
break;
case AMF_BOOLEAN: // 布尔;
{ }
break;
case AMF_STRING: // 字符串;
{
pData = pData + ; AVal valName;
AMF_DecodeString((char*)pData, &valName);
pData = pData + (valName.av_len + );
}
break;
case AMF_OBJECT: // 对象;
{ }
break;
case AMF_MOVIECLIP: // 保留,未使用;
break;
// AMF_NULL, // null;
// AMF_UNDEFINED, // 未定义;
// AMF_REFERENCE, // 引用;
case AMF_ECMA_ARRAY: // 数组;
{
pData = pData + ; // 数组元素个数;
int iArrLen = ByteToInt(pData, );
pData = pData + ; AMFObject obj;
int iSize = pDataEnd - pData;
int nRes = AMF_DecodeArray(&obj, (char*)pData, iSize, iArrLen, TRUE);
if (nRes == -)
{
return false;
} for (int i = ; i < iArrLen; i++)
{
std::string strName(obj.o_props[i].p_name.av_val, obj.o_props[i].p_name.av_len);
switch (obj.o_props[i].p_type)
{
case AMF_NUMBER: // 数字(double);
{
printf("\t %s : %.0f\n", strName.c_str(), obj.o_props[i].p_vu.p_number);
}
break;
case AMF_STRING: // 字符串;
{
std::string strValue(obj.o_props[i].p_vu.p_aval.av_val, obj.o_props[i].p_vu.p_aval.av_len);
printf("\t %s : %s\n", strName.c_str(), strValue.c_str());
}
break;
default:
break;
}
} pData = pData + iSize;
}
break;
// AMF_OBJECT_END, // 对象结束(0x09);
// AMF_STRICT_ARRAY, // 严格的数组;
// AMF_DATE, // 日期;
// AMF_LONG_STRING, // 长字符串;
// AMF_UNSUPPORTED, // 未支持;
// AMF_RECORDSET, // 保留,未使用;
// AMF_XML_DOC, // xml文档;
// AMF_TYPED_OBJECT, // 有类型的对象;
// AMF_AVMPLUS, // 需要扩展到AMF3;
// AMF_INVALID = 0xff // 无效的;
default:
break;
}
} return true;
}

main.cpp

#include <windows.h>
#include "FLVFile.h"
#include <assert.h> // FLV 文件解析;
bool FLVParse_Test()
{
CFLVFile flvFile;
return flvFile.LoadFile("../testfile/flv_test.flv");
} void main()
{
// FLV;
bool bRes = FLVParse_Test();
assert(bRes);
}

运行结果:

项目源码下载地址:https://github.com/kingsunc/AVFileParse

FLV文件格式分析(附源码)的更多相关文章

  1. leaflet 结合 d3.js 实现 geojson 数据地形剖面分析(附源码下载)

    前言 leaflet 入门开发系列环境知识点了解: leaflet api文档介绍,详细介绍 leaflet 每个类的函数以及属性等等 leaflet 在线例子 leaflet 插件,leaflet ...

  2. Cesium专栏-空间分析之剖面分析(附源码下载)

    Cesium Cesium 是一款面向三维地球和地图的,世界级的JavaScript开源产品.它提供了基于JavaScript语言的开发包,方便用户快速搭建一款零插件的虚拟地球Web应用,并在性能,精 ...

  3. Cesium-空间分析之通视分析(附源码下载)

    Cesium Cesium 是一款面向三维地球和地图的,世界级的JavaScript开源产品.它提供了基于JavaScript语言的开发包,方便用户快速搭建一款零插件的虚拟地球Web应用,并在性能,精 ...

  4. Code First Entity Framework 6化被动为主动之explicit loading模式实战分析( 附源码)

    在使用Entity Framework加载关联实体时,可以有三种方式: 1.懒加载(lazy Loading); 2.贪婪加载(eager loading); 3.显示加载(explicit load ...

  5. [C#]委托实例分析(附源码)

    一直都听说C#中的委托与事件非常重要,都没有什么切身的体会,而这次通过做一个WinForm二次开发的项目才真正感觉到了委托与事件的犀利之处. 1.C#中的事件和委托的作用? 事件代表一个组件能够被关注 ...

  6. Cesium专栏-填挖方分析(附源码下载)

    Cesium 是一款面向三维地球和地图的,世界级的JavaScript开源产品.它提供了基于JavaScript语言的开发包,方便用户快速搭建一款零插件的虚拟地球Web应用,并在性能,精度,渲染质量以 ...

  7. Vue路由实现之通过URL中的hash(#号)来实现不同页面之间的切换(图表展示、案例分析、附源码详解)

    前言 本篇随笔主要写了Vue框架中路由的基本概念.路由对象属性.vue-router插件的基本使用效果展示.案例分析.原理图解.附源码地址获取. 作为自己对Vue路由进行页面跳转效果知识的总结与笔记. ...

  8. C#编程总结(七)数据加密——附源码

    C#编程总结(七)数据加密——附源码 概述 数据加密的基本过程就是对原来为明文的文件或数据按某种算法进行处理,使其成为不可读的一段代码,通常称为“密文”,使其只能在输入相应的密钥之后才能显示出本来内容 ...

  9. Entity Framework在Asp.net MVC中的实现One Context Per Request(附源码)

    上篇中"Entity Framework中的Identity map和Unit of Work模式", 由于EF中的Identity map和Unit of Work模式,EF体现 ...

  10. Remote验证及其改进(附源码)

    Remote验证及其改进(附源码) 表单中的输入项,有些是固定的,不变的验证规则,比如字符长度,必填等.但有些是动态的,比如注册用户名是否存在这样的检查,这个需要访问服务器后台才能解决.这篇文章将会介 ...

随机推荐

  1. PAT (Advanced Level) Practice 1008 Elevator (20 分) (模拟)

    The highest building in our city has only one elevator. A request list is made up with N positive nu ...

  2. window服务session隔离

    在window服务中抓取窗体是做不到的,因为window系统的session隔离机制:如果想要调用外部程序,可以通过 创建代理进程 进行操作(通过非托管代码CreateProcessAsUser函数进 ...

  3. Linux C语言 文件操作

    打开函数 fopen 的原型如下. FILE * fopen(char *filename, char *mode); 返回值:打开成功,返回该文件对应的 FILE 类型的指针:打开失败,返回 NUL ...

  4. SSRF服务器端请求伪造

    SSRF漏洞原理 SSRF(Server-Side Request Forgery:服务器端请求伪造)是一种由恶意访问者构造形成由服务端发起请求的一个安全漏洞一般情况下,SSRF访问的目标是从外网无法 ...

  5. Java Day2(下)

    Java learning_Day2(下) 本人学习视频用的是马士兵的,也在这里献上 <链接:https://pan.baidu.com/s/1qKNGJNh0GgvlJnitTJGqgA> ...

  6. SVM-支持向量机(三)SVM回归与原理

    SVM回归 我们之前提到过,SVM算法功能非常强大:不仅支持线性与非线性的分类,也支持线性与非线性回归.它的主要思想是逆转目标:在分类问题中,是要在两个类别中拟合最大可能的街道(间隔),同时限制间隔侵 ...

  7. 微信小程序CSS之Flex布局

    转载:https://blog.csdn.net/u012927188/article/details/83040156 相信刚开始学习开发小程序的初学者一定对界面的布局很困扰,不知道怎么布局,怎么摆 ...

  8. js 获取项目名称

    //获取项目名称function getWebName(){ var pathName = window.location.pathname.substring(1); var webName = p ...

  9. C++——动态内存分配1

    9.动态内存分配  new 类型名T(初值列表) 其功能是在程序执行期间申请用于存放T类型对象的内存空间,并依初值列表赋以初值,结果值:成功则T类型的指针,指向新分配的内存:失败则为0(null).若 ...

  10. Oracle 中的 Incarnation 到底是个什么?概念理解篇

    高中时候,我深深“爱”上了一位女孩子.那个年纪确实不懂什么是真正的“爱”,反正每天满脑子都是她,只要见到她就会紧张和激动,确切的说是深深的喜欢.你告诉我这叫初恋?不,我的初恋应该是小学3年级,三六班. ...