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

【RTSP从零实践】2、使用RTP协议封装并传输H264

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍怎么使用RTP协议封装并传输H264🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭
⏰发布时间⏰: 2025-06-07

本文未经允许,不得转发!!!

目录

  • 🎄一、概述
  • 🎄二、实现步骤、实现细节
    • ✨2.1、实现H.264文件读取器
    • ✨2.2、实现 H264 的 RTP 数据包封装
      • 🎈2.2.1、RTP头(RTP Header)
      • 🎈2.2.2、H264 的 RTP负载(RTP Payload)
    • ✨2.3、SDP协议介绍
  • 🎄三、RTP协议封装并传输H264的实现源码
  • 🎄四、总结


在这里插入图片描述

在这里插入图片描述

🎄一、概述

上篇文章 根据RTSP协议实现一个RTSP服务,介绍了怎么写一个RTSP服务端,但处理的过程中,没有介绍怎么发送RTP包。这篇文章主要就是介绍怎么将H264视频数据封装成RTP包并发送的。

首先,需要对 H264的文件结构 有了解,我们要从一个H264文件读取视频帧,再进行RTP封包发送。本文会介绍需要用到的h264知识,需要了解更详细的可以参考以前文章:H.264视频编码及NALU详解。

然后,需要对 RTP包结构 有个了解,本文介绍的RTP包由2部分组成:RTP头、RTP负载。需要了解RTP头数据结构、RTP负载数据结构,下个小节会介绍怎样用代码实现。对RTP数据包格式需要了解更多的可以参考这篇文章:RTP协议详解 。

最后,需要对 SDP协议 有了解,后面代码运行后,需要使用vlc打开.sdp文件来验证功能,所以需要知道一个sdp文件的内容是什么意思,这个将会在2.3小节介绍,如果需要了解更多sdp协议知识,可以看这篇文章:SDP(会话描述协议)详解 及 抓包例子分析。


在这里插入图片描述

🎄二、实现步骤、实现细节

这个小写介绍一些基础知识点,下个小节会提供源码,可以结合着源码看帮助理解消化。

✨2.1、实现H.264文件读取器

在这里插入图片描述

H.264文件保存了h264编码的视频帧,每个视频帧之间以开始码00 00 0100 00 00 01分隔开。我们可以用下面代码判断是否为开始码。
在这里插入图片描述

在两个开始码之间的就是视频帧数据。h264视频帧数据的第一个字节是一个NAL头,内容如下图:
在这里插入图片描述
可以用下面代码读取NAL头:
在这里插入图片描述
以上H264文件结构需要了解的知识,完整代码可以到第三节查看。


✨2.2、实现 H264 的 RTP 数据包封装

本文介绍的RTP数据包主要由2部分组成,即 RTP头、RTP负载。

🎈2.2.1、RTP头(RTP Header)

在这里插入图片描述
上图是RTP头的结构图,包含了12个字节的内容,可以用代码定义成如下结构体:

struct RtpHeader
{/* byte 0 */uint8_t csrcLen:4;uint8_t extension:1;uint8_t padding:1;uint8_t version:2;/* byte 1 */uint8_t payloadType:7;uint8_t marker:1;/* bytes 2,3 */uint16_t seq;/* bytes 4-7 */uint32_t timestamp;/* bytes 8-11 */uint32_t ssrc;
};

RTP头这里涉及到一个 时间戳怎么计算 的问题,需要注意的是,这个时间戳是一个 时钟频率 为单位的,而不是具体的时间(秒、毫秒等)。
一般情况下,H264的时钟频率为90000Hz,假设帧率为25,那么每一帧的 时间间隔 就是1/25秒,每一帧的 时钟增量 就是(90000/25=3600)。那时间戳怎么算呢?举个例子,如果帧率为25的H264视频,第一帧的RTP时间戳为0的话,那么第二帧的RTP时间戳就是 3600,第三帧的RTP时间戳就是 7200,依次类推,后一帧的RTP时间戳在前一帧的RTP时间戳的值加上一个时钟增量


