当前位置: 首页 > news >正文

Flv与Rtmp

Flv和Rtmp封装格式转化

一.Flv介绍

FLV(Flash Video)是Adobe公司推出的⼀种流媒体格式,由于其封装后的⾳视频⽂件体积⼩、 封装简单等特点,⾮常适合于互联⽹上使⽤。⽬前主流的视频⽹站基本都⽀持FLV。采⽤FLV 格式封装的⽂件后缀为.flv。

二.Flv格式

1.FLV封装格式是由⼀个⽂件头(file header)和 ⽂件体(file Body)组成。

2.FLV body由⼀ 对对的(Previous Tag Size字段 + tag)组成。Previous Tag Size字段 排列在Tag之前,占⽤4 个字节。Previous Tag Size记录了前⾯⼀个Tag的⼤⼩,⽤于逆向读取处理。(Seek比较方便)

3.FLV header后 的第⼀个Pervious Tag Size的值为0。

在这里插入图片描述

在这里插入图片描述

其中,Previous Tag Size 占用4个字节,记录前一个 Tag 的大小,用于逆向读取处理。

FLV header 后的第一个 Previous Tag Size 为 0。

Tag 由 Tag Header 和 Tag Body 组成。Tag Header 占 11 字节。

而 Tag Body 一般分为三种类型:脚本(Script)数据、视频数据、音频数据。其中 Video Tag 和 Audio Tag 由 Tag header 和 Tag Data 组成;Script Tag 只有 Tag Data。

FLV 数据以大端序进行存储,在解析时需要注意。

FLV 文件的详细内容结构如下:

在这里插入图片描述

  • FLV header

从上图中可以看到,FLV 头占用 9 个字节,用来标识文件类型为 FLV 类型,以及后续的音视频标识。一个 FLV 文件,每种类型的 tag 都属于一个流,也就是一个 flv 文件最多只有一个音频流,一个视频流,不存在多个独立的音视频流在一个文件的情况。FLV 头的结构如下:

在这里插入图片描述

参考ZLMedia中FlvHeader定义,常将FlvHeader与4字节长度合并到一起定义结构如下:注意区分大小端

windows需要加上#pragma pack(push, 1) 参考如下代码

#pragma pack(push, 1)
class FLVHeader {
public:static constexpr uint8_t kFlvVersion = 1;static constexpr uint8_t kFlvHeaderLength = 9;//FLVchar flv[3];//File version (for example, 0x01 for FLV version 1)uint8_t version;
#if __BYTE_ORDER == __BIG_ENDIAN// 保留,置0  [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 5;// 是否有音频  [AUTO-TRANSLATED:9467870a]// Whether there is audiouint8_t have_audio : 1;// 保留,置0  [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 1;// 是否有视频  [AUTO-TRANSLATED:42d0ed81]// Whether there is videouint8_t have_video : 1;
#else// 是否有视频  [AUTO-TRANSLATED:42d0ed81]// Whether there is videouint8_t have_video : 1;// 保留,置0  [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 1;// 是否有音频  [AUTO-TRANSLATED:9467870a]// Whether there is audiouint8_t have_audio : 1;// 保留,置0  [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 5;
#endif// The length of this header in bytes,固定为9  [AUTO-TRANSLATED:126988fc]// The length of this header in bytes, fixed to 9uint32_t length;// 固定为0  [AUTO-TRANSLATED:d266c0a7]// Fixed to 0uint32_t previous_tag_size0;
};
#pragma pack(pop)

FLV body

FLV Header之后,就是 FLV File Body。FLV File Body 是由一连串的 back-pointers + tags 构成。

back-pointers 指的是上一个Tag的大小,Previous Tag Size永远是0, TagSize 的作用方便文件从后面向前查找例如Seek

在这里插入图片描述

FLV tag

tag 表示音视频数据,每一个 tag 由两部分组成:tag header 和 tag data。

Flv Tag Header定义,一般Flv tag与rtmp tag保持一致,参考ZLMediaKit定义结构体如下:

