热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

基于RTP协议的H.264视频传输系统:实现

实现的原理:基于RTP协议的H.264视频传输系统:原理相关文章:【1】RTP协议分析【2】jrtplib简介【3】Qt调用jrtplib实现单播、多播和广播【4】RTP有

实现的原理:基于RTP协议的H.264视频传输系统:原理

相关文章:

【1】RTP协议分析

【2】jrtplib简介

【3】Qt调用jrtplib实现单播、多播和广播

【4】RTP 有效负载(载荷)类型,RTP Payload Type

【5】H.264(H264)视频文件的制作

【6】H.264格式分析

【7】H.264视频压缩标准

关于RTP Payload Format for H.264 Video一定要参考文档rfc6184,因为rfc3984已经被废弃了,rfc6184从下面两个链接都可以打开。

https://tools.ietf.org/html/rfc6184

https://datatracker.ietf.org/doc/rfc6184/?include_text=1

RTP负载为H.264定义了三种不同的基本的负载结构,接收端可能通过RTP负载的首字节来识别它们。这一个字节类似NALU头的格式,它的类型字段则指出了代表的是哪一种结构,这个字节的结构如下:

+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI|  Type   |
+---------------+

Type定义如下:
0     没有定义
1-23  NAL单元   单个NAL单元包.
24    STAP-A   单一时间的组合包
25    STAP-B   单一时间的组合包
26    MTAP16   多个时间的组合包
27    MTAP24   多个时间的组合包
28    FU-A     分片的单元
29    FU-B     分片的单元
30-31 没有定义

首字节的类型字段和H.264的NALU头中类型字段的区别是,当Type的值为24~31表示这是一个特别格式的NAL单元,而H.264中,只取1~23是有效的值。下面分别说明这三种负载结构。


一.Single NALU Packet(单一NAL单元模式)
即一个RTP负载仅由首字节和一个NALU负载组成,在本文中对于小于1400字节的NALU便采用这种打包方案。这种情况下首字节类型字段和原始的H.264的NALU头类型字段是一样的。也就是说,在这种情况下RTP的负载是一个完整的NALU。



二. Aggregation Packet(组合封包模式)
在一个RTP中封装多个NALU,对于较小的NALU可以采用这种打包方案,从而提高传输效率。即可能是由多个NALU组成一个RTP包。分别有4种组合方式,STAP-A、STAP-B、MTAP16和MTAP24。那么这里的RTP负载首字节类型值分别是24、25、26和27。本文未涉及这种模式。


三.Fragmentation Units(分片封包模式FUs)
一个NALU封装在多个RTP中,每个RTP负载由首字节(这里实际上是FU indicator,但是它和原首字节的结构一样,这里仍然称首字节)、FU header和NALU负载的一部分组成。在本文中,对于大于1400字节的NALU便采用这种方案进行拆包处理。存在两种类型FU-A和FU-B,类型值分别是28和29。

FU-A类型如下图所示:

本文使用的是FU-A类型。


FU-B类型如下图所示:


与FU-A相比,FU-B多了一个DON(decoding order number),DON使用的是网络字节序。FU-B只能用于隔行扫描封包模式,不能用于其他方面。

FU indicator字节结构如下所示:

+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI|  Type   |
+---------------+

Type=28或29

FU header字节结构如下所示:

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

S(Start): 1 bit,当设置成1,该位指示分片NAL单元的开始。当随后的FU负载不是分片NAL单元的开始,该位设为0。
E(End): 1 bit,当设置成1, 该位指示分片NAL单元的结束,此时荷载的最后字节也是分片NAL单元的最后一个字节。当随后的FU荷载不是分片NAL单元的结束,该位设为0。
R(Reserved): 1 bit,保留位必须设置为0,且接收者必须忽略该位。

Type:与NALU头中的Type值相同


四.使用socket套接字发送RTP打包的H264

主要代码如下所示:

int main()
{
OpenBitstreamFile("480320.264");
NALU_t *n;
char sendbuf[1500];

unsigned short seq_num =0;
int bytes=0;
InitWinsock(); //初始化套接字库
int sockfd;
struct sockaddr_in addr_in;
float framerate=25;
unsigned int timestamp_increse=0,ts_current=0;
timestamp_increse=(unsigned int)(90000.0 / framerate);

addr_in.sin_family=AF_INET;
addr_in.sin_port=htons(DEST_PORT);
addr_in.sin_addr.s_addr=inet_addr(DEST_IP);
sockfd=socket(AF_INET,SOCK_DGRAM,0);
connect(sockfd, (const sockaddr *)&addr_in, sizeof(sockaddr_in)) ;//申请UDP套接字
n = AllocNALU(8000000);//为结构体nalu_t及其成员buf分配空间。返回值为指向nalu_t存储空间的指针

while(!feof(bits))
{
GetAnnexbNALU(n);//每执行一次,文件的指针指向本次找到的NALU的末尾,下一个位置即为下个NALU的起始码0x000001
dump(n);//输出NALU长度和TYPE

memset(sendbuf,0,1500);//清空sendbuf;此时会将上次的时间戳清空,因此需要ts_current来保存上次的时间戳值

//rtp固定包头,为12字节,该句将sendbuf[0]的地址赋给rtp_hdr,以后对rtp_hdr的写入操作将直接写入sendbuf。
rtp_hdr =(RTP_FIXED_HEADER*)&sendbuf[0];

//设置RTP HEADER
rtp_hdr->version = 2; //版本号,此版本固定为2
rtp_hdr->marker = 0; //标志位,由具体协议规定其值。
rtp_hdr->payload = H264;//负载类型号,
rtp_hdr->ssrc = htonl(10);//随机指定为10,并且在本RTP会话中全局唯一

//当一个NALU小于1400字节的时候,采用一个单RTP包发送
if(n->len<=MAX_RTP_PKT_LENGTH)
{
//设置rtp M 位;
rtp_hdr->marker=1;
rtp_hdr->seq_no = htons(seq_num ++); //序列号,每发送一个RTP包增1
ts_current=ts_current+timestamp_increse;
rtp_hdr->timestamp=htonl(ts_current);
//设置NALU HEADER,并将这个HEADER填入sendbuf[12]
nalu_hdr =(NALU_HEADER*)&sendbuf[12]; //将sendbuf[12]的地址赋给nalu_hdr,之后对nalu_hdr的写入就将写入sendbuf中;
nalu_hdr->F=n->forbidden_bit;
nalu_hdr->NRI=n->nal_reference_idc>>5;//有效数据在n->nal_reference_idc的第6,7位,需要右移5位才能将其值赋给nalu_hdr->NRI。
nalu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[13],n->buf+1,n->len-1);//去掉nalu头的nalu剩余内容写入sendbuf[13]开始的字符串。
bytes=n->len + 12 ; //获得sendbuf的长度,为nalu的长度(包含NALU头但除去起始前缀)加上rtp_header的固定长度12字节
if(n->nal_unit_type==1 || n->nal_unit_type==5)
{

send(sockfd,sendbuf,bytes,0);//发送RTP包
}
else
{
send(sockfd,sendbuf,bytes,0);//发送RTP包
//如果是6,7类型的包,不应该延时;之前有停顿,原因这在这
continue;
}
}
else
{
int packetNum = n->len/MAX_RTP_PKT_LENGTH;
if (n->len%MAX_RTP_PKT_LENGTH != 0)
packetNum ++;

int lastPackSize = n->len - (packetNum-1)*MAX_RTP_PKT_LENGTH;
int packetIndex = 1 ;

ts_current=ts_current+timestamp_increse;
rtp_hdr->timestamp=htonl(ts_current);

//发送第一个的FU,S=1,E=0,R=0
rtp_hdr->seq_no = htons(seq_num ++); //序列号,每发送一个RTP包增1
//设置rtp M 位;
rtp_hdr->marker=0;
//设置FU INDICATOR,并将这个HEADER填入sendbuf[12]
fu_ind =(FU_INDICATOR*)&sendbuf[12]; //将sendbuf[12]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
fu_ind->F=n->forbidden_bit;
fu_ind->NRI=n->nal_reference_idc>>5;
fu_ind->TYPE=28;//使用FU-A

//设置FU HEADER,并将这个HEADER填入sendbuf[13]
fu_hdr =(FU_HEADER*)&sendbuf[13];
fu_hdr->S=1;
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[14],n->buf+1,MAX_RTP_PKT_LENGTH);//去掉NALU头
bytes=MAX_RTP_PKT_LENGTH+14;//获得sendbuf的长度,为nalu的长度(除去起始前缀和NALU头)加上rtp_header,fu_ind,fu_hdr的固定长度14字节
send( sockfd, sendbuf, bytes, 0 );//发送RTP包

//发送中间的FU,S=0,E=0,R=0
for(packetIndex=2;packetIndex {
rtp_hdr->seq_no = htons(seq_num ++); //序列号,每发送一个RTP包增1

//设置rtp M 位;
rtp_hdr->marker=0;
//设置FU INDICATOR,并将这个HEADER填入sendbuf[12]
fu_ind =(FU_INDICATOR*)&sendbuf[12]; //将sendbuf[12]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
fu_ind->F=n->forbidden_bit;
fu_ind->NRI=n->nal_reference_idc>>5;
fu_ind->TYPE=28;

//设置FU HEADER,并将这个HEADER填入sendbuf[13]
fu_hdr =(FU_HEADER*)&sendbuf[13];
fu_hdr->S=0;
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[14],n->buf+(packetIndex-1)*MAX_RTP_PKT_LENGTH+1,MAX_RTP_PKT_LENGTH);//去掉起始前缀的nalu剩余内容写入sendbuf[14]开始的字符串。
bytes=MAX_RTP_PKT_LENGTH+14;//获得sendbuf的长度,为nalu的长度(除去原NALU头)加上rtp_header,fu_ind,fu_hdr的固定长度14字节
send( sockfd, sendbuf, bytes, 0 );//发送rtp包
}

//发送最后一个的FU,S=0,E=1,R=0
rtp_hdr->seq_no = htons(seq_num ++);
//设置rtp M 位;当前传输的是最后一个分片时该位置1
rtp_hdr->marker=1;
//设置FU INDICATOR,并将这个HEADER填入sendbuf[12]
fu_ind =(FU_INDICATOR*)&sendbuf[12]; //将sendbuf[12]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
fu_ind->F=n->forbidden_bit;
fu_ind->NRI=n->nal_reference_idc>>5;
fu_ind->TYPE=28;

//设置FU HEADER,并将这个HEADER填入sendbuf[13]
fu_hdr =(FU_HEADER*)&sendbuf[13];
fu_hdr->S=0;
fu_hdr->E=1;
fu_hdr->R=0;
fu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[14],n->buf+(packetIndex-1)*MAX_RTP_PKT_LENGTH+1,lastPackSize-1);//将nalu最后剩余的-1(去掉了一个字节的NALU头)字节内容写入sendbuf[14]开始的字符串。
bytes=lastPackSize-1+14;//获得sendbuf的长度,为剩余nalu的长度减1加上rtp_header,FU_INDICATOR,FU_HEADER三个包头共14字节
send( sockfd, sendbuf, bytes, 0 );//发送rtp包
}

