H.264帧结构和RTSP协议源码框架

news2024/11/15 8:31:37

目录

 

1、H264编码原理和基本概念

1.1、h.264编码原理

1.2、h.264编码相关的一些概念

2、H264的NAL单元详解

2.1、VCL和NAL的关系

2.2、H.264视频流分析工具

2.3、h264视频流总体分析

2.4、相关概念

3、H264的NAL单元---sps和pps

3.1、sps和pps详解

3.2、H264的profile和level

3.3、sequence

4、rtsp协议和源码框架

4.1、源码框架函数调用关系

4.2、直接发送与环状buffer发送

4.3、RTP发送一帧数据的两种不同发送格式->整发送和分包发送


 

1、H264编码原理和基本概念

1.1、h.264编码原理

(1)图像冗余信息:空间冗余、时间冗余

(2)视频编码关键点:压缩比高、算法复杂度小、还原度高

(3)H.264的2大组成部分:VCL和NAL

        VCL关心如何进行视频压缩

        NAL关心压缩后的视频流如何被网络传输和解码播放

1.2、h.264编码相关的一些概念

(1)宏块 MB(macroblock):多个像素组成的一块,宏块是视频压缩算法的最基本单位

由于图像本身在局部,颜色具有相似性,所以可以把一幅图像分成若干个宏块

(2)片 slice:构成帧的一部分

        一帧图像=若干个slice(可以是一个slice)

        一个slice=若干个MB

        一个MB=多个像素

(3)帧 frame:一整幅完整的图像

(4)I帧、B帧、P帧:帧有好几种类型

I帧:非参考帧,这一帧图像的内容只和本身有关,和前一帧后一帧图像的内容无关,一般作为起始帧,因为这一帧没有任何参考,所以只能对这一帧进行帧内压缩(空间冗余上的优化)

B帧:参考帧,这一帧图像的内容不光和本身有关,还和前一帧或后一帧图像的内容有关(空间冗余+时间冗余的优化)

P帧:参考帧,这一帧图像的内容不光和本身有关,还和前一帧图像的内容有关(空间冗余+时间冗余的优化)

(5)帧率 fps:一秒中有多少帧,帧率高(慢动作),帧率低(快动作)

(6)像素->宏块->片->帧->序列->码流

2、H264的NAL单元详解

2.1、VCL和NAL的关系

(1)VCL只关心编码部分,重点在于编码算法以及在特定硬件平台的实现,VCL输出的是编码后的纯视频流信息,没有任何头信息

(2)NAL关心的是VCL的输出纯视频流如何被表达和封包以利于网络传输

(3)SODB:String Of Data Bits(VCL输出的纯视频流)

(4)RBSP:Raw Byte Sequence Payload

(5)NALU:Network Abstraction Layer Units        NALU是H264文件的基本组成单元

(6)关系:

SODB + RBSP trailing bits = RBSP

NALU header(1 byte) + RBSP = NALU

H264文件由若干个序列组成 -> 序列由若干个帧/slice组成 -> 帧/slice由分隔符和NALU单元组成 -> 去掉NALU header得到RBSP -> 去掉RBSP trailing得到SODB-> VLC播放器解码播放SODB

(7)总结:做编码器的人关心的是VCL部分;做视频传输和解码播放的人关心的是NAL部分

2.2、H.264视频流分析工具

(1)雷神作品:SpecialVH264.exe

(2)国外工具:Elecard StreamEye Tools

(3)二进制工具:winhex

(4)网络抓包工具:wireshark

(5)播放器:vlc

2.3、h264视频流总体分析

(1)h264标准有多个版本,可能会有差异,具体差异不详

(2)网上看的资料有时讲法会有冲突,或者无法验证的差异

(3)这里以海思平台为主、为准、为案例,不能保证其他平台也完全一样

(4)海思平台编码出来的H.264码流(就是一个二进制文件)都是一个一个序列组成,序列包含:1sps+1pps+1sei+1I帧+若干p帧

3a52b471bc984eb98d57f907f9d2e64d.png

2.4、相关概念

(1)序列 sequence,一般一秒发一个sequence,也就是说sequence(除去sps、pps、sei)和帧率相等