class RtmpTagHeader {
public:uint8_t type = 0;uint8_t data_size[3] = {0};uint8_t timestamp[3] = {0};uint8_t timestamp_ex = 0;uint8_t streamid[3] = {0}; /* Always 0. */
};

上面 Timestamp 和 TimestampExtended 两个字段拼成一个 32 位的时间戳,是当前 Tag 的解码时间戳 (DTS)。对于音频帧来说,PTS 和 DTS 相同。

对于视频帧来说,若含 B 帧,则 PTS 和 DTS 不同,H264 视频帧 PTS = DTS + CTS,CTS 就是 CompositionTime 字段,表示 PTS 与 DTS 的时间偏移值,单位 ms。

tag header 一般占 11 个字节。

在这里插入图片描述

Flv Tag Header读取实例,因为是Flv Tag Header读取,结构体可以不用严格按照11字节创建结构体。

#define FLV_HEADER_SIZE		9 // DataOffset included
#define FLV_TAG_HEADER_SIZE	11 // StreamID included// FLV Tag Type
#define FLV_TYPE_AUDIO		8
#define FLV_TYPE_VIDEO		9
#define FLV_TYPE_SCRIPT		18struct FlvTagHeader
{uint8_t filter; // 0-No pre-processing requireduint8_t type; // 8-audio, 9-video, 18-script datauint32_t size; // data sizeuint32_t timestamp;uint32_t streamId;
};int RecordReaderFlv::FlvTagHeaderRead(struct FlvTagHeader* tag, const uint8_t* buf, size_t len)
{if (len < FLV_TAG_HEADER_SIZE){assert(0);return -1;}// TagTypetag->type = buf[0] & 0x1F;tag->filter = (buf[0] >> 5) & 0x01;assert(FLV_TYPE_VIDEO == tag->type || FLV_TYPE_AUDIO == tag->type || FLV_TYPE_SCRIPT == tag->type);// DataSizetag->size = ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | buf[3];// TimestampExtended | Timestamptag->timestamp = ((uint32_t)buf[4] << 16) | ((uint32_t)buf[5] << 8) | buf[6] | ((uint32_t)buf[7] << 24);// StreamID Always 0tag->streamId = ((uint32_t)buf[8] << 16) | ((uint32_t)buf[9] << 8) | buf[10];//assert(0 == tag->streamId);return FLV_TAG_HEADER_SIZE;
}

Flv Tag Header 写入实例:

void set_be24(void *p, uint32_t val)
{uint8_t *data = (uint8_t *) p;data[0] = val >> 16;data[1] = val >> 8;data[2] = val;
}void FlvMuxer::onWriteFlvTag(uint8_t type, const Buffer::Ptr &buffer, uint32_t time_stamp, bool flush) {RtmpTagHeader header;header.type = type;set_be24(header.data_size, (uint32_t) buffer->size());header.timestamp_ex = (time_stamp >> 24) & 0xff;set_be24(header.timestamp, time_stamp & 0xFFFFFF);//tag headeronWrite(obtainBuffer((char *) &header, sizeof(header)), false);//tag dataonWrite(buffer, false);//PreviousTagSizeuint32_t size = htonl((uint32_t) (buffer->size() + sizeof(header)));onWrite(obtainBuffer((char *) &size, 4), flush);
}

FLV tag 的类型可以是视频、音频和 Script(脚本类型)。

FLV tag data:audio tag

audio tag 包含 audio tag header 和 audio tag body 两部分。

其中,audio tag header 占 1 字节,包含了音频数据的参数信息,从第 2 个字节开始为音频流数据。

在这里插入图片描述

前 4bit 表示音频格式;第 5、6bit 表示采样率;第 7bit 表示采样精度;第 8bit 表示音频声道。

在这里插入图片描述

这里着重强调一下格式10格式:AAC。声音类型应为 1 (立体声) 且采样率应为 3 (44 kHz)。这并不表示 FLV 中的 AAC 音频总是立体声、44 kHz的数据。实际上,Flash 播放器会忽略这两个值,而从已编码的 AAC 位流中提取出声道数和采样率信息。

第二个字节开始为音频数据:

在这里插入图片描述

如果音频格式为 10,即是 AAC 格式。AudioTagHeader 中会多出一个字节 AACPacketType,这个字段来表示 AACAUDIODATA 的类型:0 = AAC sequence header,1 = AAC raw。

AAC sequence header 也就是包含了 AudioSpecificConfig,里面有一些更加详细音频的信息。
AAC raw 这种包含的就是音频 ES 流了,也就是音频负载(audio payload)。

FLV tag data:video tag

video tag 包含 video tag header 和 video tag body 两部分。video tag data 开始的第一个字节包含视频数据的参数信息,从第二个字节开始为视频流数据。结构如下:

在这里插入图片描述

FLV 的写流程

参考ZLMediaKit FlvMuxer,写Flv主要流程

void FlvMuxer::onWriteFlvHeader(const RtmpMediaSource::Ptr &src) {// 发送flv文件头  [AUTO-TRANSLATED:ee2c5556]// Send the flv file header.auto buffer = obtainBuffer();buffer->setCapacity(sizeof(FLVHeader));buffer->setSize(sizeof(FLVHeader));FLVHeader *header = (FLVHeader *) buffer->data();memset(header, 0, sizeof(FLVHeader));header->flv[0] = 'F';header->flv[1] = 'L';header->flv[2] = 'V';header->version = FLVHeader::kFlvVersion;header->length = htonl(FLVHeader::kFlvHeaderLength);header->have_video = src->haveVideo();header->have_audio = src->haveAudio();// memset时已经赋值为0  [AUTO-TRANSLATED:0f71eef1]// It has already been assigned to 0 during memset.//header->previous_tag_size0 = 0;//flv headeronWrite(buffer, false);// metadatasrc->getMetaData([&](const AMFValue &metadata) {AMFEncoder invoke;invoke << "onMetaData" << metadata;onWriteFlvTag(MSG_DATA, std::make_shared<BufferString>(invoke.data()), 0, false);});//config framesrc->getConfigFrame([&](const RtmpPacket::Ptr &pkt) {onWriteRtmp(pkt, true);});
}void FlvMuxer::onWriteFlvTag(const RtmpPacket::Ptr &pkt, uint32_t time_stamp, bool flush) {onWriteFlvTag(pkt->type_id, pkt, time_stamp, flush);
}void FlvMuxer::onWriteFlvTag(uint8_t type, const Buffer::Ptr &buffer, uint32_t time_stamp, bool flush) {RtmpTagHeader header;header.type = type;set_be24(header.data_size, (uint32_t) buffer->size());header.timestamp_ex = (time_stamp >> 24) & 0xff;set_be24(header.timestamp, time_stamp & 0xFFFFFF);//tag headeronWrite(obtainBuffer((char *) &header, sizeof(header)), false);//tag dataonWrite(buffer, false);//PreviousTagSizeuint32_t size = htonl((uint32_t) (buffer->size() + sizeof(header)));onWrite(obtainBuffer((char *) &size, 4), flush);
}void FlvMuxer::onWriteRtmp(const RtmpPacket::Ptr &pkt, bool flush) {onWriteFlvTag(pkt, pkt->time_stamp, flush);
}

参考链接:https://blog.csdn.net/ProgramNovice/article/details/137468819

三.Rtmp协议格式

Message介绍

1、消息(Message)是RTMP协议中基本的数据单元。由Message Header和Message Payload(可以理解成message body)组成。

2、对于音视频数据而言每一个message就是一帧数据。对于flv的tag而言,就是对应rtmp每个message,一个tag就是一个message,是一一对应的关系;相当于每一个tag都封装成一个message。message payload的数据格式和tag data的数据格式是相同的,message header和tag header的格式不同。

3、Message Format如下:

FieldTypeComment
Message HeaderLength3 bytesMessage Payload(消息负载)的长度,不包含Message Header
Timestamp4 bytes时间戳(既是pts也是dts,因为直播场景中没有B帧,所以pts=dts)
Message [Type Id](https://so.csdn.net/so/search?q=Type Id&spm=1001.2101.3001.7020)1 bytes消息类型,主要包括协议控制消息、音视频消息、命令消息等
Message Stream Id3 bytes消息流ID可以是任意值。不同的message可以有相同的值。复用到同一块流上的不同消息流基于它们的消息流ID解复用。
Message Payloadn bytes是消息中包含的实际数据,消息类型不同payload大小也不同。例如,它可以是一些音频样本或压缩视频数据或Metadata等

4、多路复用,RTMP可以将来自不同视频流的切片(chunk)在单个连接上传输,这种方法被称为“多路复用”,不同的流就用不同的Message Stream Id区分。

详细参考链接: https://blog.csdn.net/weixin_39399492/article/details/128069969

Chunk介绍

1、RTMP以Message为基本单位,通过把Message拆分成Chunk来进行网络发送。chunk data默认是128字节。chunk是RTMP最小的传输单元。目的是:防止一个大的数据包传输时间过长,阻塞其它数据包的传输。chunk合成message:接收端将接收到chunk的chunk data的大小加和,如果等于message payload(通过chunk->message header->message length获取)的则认为是同一个message。

2、Chunk在传输时:同一个Message产生的多个Chunk只会串行发送。先发送的Chunk一定先到达。不同Message产生的Chunk可以并行发送。并行发送的Chunk复用了一条TCP链接。

3、Chunk Format如下:

在这里插入图片描述

FieldTypeComment
Chunk HeaderBasic Header1-3 bytes包含fmt(chunk type)和chunk stream id(csid),其中fmt决定了chunk的类型及message header的长度,占2 bit,而Basic header的长度取决于csid的数值大小,最少占1 byte。
Message Header0,3,7 or 11 bytes要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。 长度取决于Basic Header中的chunk type,有Type 0,1,2,
Extended Timestamp0 or 4 bytes扩展时间戳(0 bytes时表示此字段不存在)
Chunk Datan bytes是消息中包含的实际数据,消息类型不同data大小也不同

4、有多种chunk type的目的是:减少重复数据发送,提高chunk data的占比。

Basic Header
1.Basic Header
FieldTypeComment
fmt2 bits表示chunk type,取值[0, 3],即chunk共有4种类型
csid6,14 or 22 bitscsid范围是365599,02为协议保留用作特殊信息;**通常控制流csid为2,命令流为3,开发中发现音视频流csid可自定义,如音频流4,视频流6.**上文提到Basic Header大小为1-3 bytes,由于fmt域占2bits,所以CSID长度分别是6 bits、14 bits或22 bits
2. Message Header

1、Message Header的格式和长度取决于Basic Header的chunk type,即fmt,fmt取值[0-3],所以共有4种不同的chunk格式,目的是减少重复数据发送,提高 chunk data的占比。同时也有4种不同的Message Header。

2、Message Header结构如下:

(1) chunk type = 0(fmt) = 0:11 bytes

.0               1               2               30 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    timestamp                  |message length |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|    message length (coutinue)  |message type id| msg stream id |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                  msg stream id                |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

type=0时Message Header占用11个字节,其他三种能表示的数据它都能表示,但在chunk stream 的开始第一个chunk和头信息
中的时间戳后退(即值与上一个chunk相比减小,通常在回退播放的时候会出现这种情况)的时候必须采用这种格式。

  • timestamp(时间戳):占用3个字节,因此它最多能表示到16777215=0xFFFFFF=2^24-1,当它
    的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp会转存到 Extended
    Timestamp 字段中,接收端在判断timestamp字段24个位都为1时就会去Extended Timestamp
    中解析实际的时间戳。
  • message length(消息数据长度):占用3个字节,表示实际发送的消息的数据如音频帧、视频
    帧等数据的长度,单位是字节。注意这里是Message的长度,也就是chunk属于的Message的总长
    度,而不是chunk本身data的长度。
  • message type id(消息的类型id):1个字节,表示实际发送的数据的类型,如8代表音频数据,
    9代表视频数据。
  • message stream id(消息的流id):4个字节,表示该chunk所在的流的ID,和Basic Header
    的CSID一样,它采用小端存储方式。

(2) Chunk Type(fmt) = 1:7 bytes

.0               1               2               30 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|               timestamp delta                 |message length |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|    message length (coutinue)  |message type id|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

type为1时占用7个字节,省去了表示message stream id的4个字节,表示此chunk和上一次发的 chunk 所在的流相同,如果在
发送端和对端有一个流链接的时候可以尽量采取这种格式。

  • timestamp delta:3 bytes,这里和type=0时不同,存储的是和上一个chunk的时间差。类似
    上面提到的timestamp,当它的值超过3个字节所能表示的最大值时,三个字节都置为1,实际
    的时间戳差值就会转存到Extended Timestamp字段中,接收端在判断timestamp delta字段24
    个bit都为1时就会去Extended Timestamp 中解析实际的与上次时间戳的差值。

  • 其他字段与上面的解释相同.

    (3) Chunk Type(fmt) = 2:3 bytes

.0               1               2               0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|               timestamp delta                 |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

type 为 2 时占用 3 个字节,相对于 type = 1 格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此 chunk
和上一次发送的 chunk 所在的流、消息的长度和消息的类型都相同。余下的这三个字节表示 timestamp delta,使用同type=1。

(4)Chunk Type(fmt) = 3: 0 byte

type=3时,为0字节,表示这个chunk的Message Header和上一个是完全相同的。当它跟在type=0的chunk后面时,表示和前一
个 chunk 的时间戳都是相同。什么时候连时间戳都是相同呢?就是一个 Message 拆分成多个 chunk,这个 chunk 和上
一个 chunk 同属于一个 Message。而当它跟在 type = 1或 type = 2 的chunk后面时的chunk后面时,表示和前一个 chunk
的时间戳的差是相同的。比如第一个 chunk 的 type = 0,timestamp = 100,第二个 chunk 的 type = 2,
timestamp delta = 20,表示时间戳为 100 + 20 = 120,第三个 chunk 的 type = 3,表示 timestamp delta = 20,
时间戳为 120 + 20 = 140。

参考链接:https://www.cnblogs.com/jimodetiantang/p/8974075.html

FLV转Rtmp Decoder解析

Flv与Rtmp内容一样只是,header不一致。

Rtmp定义如下:


#pragma pack(1)struct RtmpMessageHeader
{uint8_t timestamp[3];uint8_t length[3];uint8_t type_id;uint8_t stream_id[4];
};// chunk header: basic_header + rtmp_message_header 
class RtmpMessage
{
public:using Ptr = shared_ptr<RtmpMessage>;void clear(){index = 0;timestamp = 0;extend_timestamp = 0;if (length > 0 || payload) {length = 0;payload = nullptr;}}bool isCompleted() const{if (length > 0 && index == length && payload) {return true;}return false;}bool isKeyFrame() const{bool isEnhance = (payload->data()[0] >> 4) & 0b1000;uint8_t frame_type;uint8_t packet_type;if (isEnhance) {frame_type = (payload->data()[0] >> 4) & 0b0111;packet_type = payload->data()[0] & 0x0f;return type_id == RTMP_VIDEO && frame_type == 1 && (packet_type == 1 || packet_type == 3);} else {frame_type = (payload->data()[0] >> 4) & 0x0f;packet_type = payload->data()[1];return type_id == RTMP_VIDEO && frame_type == 1 && packet_type == 1;}}public:int trackIndex_;// chunk stream ID(流通道id)和chunk type(即fmt),chunk stream id 一般被简写为CSIDuint8_t  type_id = 0; uint8_t  codecId = 0;uint8_t  csid = 0;uint32_t index = 0;uint32_t timestamp = 0;uint32_t length = 0;uint32_t stream_id = 0;uint32_t extend_timestamp = 0;uint64_t abs_timestamp = 0;uint64_t laststep = 0;StreamBuffer::Ptr payload = nullptr;
};#pragma pack()

实现转换的实例:

void RecordReaderFlv::handleVideo(const char* data, int len)
{if (!_validVideoTrack) {return ;}weak_ptr<RecordReaderFlv> wSelf = dynamic_pointer_cast<RecordReaderFlv>(shared_from_this());uint8_t type = RTMP_VIDEO;uint8_t *payload = (u_char*)data + 11;uint32_t length = len - 11;uint8_t frame_type = (payload[0] >> 4) & 0x0f;uint8_t codec_id = payload[0] & 0x0f;uint32_t timestamp = readUint24BE(data + 4); //扩展字段也读了logTrace << "timestamp: " << timestamp;timestamp |= ((data[8]) << 24);if (!_rtmpVideoDecodeTrack) {_rtmpVideoDecodeTrack = make_shared<RtmpDecodeTrack>(VideoTrackType);if (_rtmpVideoDecodeTrack->createTrackInfo(VideoTrackType, codec_id) != 0) {_validVideoTrack = false;return;}_rtmpVideoDecodeTrack->setOnFrame([wSelf](const FrameBuffer::Ptr& frame) {auto self = wSelf.lock();if (!self) {return;}lock_guard<mutex> lck(self->_mtxFrameList);logTrace << "###input frame flv";self->_frameList.push_back(frame);});if (_onTrackInfo) {_onTrackInfo(_rtmpVideoDecodeTrack->getTrackInfo());}_rtmpVideoDecodeTrack->startDecode();}// flv解析到Rtmp 转为Rtmp Messageauto msg = make_shared<RtmpMessage>();msg->payload = make_shared<StreamBuffer>(length + 1);memcpy(msg->payload->data(), payload, length);msg->abs_timestamp = timestamp;msg->length = length;msg->type_id = RTMP_VIDEO;msg->csid = RTMP_CHUNK_VIDEO_ID;if (!_avcHeader && frame_type == 1/* && codec_id == RTMP_CODEC_ID_H264*/) {// logInfo << "payload[1] : " << (int)payload[1];if (payload[1] == 0) {// sps pps??_avcHeaderSize = length;_avcHeader = make_shared<StreamBuffer>(length + 1);memcpy(_avcHeader->data(), msg->payload->data(), length);type = RTMP_AVC_SEQUENCE_HEADER;_rtmpVideoDecodeTrack->setConfigFrame(msg);}}msg->trackIndex_ = VideoTrackType;_rtmpVideoDecodeTrack->onRtmpPacket(msg);logTrace << "_rtmpVideoDecodeTrack decodeRtmp";_rtmpVideoDecodeTrack->decodeRtmp(msg);}

完整开源代码实现链接:https://gitee.com/inyeme/simple-media-server

相关文章:

  • python打卡训练营打卡记录day49
  • SDC命令详解:使用set_wire_load_model命令进行约束
  • 最好的无线麦克风是那款?2025硬核测评西圣和飞利浦无线领夹麦克风
  • CCleaner Professional 下载安装教程 - 电脑清理优化工具详细使用指南
  • 2 Studying《Android源代码情景分析(罗升阳)》
  • 性能优化中,多面体模型基本原理
  • 易学探索助手-个人记录(十四)
  • 常见的Linux命令
  • SQL Server 触发器调用存储过程实现发送 HTTP 请求
  • 基于算法竞赛的c++编程(26)指针的高阶用法
  • DeepSeek越强,Kimi越慌?
  • FTP下载Argo数据
  • 基于UniApp开发HarmonyOS 5.0鸿蒙汽车应用的指南
  • 新基建浪潮下:中国新能源汽车充电桩智慧化建设与管理实践
  • Linux 关键目录解析:底层机制与技术细节
  • 触发DMA传输错误中断问题排查
  • JS红宝书笔记 - 3.3 变量
  • 计算机网络自定向下:第二章复习
  • 多面体模型-学习笔记2
  • Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术点解析
  • 衡水做wap网站/志鸿优化设计答案
  • 企业内部门户网站建设/seo教育
  • 定制b2b网站/凡科网建站系统源码
  • 北京好用的h5建站/app拉新推广平台渠道商
  • 做worksheet的网站/seo排名第一的企业
  • 移动端开发流程/优化推广关键词