Sleep(40);
}

FreeNALU(n);
#ifdef WIN32
WSACleanup();
#endif // WIN32
return 0;
}


五.使用jrtplib发送RTP打包的H264
使用jrtplib的好处是,不需要自己再添加RTP头了,而且jrtplib内部实现了RTCP协议。jrtplib的简介可以参考【2】,使用方法可以参考【3】。

主要代码如下所示。

int main()
{
//初始化jrtplib
uint8_t destIP[]={127,0,0,1};
initialRTP(destIP);
//打开264文件
OpenBitstreamFile("480320.264");
NALU_t *n;
char sendbuf[1500];
int bytes=0;

n = AllocNALU(8000000);//为结构体nalu_t及其成员buf分配空间。返回值为指向nalu_t存储空间的指针

while(!feof(bits))
{
GetAnnexbNALU(n);//每执行一次,文件的指针指向本次找到的NALU的末尾,下一个位置即为下个NALU的起始码0x000001
dump(n);//输出NALU长度和TYPE

memset(sendbuf,0,1500);//清空sendbuf

//当一个NALU小于1400字节的时候,采用一个单RTP包发送
if(n->len<=MAX_RTP_PKT_LENGTH)
{

//设置NALU HEADER,并将这个HEADER填入sendbuf[0]
nalu_hdr =(NALU_HEADER*)&sendbuf[0]; //将sendbuf[0]的地址赋给nalu_hdr,之后对nalu_hdr的写入就将写入sendbuf中;
nalu_hdr->F=n->forbidden_bit;
nalu_hdr->NRI=n->nal_reference_idc>>5;//有效数据在n->nal_reference_idc的第6,7位,需要右移5位才能将其值赋给nalu_hdr->NRI。
nalu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[1],n->buf+1,n->len-1);//去掉nalu头的nalu剩余内容写入sendbuf[1]开始的字符串。
bytes=n->len;
if(n->nal_unit_type==1 || n->nal_unit_type==5)
{
//第三参数Mark即RTP头中的M位
int status = m_session.SendPacket((void *)sendbuf,bytes,96,true,3600);
if (status <0)
{
std::cout< }
}
else
{
//注意此处与使用socekt发送的不同,此时时间戳不增加
int status = m_session.SendPacket((void *)sendbuf,bytes,96,true,0);
if (status <0)
{
std::cout< }
//如果是6,7类型的包,不应该延时;之前有停顿,原因这在这
continue;
}
}
else
{
int packetNum = n->len/MAX_RTP_PKT_LENGTH;
if (n->len%MAX_RTP_PKT_LENGTH != 0)
packetNum ++;

int lastPackSize = n->len - (packetNum-1)*MAX_RTP_PKT_LENGTH;
int packetIndex = 1 ;


//发送第一个的FU,S=1,E=0,R=0
fu_ind =(FU_INDICATOR*)&sendbuf[0]; //将sendbuf[0]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
fu_ind->F=n->forbidden_bit;
fu_ind->NRI=n->nal_reference_idc>>5;
fu_ind->TYPE=28;//使用FU-A

//设置FU HEADER,并将这个HEADER填入sendbuf[1]
fu_hdr =(FU_HEADER*)&sendbuf[1];
fu_hdr->S=1;
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[2],n->buf+1,MAX_RTP_PKT_LENGTH);//去掉NALU头
bytes=MAX_RTP_PKT_LENGTH+2;//获得sendbuf的长度,为nalu的长度(除去起始前缀和NALU头)加fu_ind,fu_hdr的固定长度2字节
int status = m_session.SendPacket((void *)sendbuf,bytes,96,false,0);
if (status <0)
{
std::cout< }

//发送中间的FU,S=0,E=0,R=0
for(packetIndex=2;packetIndex {
//设置FU INDICATOR,并将这个HEADER填入sendbuf[0]
fu_ind =(FU_INDICATOR*)&sendbuf[0]; //将sendbuf[0]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
fu_ind->F=n->forbidden_bit;
fu_ind->NRI=n->nal_reference_idc>>5;
fu_ind->TYPE=28;

//设置FU HEADER,并将这个HEADER填入sendbuf[1]
fu_hdr =(FU_HEADER*)&sendbuf[1];
fu_hdr->S=0;
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[2],n->buf+(packetIndex-1)*MAX_RTP_PKT_LENGTH+1,MAX_RTP_PKT_LENGTH);//去掉起始前缀的nalu剩余内容写入sendbuf[24]开始的字符串。
bytes=MAX_RTP_PKT_LENGTH+2;//获得sendbuf的长度,为nalu的长度(除去原NALU头)加上fu_ind,fu_hdr的固定长度2字节
int status = m_session.SendPacket((void *)sendbuf,bytes,96,false,0);
if (status <0)
{
std::cout< }
}

//设置FU INDICATOR,并将这个HEADER填入sendbuf[0]
fu_ind =(FU_INDICATOR*)&sendbuf[0]; //将sendbuf[0]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
fu_ind->F=n->forbidden_bit;
fu_ind->NRI=n->nal_reference_idc>>5;
fu_ind->TYPE=28;

//设置FU HEADER,并将这个HEADER填入sendbuf[1]
fu_hdr =(FU_HEADER*)&sendbuf[1];
fu_hdr->S=0;
fu_hdr->E=1;
fu_hdr->R=0;
fu_hdr->TYPE=n->nal_unit_type;

memcpy(&sendbuf[2],n->buf+(packetIndex-1)*MAX_RTP_PKT_LENGTH+1,lastPackSize-1);//将nalu最后剩余的-1(去掉了一个字节的NALU头)字节内容写入sendbuf[2]开始的字符串。
bytes=lastPackSize-1+2;//获得sendbuf的长度,为剩余nalu的长度减1加上FU_INDICATOR,FU_HEADER两个包头共2字节
status = m_session.SendPacket((void *)sendbuf,bytes,96,true,3600);
if (status <0)
{
std::cout< }
}