(2)分隔符:00 00 00 01的四字节组合,是用来做识别的,不是有效数据,表示一个slice的开始,表示我们有一个新的片到来。

d8fcfe76a46d42b884f6c5575dcaa3a8.png

帧的有效数据部分是不会出现(00 00 00 01)的,h264的标准规定有效数据不允许出现连续的3个00,会在第二个00后面,第三个00前面添加一个03进去,用(00 00 03 00)来表示(00 00 00)。

(3)sps

(4)pps

(5)sei

(6)NALU header:分隔符后的第一个字节

914df4ea696f4168ab46dd2c24380990.png

NALU header有8个位

bit7(forbidden_zero_bit):禁止位,默认位0,值为1表示语法出错

bit6~5(nal_ref_idc):重要性,代表这一帧的内容在视频流中的重要程度,重要程度由大到小11>10>01>00

bit4~1(nal_unit_type):NAL单元类型

f4574dd7343640338fde646a0dbde61b.png

1:表示 非I帧,具体是P还是B帧不知道

5:表示 I帧

6:表示 SEI

7:表示 SPS

8:表示 PPS

参考博文:

H264(NAL简介与I帧判断)_h264 判断i帧_jefry_xdz的博客-CSDN博客

 

3、H264的NAL单元---sps和pps

参考博文:

https://www.cnblogs.com/wainiwann/p/7477794.html

3.1、sps和pps详解

在H.264标准协议中规定了多种不同的NAL Unit类型,其中类型7表示该NAL Unit内保存的数据为Sequence Paramater Set(序列参数集),描述这个序列的参数信息都在这里。在H.264的各种语法元素中,SPS中的这些参数信息至关重要。如果其中的数据丢失或出现错误,那么解码过程很可能会失败。SPS和PPS中的信息会用于播放器的初始化使用。

SPS中保存了一组视频序列的全局参数。所谓的视频序列即原始视频的一帧一帧的图像数据经过编码之后组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集(PPS)中,例如P帧需要参考前面的帧进行编码,那么是怎么参考的,这个参考数据就保存在PPS中。

一般情况SPS和PPS位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:

  • 解码器需要在码流中间开始解码;
  • 编码器在编码的过程中改变了码流的参数(如图像分辨率等);

视频播放器为了让后续的解码过程可以使用SPS中包含的参数,必须对SPS其中的数据进行解析。其中H.264标准协议中规定的SPS格式位于文档的7.3.2.1.1部分,如下图所示:

2c6060b688da4e33b1b62e52c1f37853.png

(1) profile_idc:

标识当前H.264码流的profile。我们知道,H.264中定义了三种常用的档次profile:

基准档次:baseline profile;

主要档次:main profile;

扩展档次:extended profile;

在H.264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:

profile_idc = 66(0x42) → baseline profile;

profile_idc = 77 → main profile;

profile_idc = 88 → extended profile;

在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。

当前码流中,profile_idc = 0x42 = 66 ,因此profile档次为 baseline profile;

8eb9eae6d74d47e98d2ac45987d27d6e.png

(2) level_idc

标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。

455fbb5138304c76a06574a73ec7fb0b.png

当前码流中,level_idc = 0x1F = 31,因此码流的级别为3.1。

Level为3.1的特性:

c99f45c9897d4b54b2e145f722a362e6.png

3.2、H264的profile和level

参考博文:

h264中profile和level的含义_xiaojun11-的博客-CSDN博客

Profile是对视频压缩特性的描述(CABAC呀、颜色采样数等等),简而言之就是压缩算法的档次。Level是对视频本身特性的描述(码率、分辨率、fps)。

简单来说,Profile越高,就说明采用了越高级的压缩特性(越高级的压缩算法)。Level越高,视频的码率、分辨率、fps越高。

一些移动设备(手机、游戏机、PMP)由于性能有限,不支持全部高级视频压缩特性和高分辨率图像,只支持基础压缩特性和分辨率低一些的图像。为了让这个限制更加清晰明了,H264从低到高划分了很多Profile和Level。

profile主要是定义了编码工具(压缩算法)的集合,不同的profile,包含了不同的编码技术;

h.264的profile有三种Baseline Profile(基本的)、Main Profile(主线的)、High Profile(高等级的)