🎈2.2.2、H264 的 RTP负载(RTP Payload)

H264 的 RTP负载需要介绍两种方式,第一种是 单个NAL单元封包(Single NAL Unit Packet);第二种是 分片单元(Fragmentation Unit) 。如果H264的视频帧NALU(NAL Unit)总字节数小于 MTU(网络最大传输单元1500字节),就可以使用第一种方式,因为有一些TCP/UDP头数据,所以一般判断小于1400字节,就采用 单个NAL单元封包(Single NAL Unit Packet),否则使用分片单元(Fragmentation Unit)的方式封装RTP包。

单个NAL单元封包 的RTP负载结构如下图,相当于直接将整个NAL Unit 填入RTP负载即可:
在这里插入图片描述

分片单元的RTP负载方式也有两种,本文介绍的是FU-A的方式,RTP负载最开始由三部分组成:第一个字节是FU indicator,第二个字节是FU header,第三个字节开始就是NAL单元去掉NAL头之后的数据:
在这里插入图片描述

  • FU indicatorFU indicator的大小是一个字节,格式如下,跟NAL头的格式一样,但作为 分片RTP封包 ,并不能直接将H264的NAL头直接填上去。
    F:一般为0。为0表示此NAL单元不应包含bit错误或语法违规;为1表示此NAL单元可能包含bit错误或语法违规;
    NRI:直接将H264NAL头的NRI值填入即可;
    Type:FU-A格式的封包填28,FU-B格式的封包填29。

    +---------------+
    |0|1|2|3|4|5|6|7|
    +-+-+-+-+-+-+-+-+
    |F|NRI|  Type   |
    +---------------+
    
  • FU header FU header的大小也是一个字节,格式如下:
    S:start,NALU拆分多个分包后,第一个发送的分包,此bit位置1,其他分包为0;
    E:end,NALU拆分多个分包后,最后一个发送的分包,此bit位置1,其他分包为0;
    R:保留位,必须等于0;
    Type:将H264的NAL头的负载类型Type直接填入。

    +---------------+
    |0|1|2|3|4|5|6|7|
    +-+-+-+-+-+-+-+-+
    |S|E|R|  Type   |
    +---------------+
    

这部分可以结合下个小节代码,多看几篇去理解。


✨2.3、SDP协议介绍

本文是使用VLC打开.sdp文件来验证功能的,所以也需要知道SDP协议的一些知识,下面是本文使用的sdp文件:

m=video 9832 RTP/AVP 96 
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 192.168.2.180
  • m=video 9832 RTP/AVP 96 :表示这是一个媒体描述,媒体类型是video;接收媒体流的端口号是9832;传输协议是RTP/AVP(通过UDP传输);RTP负载类型为96。

  • a=rtpmap:96 H264/90000:RTP负载类型的具体编码是 H264,时钟频率为 90000Hz。

  • a=framerate:25:帧率为25。

  • c=IN IP4 192.168.2.180:表示连接信息,网络类型为IN,表示Internet。地址类型为IP4,表示IPv4地址。接收端地址为192.168.2.180。


在这里插入图片描述

🎄三、RTP协议封装并传输H264的实现源码

总共有5个源代码文件,1个sdp文件,具体如下:

1、H264Reader.h