Sleep(40);
}

FreeNALU(n);

m_session.BYEDestroy(RTPTime(10,0),0,0);
#ifdef WIN32
WSACleanup();
#endif // WIN32

return 0;
}

六.测试

上述两种方法的测试效果相同。打开VLC或者MPlayer播放器(强烈推荐MPlayer,效果比VLC好),将w.sdp拖入到播放器中,播放器进入等待状态,然后运行程序发送RTP流,播放器开始播放。

测试使用了176144.264和480320.264两个H.264文件,第一个分辨率为176*144,NALU比较小,打包时不需要分段;第二个分辨率480*320,NALU大于1400,固存在分段打包的情况。

w.sdp

m=video 25000 RTP/AVP 96   
a=rtpmap:96 H264
a=framerate:25
c=IN IP4 127.0.0.1
注意端口号25000要和程序中设置的相同。

测试效果如下所示:




七.小结

关于时间戳,需要注意的是h264的采样率为90000HZ,因此时间戳的单位为1(秒)/90000,因此如果当前视频帧率为25fps,那时间戳间隔或者说增量应该为3600,如果帧率为30fps,则增量为3000,以此类推。
关于h264拆包,按照FU-A方式说明:
1.第一个FU-A包的FU indicator:F应该为当前NALU头的F,而NRI应该为当前NALU头的NRI,Type则等于28,表明它是FU-A包。FU header生成方法:S = 1,E = 0,R = 0,Type则等于NALU头中的Type。
2.后续的N个FU-A包的FU indicator和第一个是完全一样的,如果不是最后一个包,则FU header应该为:S = 0,E = 0,R = 0,Type等于NALU头中的Type。
3.最后一个FU-A包FU header应该为:S = 0,E = 1,R = 0,Type等于NALU头中的Type。
因此总结就是:同一个NALU分包厚的FU indicator头是完全一致的,FU header只有S以及E位有区别,分别标记开始和结束,它们的RTP分包的序列号应该是依次递增的,并且它们的时间戳必须一致,而负载数据为NALU包去掉1个字节的NALU头后对剩余数据的拆分,这点很关键,你可以认为NALU头被拆分成了FU indicator和FU header,所以不再需要1字节的NALU头了。
关于SPS以及PPS,配置帧的传输我采用了先发SPS,再发送PPS,并使用同样的时间戳,或者按照正常时间戳增量再或者组包发送的形式处理貌似都可以,看播放器怎么解码了,另外提一下,如果我们使用vlc进行播放的话,可以在sdp文件中设置SPS以及PPS,这样就可以不用发送它们了。
使用VLC播放时,sdp文件中的分包模式选项:packetization-mode=1,即在文件末尾加入=fmtp:packetization-mode=1,否则有问题。