3.3、sequence

(1)一段h.264的码流其实就是多个sequence组成的

(2)每个sequence均有固定结构:1sps+1pps+1sei+1I帧+若干p帧

(3)sps和pps和sei描述该sequence的图像信息,这些信息有利于网络传输或解码

(4)I帧是关键,丢了I帧整个sequence就废了,因为I帧是自参考的,其它的帧都参考于它,每个sequence有且只有1个I帧

(5)p帧的个数等于fps-1

(6)I帧越大则P帧可以越小,反之I帧越小则P帧会越大

I帧越大,说明I帧本身压缩比例不高,参考信息比较多,比较详细,所以P帧就越好参考,就可以越小

(7)I帧的大小取决于图像本身内容,和压缩算法的空间压缩部分

如果图像本身色彩丰富、画面复制,那么I帧就会很大

(8)P帧的大小取决于图像变化的剧烈程度

(9)CBR和VBR下P帧的大小策略会不同,CBR时P帧大小基本恒定,VBR时变化会比较剧烈

CBR:固定码率

VBR:可变码率

4、rtsp协议和源码框架

rtsp协议参考:

链接:https://pan.baidu.com/s/1IHU8vaFShMq3mOUz3c2iDw?pwd=0000 
提取码:0000

4.1、源码框架函数调用关系

main
	RtspServer_init
		RtspServerListen								    //线程函数---达到PLAY状态
			socket                            		        //创建TCP套接字
			setsockopt                                 	    //设置端口复用
			bind                                      	    //绑定服务器IP和端口
			listen								            //监听、并告诉linux系统这是个服务器
			while(accept)							        //不断等待客户端连接
				setsockopt
				for                                         //实际只执行一次
					RtspClientMsg                           //此函数是重点,创建一个线程去处理这此连接
					    pthread_detach                      //分离线程,子线程结束后自动回收资源
					    while                               //如果当前不是未连接状态
					    {
						    recv                            //读1024个字节
						    if(没读到)
						    {
							    退出线程
						    }
						    ParseRequestString              //解析客户端发来的Requst请求并填充字符串数组    

						    if(是OPTIONS请求)
							    OptionAnswer                //返回服务器提供的可用方法
								    sprintf                 //组包
								    strcat                  //连接RTSP头和sdp信息
								    send                    //发送
						    if(是DESCRIBE请求)
							    DescribeAnswer              //返回sdp包给播放器client去配置解码
								    GetLocalIP              //获取本地IP
     								    	ioctl
								    sprintf                 //组包
								    send                    //发送
						    if(是SETUP请求)
							    SetupAnswer
								    ParseTransportHeader    //解析客户端发来的SETUP Request,然后填充到变量中
								    GetLocalIP              //获取本地IP
								    sprintf                 //组包
								    send                    //发送
						    if(是PLAY请求)
							    PlayAnswer
								    sprintf                 //组包
								    send                    //发送
								    socket                  //创建一个UDPsocket
								    //由此可见,之前的TCPsocket是一个命令通道,这里的UDPsocket是一个传输数据的通道
						    if(是PAUSE请求)                 //暂停请求
							    PauseAnswer                 //源码分析得知,这里并没有实现暂停码流的传输
								    sprintf                 //组包
								    send                    //发送
						    if(是TEARDOWN请求)
							    TeardownAnswer
								    sprintf                 //组包
								    send                    //发送
								    close(udp)              //关闭数据传输通道UDPsocket
							    close(tcp)                  //关闭命令传输通道TCPsocket
					      }   
		vdRTPSendThread                                     //线程函数---发送流媒体的数据
			while(1)                                        //每5ms判断当前是否有数据要发送
			{
				if(!list_empty)                             //链表为空,则表明当前没有数据要发送
				{
					get_first_item                          //取出链表中第一个非空节点
					VENC_Sent//发送码流数据
						sendto                              //UDP发送
					list_del                                //从链表中把节点去掉
				}
				usleep(5000);                               //延时5ms
			}
	SAMPLE_VENC_720P_CLASSIC
		SAMPLE_COMM_VENC_StartGetStream
			SAMPLE_COMM_VENC_GetVencStreamProc              //线程函数
				HI_MPI_VENC_GetChnAttr
				SAMPLE_COMM_VENC_GetFilePostfix
				HI_MPI_VENC_GetFd
				while
				{
					HI_MPI_VENC_Query
					malloc
					HI_MPI_VENC_GetStream                   //获取一帧编码完的码流

					//以下两种方式二选一
					SAMPLE_COMM_VENC_Sentjin                //此函数是重点---编码完后直接发送
						VENC_Sent                           //直接发送    
					saveStream                              //此函数是重点---编码完后保存到环状buf中
						malloc                              //生成一个链表节点
						INIT_LIST_HEAD                      //初始化这个节点的next指针和prev指针都指向自己
						填充节点内容
						list_add_tail                       //将节点加入链表
					//当我将编码完的数据保存到环形链表后,此时链表就有节点数据了
					//然后就可以在 vdRTPSendThread 线程中发送了

					HI_MPI_VENC_ReleaseStream
					free
				}