#ifndef __H264_READER_H__
#define __H264_READER_H__#include <stdio.h>#define MAX_STARTCODE_LEN (4)typedef enum
{FALSE,TRUE,
} BOOL;typedef enum
{H264_NALU_TYPE_SLICE = 1,H264_NALU_TYPE_DPA = 2,H264_NALU_TYPE_DPB = 3,H264_NALU_TYPE_DPC = 4,H264_NALU_TYPE_IDR = 5,H264_NALU_TYPE_SEI = 6,H264_NALU_TYPE_SPS = 7,H264_NALU_TYPE_PPS = 8,H264_NALU_TYPE_AUD = 9,H264_NALU_TYPE_EOSEQ = 10,H264_NALU_TYPE_EOSTREAM = 11,H264_NALU_TYPE_FILL = 12,
} H264NaluType;typedef enum
{H264_NALU_PRIORITY_DISPOSABLE = 0,H264_NALU_PRIRITY_LOW = 1,H264_NALU_PRIORITY_HIGH = 2,H264_NALU_PRIORITY_HIGHEST = 3
} H264NaluPriority;typedef struct
{int startcode_len;		  //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)int forbidden_bit;		  //! should be always FALSEint nal_reference_idc;	  //! H264_NALU_PRIORITY_xxxxint nal_unit_type;		  //! H264_NALU_TYPE_xxxxBOOL isLastFrame;		  //!int frame_len;			  //!unsigned char *pFrameBuf; //!
} H264Frame_t;typedef struct H264ReaderInfo_s
{FILE *pFileFd;int frameNum;
} H264ReaderInfo_t;int H264_FileOpen(char *fileName, H264ReaderInfo_t *pH264Info);
int H264_FileClose(H264ReaderInfo_t *pH264Info);
int H264_GetFrame(H264Frame_t *pH264Frame, H264ReaderInfo_t *pH264Info);
BOOL H264_IsEndOfFile(const H264ReaderInfo_t *pH264Info);
void H264_SeekFile(H264ReaderInfo_t *pH264Info);#endif // __H264_READER_H__

2、H264Reader.c

#include "H264Reader.h"
#include <stdlib.h>#define MAX_FRAME_LEN (1920 * 1080 * 1.5) // 一帧数据最大字节数static BOOL findStartCode_001(unsigned char *Buf)
{// printf("[%d %d %d]\n", Buf[0], Buf[1], Buf[2]);return (Buf[0] == 0 && Buf[1] == 0 && Buf[2] == 1); // 0x000001 ?
}static BOOL findStartCode_0001(unsigned char *Buf)
{// printf("[%d %d %d %d]\n", Buf[0], Buf[1], Buf[2], Buf[3]);return (Buf[0] == 0 && Buf[1] == 0 && Buf[2] == 0 && Buf[3] == 1); // 0x00000001 ?
}int H264_FileOpen(char *fileName, H264ReaderInfo_t *pH264Info)
{pH264Info->pFileFd = fopen(fileName, "rb+");if (pH264Info->pFileFd == NULL){printf("[%s %d]Open file error\n", __FILE__, __LINE__);return -1;}pH264Info->frameNum = 0;return 0;
}int H264_FileClose(H264ReaderInfo_t *pH264Info)
{if (pH264Info->pFileFd != NULL){fclose(pH264Info->pFileFd);pH264Info->pFileFd = NULL;}return 0;
}BOOL H264_IsEndOfFile(const H264ReaderInfo_t *pH264Info)
{return feof(pH264Info->pFileFd);
}void H264_SeekFile(H264ReaderInfo_t *pH264Info)
{fseek(pH264Info->pFileFd, 0, SEEK_SET);pH264Info->frameNum = 0;
}/*** @brief 获取一阵h264视频帧** @param pH264Frame :输出参数,使用后 pH264Frame->pFrameBuf 需要free* @param pH264Info :输入参数* @return int*/
int H264_GetFrame(H264Frame_t *pH264Frame, H264ReaderInfo_t *pH264Info)
{int rewind = 0;if (pH264Info->pFileFd == NULL){printf("[%s %d]pFileFd error\n", __FILE__, __LINE__);return -1;}// 1.读取帧数据unsigned char *pFrame = (unsigned char *)malloc(MAX_FRAME_LEN);int readLen = fread(pFrame, 1, MAX_FRAME_LEN, pH264Info->pFileFd);if (readLen <= 0){printf("[%s %d]fread error\n", __FILE__, __LINE__);free(pFrame);return -1;}// 2.查找当前帧开始码int i = 0;for (; i < readLen - MAX_STARTCODE_LEN; i++){if (!findStartCode_0001(&pFrame[i])){if (!findStartCode_001(&pFrame[i])){continue;}else{pH264Frame->startcode_len = 3;break;}}else{pH264Frame->startcode_len = 4;break;}}if (i != 0) // 不是帧开头,偏移到帧开头重新读{printf("[%s %d]startcode error, i=%d\n", __FILE__, __LINE__, i);free(pFrame);rewind = (-(readLen - i));fseek(pH264Info->pFileFd, rewind, SEEK_CUR);return -1;}// 3.查找下一帧开始码i += MAX_STARTCODE_LEN;for (; i < readLen - MAX_STARTCODE_LEN; i++){if (!findStartCode_0001(&pFrame[i])){if (!findStartCode_001(&pFrame[i])){continue;}else{break;}}else{break;}}if (i == (readLen - MAX_STARTCODE_LEN)){if (!feof(pH264Info->pFileFd)){printf("[%s %d]MAX_FRAME_LEN too small\n", __FILE__, __LINE__);free(pFrame);return -1;}else{pH264Frame->isLastFrame = TRUE;}}// 4.填数据pH264Frame->forbidden_bit = pFrame[pH264Frame->startcode_len] & 0x80;     // 1 bitpH264Frame->nal_reference_idc = pFrame[pH264Frame->startcode_len] & 0x60; // 2 bitpH264Frame->nal_unit_type = pFrame[pH264Frame->startcode_len] & 0x1f;     // 5 bit, naluType 是开始码后一个字节的最后 5 位pH264Frame->pFrameBuf = pFrame;pH264Frame->frame_len = i;// 5.文件读取指针偏移到下一帧位置rewind = (-(readLen - i));fseek(pH264Info->pFileFd, rewind, SEEK_CUR);pH264Info->frameNum++;return pH264Frame->frame_len;
}