关于packetization-mode的详细说明如下:

当packetization-mode的值为0时或不存在时, 必须使用单一NALU单元模式。
当packetization-mode的值为1时必须使用非交错(non-interleaved)封包模式。
当packetization-mode的值为2时必须使用交错(interleaved)封包模式。

每个打包方式允许的NAL单元类型总结(yes = 允许, no = 不允许, ig = 忽略)

Type   Packet    Single NAL    Non-Interleaved    Interleaved
                 Unit Mode           Mode             Mode
-------------------------------------------------------------

0      undefined     ig               ig               ig
1-23   NAL unit     yes              yes               no
24     STAP-A        no              yes               no
25     STAP-B        no               no              yes
26     MTAP16        no               no              yes
27     MTAP24        no               no              yes
28     FU-A          no              yes              yes
29     FU-B          no               no              yes
30-31  undefined     ig               ig               ig


源码链接:见http://blog.csdn.net/caoshangpa/article/details/53009604的评论



推荐阅读
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 在Oracle11g以前版本中的的DataGuard物理备用数据库,可以以只读的方式打开数据库,但此时MediaRecovery利用日志进行数据同步的过 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 本文介绍了响应式页面的概念和实现方式,包括针对不同终端制作特定页面和制作一个页面适应不同终端的显示。分析了两种实现方式的优缺点,提出了选择方案的建议。同时,对于响应式页面的需求和背景进行了讨论,解释了为什么需要响应式页面。 ... [详细]
author-avatar
淡定_一辈子
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有