4.2、直接发送与环状buffer发送

直接发送:编码完直接发送,编多少发多少

代码体现在:

SAMPLE_COMM_VENC_GetVencStreamProc线程函数:
    HI_MPI_VENC_GetStream            //获取编码完的码流
    SAMPLE_COMM_VENC_Sentjin         //发送函数

缺陷:视频采集和编码速度 与 网络传输速度 都是变动的,速度不一定一致,会导致系统进入漫长的等待,不能更好的实时视频监控

 

环状buffer发送:编码完放到环状buffer中,待发送

环状buffer即环形链表,当生产者(获取码流的线程)编码完一帧数据就将这帧数据作为节点加入链表,消费者(发送线程)只管从中取。

对比代码:SAMPLE_COMM_VENC_GetVencStreamProc线程函数中,调用HI_MPI_VENC_GetStream获取编码完的码流,调用saveStream将码流数据节点加入链表,vdRTPSendThread线程从链表中取走节点,然后调用VENC_Sent发送。

当链表中没有节点,则消费者要阻塞,等待生产者编码完将数据节点加入链表。环形链表相比数组的优点是使用内存可以足够大,直到内存撑爆。

代码体现在:

SAMPLE_COMM_VENC_GetVencStreamProc线程函数:
        HI_MPI_VENC_GetStream         //获取编码完的码流
        saveStream                    //编码完后保存到环状buf中
vdRTPSendThread线程函数:
        get_first_item                //取出链表中第一个非空节点
        VENC_Sent                     //发送码流数据
        list_del                      //从链表中把节点去掉

接下来就只用关注VENC_Sent函数内是怎么组包,然后通过UDP传输码流数据了。。

4.3、RTP发送一帧数据的两种不同发送格式->整发送和分包发送

关于rtp头的timestamp位:
RTP timestamp与帧率及时钟频率的关系_帧率 时钟频率_jasonhwang的博客-CSDN博客

关于RTP发送一帧数据的两种不同发送格式:整包发送和分包发送

https://www.cnblogs.com/yjg2014/p/6144977.html

VENC_Sent函数全解:

HI_S32 VENC_Sent(char *buffer,int buflen)
{
    HI_S32 i;
    int is=0;
    int nChanNum=0;

    for(is=0;is<MAX_RTSP_CLIENT;is++)
    {
        if(g_rtspClients[is].status!=RTSP_SENDING)//判断是否是 可发送 状态
        {
            continue;
        }
        int heart = g_rtspClients[is].seqnum % 10000;//seqnum:序列号,对10000取余
        
        char* nalu_payload;
        int nAvFrmLen = 0;
        int nIsIFrm = 0;
        int nNaluType = 0;
        char sendbuf[500*1024+32];

    
        nAvFrmLen = buflen;//音频或视频数据一帧的长度

        struct sockaddr_in server;
        server.sin_family=AF_INET;
           server.sin_port=htons(g_rtspClients[is].rtpport[0]);   //对方的端口       
           server.sin_addr.s_addr=inet_addr(g_rtspClients[is].IP);//对方的IP
        int    bytes=0;
        unsigned int timestamp_increse=0;
        
        timestamp_increse=(unsigned int)(90000.0 / 25);    //  两帧之间RTP timestamp的增量 = 时钟频率 / 帧率

        rtp_hdr =(RTP_FIXED_HEADER*)&sendbuf[0];         //填充RTP包头
    
        rtp_hdr->payload     = RTP_H264;   
        rtp_hdr->version     = 2;                          //RTP版本号
        rtp_hdr->marker    = 0;           
        rtp_hdr->ssrc      = htonl(10);                   //信源标记

        if(nAvFrmLen<=nalu_sent_len)    //如果这一帧数据的长度<=1400
        {
            rtp_hdr->marker=1;                            //标记一帧的结束
            rtp_hdr->seq_no     = htons(g_rtspClients[is].seqnum++);     //下一帧的序列号
            nalu_hdr =(NALU_HEADER*)&sendbuf[12]; 
            nalu_hdr->F=0; 
            nalu_hdr->NRI=  nIsIFrm; 
            nalu_hdr->TYPE=  nNaluType;
            nalu_payload=&sendbuf[13];
            memcpy(nalu_payload,buffer,nAvFrmLen);
                    g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;            
            rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);
            bytes=nAvFrmLen+ 13 ;                
            sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
        }
        else if(nAvFrmLen>nalu_sent_len)    //如果这一帧数据的长度>1400
        {
            int k=0,l=0;
            k=nAvFrmLen/nalu_sent_len;                    //这一帧数据的长度/1400
            l=nAvFrmLen%nalu_sent_len;                    //这一帧数据的长度对1400取余
            int t=0;        

            g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;    //计算timestamp
            rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);                 //把timestamp放入RTP包头       
            while(t<=k)            //循环k+1次,也就是发k+1个包,代表这一帧发完
            {
                rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++);
                if(t==0)        //第一包
                {
                    rtp_hdr->marker=0;            //填充完RTP_header的12个字节后,接下来填充的是NALU的分包形式
                    fu_ind =(FU_INDICATOR*)&sendbuf[12];//fu_indicato是NALU头的分包形式的第一部分
                    fu_ind->F= 0; 
                    fu_ind->NRI= nIsIFrm;
                    fu_ind->TYPE=28;                    //Type为FU-A
        
                    fu_hdr =(FU_HEADER*)&sendbuf[13];    //fu_header     是NALU头的分包形式的第二部分
                    fu_hdr->E=0;
                    fu_hdr->R=0;
                    fu_hdr->S=1;                        //表示帧开始
                    fu_hdr->TYPE=nNaluType;                //NALU type
    
                    nalu_payload=&sendbuf[14];    //填充完NALU的分包形式后,接下来开始填充有效数据
                    memcpy(nalu_payload,buffer,nalu_sent_len);
    
                    bytes=nalu_sent_len+14;        //发送的长度是有效数据长度+14(14就是前面添加的这些信息)                
                    sendto( udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
    
                }
                else if(k==t)    //最后一包
                {
                    rtp_hdr->marker=1;                    //标记一帧的结束
                    fu_ind =(FU_INDICATOR*)&sendbuf[12]; 
                    fu_ind->F= 0 ;
                    fu_ind->NRI= nIsIFrm ;
                    fu_ind->TYPE=28;

                    fu_hdr =(FU_HEADER*)&sendbuf[13];
                    fu_hdr->R=0;
                    fu_hdr->S=0;
                    fu_hdr->TYPE= nNaluType;
                    fu_hdr->E=1;                        //表示帧结束,和marker一样
                    nalu_payload=&sendbuf[14];
                    memcpy(nalu_payload,buffer+t*nalu_sent_len,l);
                    bytes=l+14;        
                    sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
                }
                else if(t<k && t!=0)    //中间的包
                {

                    rtp_hdr->marker=0;

                    fu_ind =(FU_INDICATOR*)&sendbuf[12]; 
                    fu_ind->F=0; 
                    fu_ind->NRI=nIsIFrm;
                    fu_ind->TYPE=28;
                    fu_hdr =(FU_HEADER*)&sendbuf[13];
                    //fu_hdr->E=0;
                    fu_hdr->R=0;
                    fu_hdr->S=0;
                    fu_hdr->E=0;                        //S=0,E=0表示既不是开头也不是结尾
                    fu_hdr->TYPE=nNaluType;
                    nalu_payload=&sendbuf[14];
                    memcpy(nalu_payload,buffer+t*nalu_sent_len,nalu_sent_len);
                    bytes=nalu_sent_len+14;    
                    sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
                }
            }
        }

    }

    //------------------------------------------------------------
}

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/658182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HELLO算法笔记之散列表(哈希)