3、rtp.h

 #ifndef _RTP_H_
#define _RTP_H_
#include <stdint.h>#define RTP_VESION              2#define RTP_PAYLOAD_TYPE_H264   96
#define RTP_PAYLOAD_TYPE_AAC    97#define RTP_HEADER_SIZE         12
#define RTP_MAX_PKT_SIZE        1400/***    0                   1                   2                   3*    7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |V=2|P|X|  CC   |M|     PT      |       sequence number         |*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |                           timestamp                           |*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |           synchronization source (SSRC) identifier            |*   +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+*   |            contributing source (CSRC) identifiers             |*   :                             ....                              :*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+**/
struct RtpHeader
{/* byte 0 */uint8_t csrcLen:4;uint8_t extension:1;uint8_t padding:1;uint8_t version:2;/* byte 1 */uint8_t payloadType:7;uint8_t marker:1;/* bytes 2,3 */uint16_t seq;/* bytes 4-7 */uint32_t timestamp;/* bytes 8-11 */uint32_t ssrc;
};struct RtpPacket
{struct RtpHeader rtpHeader;uint8_t payload[0];
};void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,uint16_t seq, uint32_t timestamp, uint32_t ssrc);
int rtpSendPacket(int socket, char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize);#endif //_RTP_H_

4、rtp.c

 
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "rtp.h"void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,uint16_t seq, uint32_t timestamp, uint32_t ssrc)
{rtpPacket->rtpHeader.csrcLen = csrcLen;rtpPacket->rtpHeader.extension = extension;rtpPacket->rtpHeader.padding = padding;rtpPacket->rtpHeader.version = version;rtpPacket->rtpHeader.payloadType =  payloadType;rtpPacket->rtpHeader.marker = marker;rtpPacket->rtpHeader.seq = seq;rtpPacket->rtpHeader.timestamp = timestamp;rtpPacket->rtpHeader.ssrc = ssrc;
}int rtpSendPacket(int socket, char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize)
{struct sockaddr_in addr;int ret;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip);rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);ret = sendto(socket, (void*)rtpPacket, dataSize+RTP_HEADER_SIZE, 0,(struct sockaddr*)&addr, sizeof(addr));rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);return ret;
}