一、哈希表 建立键 key 与值 value 之间的映射&#xff0c;实现高效的元素查询。输入一个key&#xff0c;以O&#xff08;1&#xff09;获取对应的value 遍历&#xff1a; # 遍历哈希表 # 遍历键值对 key->value for key, value in mapp.items():print(key, "->&q…

REDIS缓存穿透 击穿 雪崩

一、前言 在我们日常的开发中&#xff0c;无不都是使用数据库来进行数据的存储&#xff0c;由于一般的系统任务中通常不会存在高并发的情况&#xff0c;所以这样看起来并没有什么问题&#xff0c;可是一旦涉及大数据量的需求&#xff0c;比如一些商品抢购的情景&#xff0c;或者…

chatgpt赋能python:Python小数运算:解决精度问题的最佳实践

Python小数运算&#xff1a;解决精度问题的最佳实践 在进行小数运算时&#xff0c;Python是一种十分常用的语言&#xff0c;但在进行小数运算时&#xff0c;由于二进制和十进制之间的转换不完全&#xff0c;可能会导致一些精度问题。为了避免这些问题&#xff0c;让我们一起了…

推荐工具D1

Windows右键菜单管理程序&#xff1a; 主要功能 启用或禁用文件、文件夹、新建、发送到、打开方式、自定义文件格式、IE浏览器、WinX等右键菜单项目 对上述场景右键菜单项目进行修改名称、修改图标、导航注册表位置、导航文件位置、永久删除等操作 对上述场景右键菜单自定义添…

meethigher-基于Netty的轻量级Web框架Jooby

Spring-Web的好处是&#xff0c;快速上手、快速成型&#xff0c;且成熟稳定无Bug。 但对于个人而言&#xff0c;这套框架太重了。由此探寻更好的轻量Web框架Jooby&#xff01; 本文源码地址meethigher/jooby-example: 基于Netty的轻量级Web框架Jooby使用示例 一、搭建项目 …

chatgpt赋能python:Python遍历指南:掌握5种常用方法实现高效遍历

Python遍历指南&#xff1a;掌握5种常用方法实现高效遍历 作为一种高效且易学的编程语言&#xff0c;Python在数据处理和分析方面常常被誉为行业标准。在Python中&#xff0c;遍历数据结构是处理数据的基本操作之一。它可以帮助您将大规模数据转换成可视化、文本分析、机器学习…

复杂前端组件 - 拖拽排序功能设计与实现

复杂前端组件 - 拖拽排序功能设计与实现 最终效果 原生实现原理 关于拖拽 标签的图片默认是可以拖动的&#xff08;效果如上图&#xff09; 然而其他的标签&#xff08;div等&#xff09;是不能被拖动的&#xff0c;鼠标点击选择后移动没有拖拽效果&#xff0c;需要添加属性…

dubbo源码阅读之-ExtensionLoader

dubbo源码阅读之-ExtensionLoader 概述构造方法说起extensionPostProcessors 后置处理器初始化实例策略ExtensionInjector 完成ioc 中的set注入 获取扩展点实现类getExtensionClasses 加载普通的扩展点getAdaptiveExtensionClass 加载自适应的扩展点创建Adaptive代理类 获取扩展…

算法刷题-字符串-替换空格

题目&#xff1a;剑指Offer 05.替换空格 力扣题目链接 请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20"。 示例 1&#xff1a; 输入&#xff1a;s “We are happy.” 输出&#xff1a;“We%20are%20happy.” 思路 如果想把这道题目做到极致&…

Python 3 | 菜鸟教程 (一)

目录 一、Python3 简介 二、Python 发展历史 三、Python 特点 &#xff08;一&#xff09;易于学习 &#xff08;二&#xff09;易于阅读 &#xff08;三&#xff09;易于维护 &#xff08;四&#xff09;一个广泛的标准库 &#xff08;五&#xff09;互动模式 &#…