5、rtp_h264_main.c


#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>#include "rtp.h"
#include "H264Reader.h"#define H264_FILE_NAME "test.h264"
#define CLIENT_IP "192.168.2.180" // 运行vlc打开sdp文件的电脑IP
#define CLIENT_PORT 9832
#define FPS 25static int createUdpSocket()
{int fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0)return -1;int on = 1;setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on));return fd;
}static int rtpSendH264Frame(int socket, char *ip, int16_t port,struct RtpPacket *rtpPacket, uint8_t *frame, uint32_t frameSize)
{uint8_t naluType; // nalu第一个字节int sendBytes = 0;int ret;naluType = frame[0];if (frameSize <= RTP_MAX_PKT_SIZE) // nalu长度小于最大包场:单一NALU单元模式{/**   0 1 2 3 4 5 6 7 8 9*  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*  |F|NRI|  Type   | a single NAL unit ... |*  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/memcpy(rtpPacket->payload, frame, frameSize);ret = rtpSendPacket(socket, ip, port, rtpPacket, frameSize);if (ret < 0)return -1;rtpPacket->rtpHeader.seq++;sendBytes += ret;if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加时间戳goto out;}else // nalu长度大于最大包场:分片模式{/**  0                   1                   2*  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+* | FU indicator  |   FU header   |   FU payload   ...  |* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*//**     FU Indicator*    0 1 2 3 4 5 6 7*   +-+-+-+-+-+-+-+-+*   |F|NRI|  Type   |*   +---------------+*//**      FU Header*    0 1 2 3 4 5 6 7*   +-+-+-+-+-+-+-+-+*   |S|E|R|  Type   |*   +---------------+*/int pktNum = frameSize / RTP_MAX_PKT_SIZE;        // 有几个完整的包int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小int i, pos = 1;/* 发送完整的包 */for (i = 0; i < pktNum; i++){rtpPacket->payload[0] = (naluType & 0x60) | 28;rtpPacket->payload[1] = naluType & 0x1F;if (i == 0)                                     // 第一包数据rtpPacket->payload[1] |= 0x80;              // startelse if (remainPktSize == 0 && i == pktNum - 1) // 最后一包数据rtpPacket->payload[1] |= 0x40;              // endmemcpy(rtpPacket->payload + 2, frame + pos, RTP_MAX_PKT_SIZE);ret = rtpSendPacket(socket, ip, port, rtpPacket, RTP_MAX_PKT_SIZE + 2);if (ret < 0)return -1;rtpPacket->rtpHeader.seq++;sendBytes += ret;pos += RTP_MAX_PKT_SIZE;}/* 发送剩余的数据 */if (remainPktSize > 0){rtpPacket->payload[0] = (naluType & 0x60) | 28;rtpPacket->payload[1] = naluType & 0x1F;rtpPacket->payload[1] |= 0x40; // endmemcpy(rtpPacket->payload + 2, frame + pos, remainPktSize + 2);ret = rtpSendPacket(socket, ip, port, rtpPacket, remainPktSize + 2);if (ret < 0)return -1;rtpPacket->rtpHeader.seq++;sendBytes += ret;}}out:return sendBytes;
}int main(int argc, char *argv[])
{int socket = createUdpSocket();if (socket < 0){printf("failed to create socket\n");return -1;}struct RtpPacket *rtpPacket = (struct RtpPacket *)malloc(sizeof(struct RtpPacket) + (1920 * 1080 * 4));rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264, 0,0, 0, 0x88923423);// h264H264ReaderInfo_t h264Info;if (H264_FileOpen(H264_FILE_NAME, &h264Info) < 0){printf("failed to open %s\n", H264_FILE_NAME);return -1;}while (1){if (!H264_IsEndOfFile(&h264Info)){H264Frame_t h264Frame;memset(&h264Frame, 0, sizeof(h264Frame));H264_GetFrame(&h264Frame, &h264Info);if (h264Frame.pFrameBuf != NULL){if (h264Frame.isLastFrame) // 最后一帧,移到开头重新读{printf("warning SeekFile 1\n");H264_SeekFile(&h264Info);}// printf("rtpSendH264Frame\n");rtpSendH264Frame(socket, CLIENT_IP, CLIENT_PORT, rtpPacket,h264Frame.pFrameBuf + h264Frame.startcode_len,h264Frame.frame_len - h264Frame.startcode_len);free(h264Frame.pFrameBuf);rtpPacket->rtpHeader.timestamp += 90000 / FPS; // RTP 传输视频每秒 90k HZusleep(1000 * 1000 / FPS);}}}free(rtpPacket);return 0;
}