【C】static关键字详解

概述 static的汉语意思是静态的&#xff0c;在C语言中static关键字可以用来修饰局部变量、全局变量和函数。 在这里给大家补充一个知识&#xff0c;我们的数据在内存中存储时&#xff0c;大概分为3个区域。 1.栈区&#xff1a;我们创建的局部变量、形参等一般就存放在这个区域…

Python3 数字(Number)与字符串 | 菜鸟教程(五)

目录 一、Python3 数字(Number) &#xff08;一&#xff09;Python 数字数据类型用于存储数值。 1、以下实例在变量赋值时 Number 对象将被创建&#xff1a; 2、您也可以使用del语句删除一些数字对象的引用。 3、您可以通过使用del语句删除单个或多个对象的引用 &#xff08;…

Golang每日一练(leetDay0100) 数据流中位数、二叉树序列化

目录 295. 数据流的中位数 Find-median-from-data-stream &#x1f31f;&#x1f31f;&#x1f31f; 297. 二叉树的序列化与反序列化 Serialize-and-deserialize-binary-tree &#x1f31f;&#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rus…

从零开始Vue项目中使用MapboxGL开发三维地图教程(六)加载点、线、面图层以及三维面图层(白模)

目录 1、加载点图层2、加载线和面图层3、加载三维面图层&#xff08;白模&#xff09; 1、加载点图层 开发地图应用时&#xff0c;加载POI等点状数据&#xff0c;显示文字或者图标信息&#xff0c;mapbox-gl对应使用的是符号图层&#xff08;symbol&#xff09;&#xff0c;下面…

Canvas.drawText 是以哪里为基线往什么方向开始画的。有什么居中方案?

0 前言 Canvas.drawText(String text, float x, float y, Paint paint) 这个方法在绘制文本时是从以什么为基线向什么地方开始绘制呢&#xff0c;水平方向上&#xff0c;可以通过设置 setTextAlign(Paint.Align.??) 来设置基线在文本左边、右边或者中间。但是垂直方向上是在哪…

三、DI 依赖注入学习总结

文章目录 一、依赖注入1.1 构造函数注入1.2 Setter 方法注入&#xff08;重点掌握&#xff09;1.2.1 通过 Set 注入复杂类型和集合类型数据 一、依赖注入 依赖注入&#xff08;Dependency Injection&#xff0c;DI&#xff09;是 Spring 框架的核心特性之一&#xff0c;也是 S…

chatgpt赋能python:Python中如何输出换行符\n

Python中如何输出换行符\n 如果你是一个Python开发者&#xff0c;你可能已经熟悉了多个输出Python变量的方法。但是&#xff0c;当你需要输出换行符’\n’时&#xff0c;你可能会遇到一些问题。这篇文章将介绍在Python中输出换行符的几种方法&#xff0c;并且告诉你哪种方法是…

13.IOC容器

IOC容器 IOC&#xff1a;Inversion of Control&#xff0c;翻译过来是反转控制 IOC思想 获取资源的传统方式&#xff1a;在应用程序中的组件需要获取资源时&#xff0c;传统的方式是组件主动的从容器中获取所需要的资源&#xff0c;在这样的模式下开发人员往往需要知道在具体…

第12课【嵌入式常见存储器类型】ROM RAM 一次性 DDR双倍速率 Flash

目录 存储器易失性存储器RAMDRAMSDRAMDDR SDRAM SRAMDRAM/SRAM总结 非易失性存储器ROMMASK ROMOTPROMEPROMEEPROM FLASH 存储器 存储器是组成计算机的重要部分&#xff0c;它可以存储数据&#xff0c;能让计算机拥有“记忆”。目前根据断电后&#xff0c;存储的数据是否会丢失…

TiDB v7.1.0 版本 Resource Control体验

作者&#xff1a; Ming 原文来源&#xff1a; https://tidb.net/blog/8abfaa25 简介 近期迎来了 TiDB v7.1.0 版本&#xff0c;也是2023年首发的LTS&#xff08;Long-Term Support Release&#xff09;版本&#xff0c;相比于之前的 v6.5.0 LTS版本已经过去了很长时间&…