6、rtp_h264.sdp

m=video 9832 RTP/AVP 96 
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 192.168.2.180

将上面代码保存在同一个目录后,并且在同目录里放一个.h264文件,然后运行 gcc *.c 编译,再执行./a.out运行程序,下面是我运行的过程:
在这里插入图片描述


在这里插入图片描述

🎄四、总结

本文介绍了实现 使用RTP协议封装并传输H264 的一些步骤和细节,介绍了RTP封包格式,sdp相关知识等,也提供了实现源码和运行结果,可以帮助读者快速了解RT协议。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

参考:
https://blog.csdn.net/huabiaochen/article/details/104550107

http://www.lqws.cn/news/449425.html

相关文章:

  • JetBrains IDE v2025.1 升级,AI 智能+语言支持齐飞
  • Maven 之工程化开发核心指南:插件配置、pom 文件与依赖管理
  • GreatSQL加入openKylin社区,共推开源生态建设
  • keep-alive缓存文章列表案例完整代码(Vue3)
  • Symbol.iterator 详解
  • 《被讨厌的勇气》详细概述
  • 【大数据】java API 进行集群间distCP 报错unresolvedAddressException
  • vue3打包后,图片丢失
  • 【unity游戏开发——热更新】什么是Unity热更新
  • 【精选】基于SpringBoot的宠物互助服务小程序平台开发 微信小程序宠物互助系统 宠物互助小程序平台设计与实现 支持救助发布+领养申请+交流互动功能
  • 无人机不再“盲飞”!用Python搞定实时目标识别与跟踪
  • 【Linux驱动开发 ---- 4.2_平台设备(Platform Devices)概述】
  • 1.容器技术与docker环境部署
  • phpstudy无法启动mysql,一启动就关闭,完美解决
  • PLuTo 编译器示例9-12
  • 从生活场景学透 JavaScript 原型与原型链
  • 力扣-62.不同路径
  • 【MySQL篇08】:undo log日志和MVCC实现事务隔离
  • 【小程序】如何生成特定页面的小程序码
  • apifox接收流式响应无法自动合并json内容
  • 组态王工程运行时间显示
  • 数据库从零开始:MySQL 中的 DDL 库操作详解【Linux版】
  • 大话软工笔记—业务功能分类
  • 青少年编程与数学 01-012 通用应用软件简介 03 图像处理软件
  • VR 看房:突破成长痛点,展望未来趋势
  • OPENGLPG第九版学习 - 纹理与帧缓存 part1
  • 【笔记】解决部署国产AI Agent 开源项目 MiniMax-M1时 Hugging Face 模型下载缓存占满 C 盘问题:更改缓存位置全流程
  • 【Python进阶系列】第10篇:Python 项目的结构设计与目录规范 —— 从脚本到模块,从混乱到整洁
  • Flutter Hero 组件详解及应用
  • 定位接口偶发超时的实战分析:iOS抓包流程的完整复现