RTMPdump(libRTMP) 源代码分析 10: 处理各种消息(Message)

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

注:此前写了一些列的分析RTMPdump(libRTMP)源代码的文章,在此列一个列表:
RTMPdump 源代码分析 1: main()函数
RTMPDump(libRTMP)源代码分析 2:解析RTMP地址——RTMP_ParseURL()
RTMPdump(libRTMP) 源代码分析 3: AMF编码
RTMPdump(libRTMP)源代码分析 4: 连接第一步——握手(Hand Shake)
RTMPdump(libRTMP) 源代码分析 5: 建立一个流媒体连接 (NetConnection部分)
RTMPdump(libRTMP) 源代码分析 6: 建立一个流媒体连接 (NetStream部分 1)
RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)
RTMPdump(libRTMP) 源代码分析 8: 发送消息(Message)
RTMPdump(libRTMP) 源代码分析 9: 接收消息(Message)(接收视音频数据)
RTMPdump(libRTMP) 源代码分析 10: 处理各种消息(Message)

===============================


已经连续写了一系列的博客了,其实大部分内容都是去年搞RTMP研究的时候积累的经验,回顾一下过去的知识,其实RTMPdump(libRTMP)主要的功能也都分析的差不多了,现在感觉还需要一些查漏补缺。主要就是它是如何处理各种消息(Message)的这方面还没有研究的特明白,在此需要详细研究一下。

再来看一下RTMPdump(libRTMP)的“灵魂”函数RTMP_ClientPacket(),主要完成了各种消息的处理。

//处理接收到的数据
int
RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
{int bHasMediaPacket = 0;switch (packet->m_packetType){//RTMP消息类型ID=1,设置块大小case 0x01:/* chunk size *///----------------r->dlg->AppendCInfo("处理收到的数据。消息 Set Chunk Size (typeID=1)。");//-----------------------------RTMP_LogPrintf("处理消息 Set Chunk Size (typeID=1)\n");HandleChangeChunkSize(r, packet);break;//RTMP消息类型ID=3,致谢case 0x03:/* bytes read report */RTMP_Log(RTMP_LOGDEBUG, "%s, received: bytes read report", __FUNCTION__);break;//RTMP消息类型ID=4,用户控制case 0x04:/* ctrl *///----------------r->dlg->AppendCInfo("处理收到的数据。消息 User Control (typeID=4)。");//-----------------------------RTMP_LogPrintf("处理消息 User Control (typeID=4)\n");HandleCtrl(r, packet);break;//RTMP消息类型ID=5case 0x05:/* server bw *///----------------r->dlg->AppendCInfo("处理收到的数据。消息 Window Acknowledgement Size (typeID=5)。");//-----------------------------RTMP_LogPrintf("处理消息 Window Acknowledgement Size (typeID=5)\n");HandleServerBW(r, packet);break;//RTMP消息类型ID=6case 0x06:/* client bw *///----------------r->dlg->AppendCInfo("处理收到的数据。消息 Set Peer Bandwidth (typeID=6)。");//-----------------------------RTMP_LogPrintf("处理消息 Set Peer Bandwidth (typeID=6)\n");HandleClientBW(r, packet);break;//RTMP消息类型ID=8,音频数据case 0x08:/* audio data *//*RTMP_Log(RTMP_LOGDEBUG, "%s, received: audio %lu bytes", __FUNCTION__, packet.m_nBodySize); */HandleAudio(r, packet);bHasMediaPacket = 1;if (!r->m_mediaChannel)r->m_mediaChannel = packet->m_nChannel;if (!r->m_pausing)r->m_mediaStamp = packet->m_nTimeStamp;break;//RTMP消息类型ID=9,视频数据case 0x09:/* video data *//*RTMP_Log(RTMP_LOGDEBUG, "%s, received: video %lu bytes", __FUNCTION__, packet.m_nBodySize); */HandleVideo(r, packet);bHasMediaPacket = 1;if (!r->m_mediaChannel)r->m_mediaChannel = packet->m_nChannel;if (!r->m_pausing)r->m_mediaStamp = packet->m_nTimeStamp;break;//RTMP消息类型ID=15,AMF3编码,忽略case 0x0F:			/* flex stream send */RTMP_Log(RTMP_LOGDEBUG,"%s, flex stream send, size %lu bytes, not supported, ignoring",__FUNCTION__, packet->m_nBodySize);break;//RTMP消息类型ID=16,AMF3编码,忽略case 0x10:			/* flex shared object */RTMP_Log(RTMP_LOGDEBUG,"%s, flex shared object, size %lu bytes, not supported, ignoring",__FUNCTION__, packet->m_nBodySize);break;//RTMP消息类型ID=17,AMF3编码,忽略case 0x11:			/* flex message */{RTMP_Log(RTMP_LOGDEBUG,"%s, flex message, size %lu bytes, not fully supported",__FUNCTION__, packet->m_nBodySize);/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); *//* some DEBUG code */
#if 0RTMP_LIB_AMFObject obj;int nRes = obj.Decode(packet.m_body+1, packet.m_nBodySize-1);if(nRes < 0) {RTMP_Log(RTMP_LOGERROR, "%s, error decoding AMF3 packet", __FUNCTION__);/*return; */}obj.Dump();
#endifif (HandleInvoke(r, packet->m_body + 1, packet->m_nBodySize - 1) == 1)bHasMediaPacket = 2;break;}//RTMP消息类型ID=18,AMF0编码,数据消息case 0x12:/* metadata (notify) */RTMP_Log(RTMP_LOGDEBUG, "%s, received: notify %lu bytes", __FUNCTION__,packet->m_nBodySize);//处理元数据,暂时注释/*if (HandleMetadata(r, packet->m_body, packet->m_nBodySize))bHasMediaPacket = 1;break;*///RTMP消息类型ID=19,AMF0编码,忽略case 0x13:RTMP_Log(RTMP_LOGDEBUG, "%s, shared object, not supported, ignoring",__FUNCTION__);break;//RTMP消息类型ID=20,AMF0编码,命令消息//处理命令消息!case 0x14://----------------r->dlg->AppendCInfo("处理收到的数据。消息 命令 (AMF0编码) (typeID=20)。");//-----------------------------/* invoke */RTMP_Log(RTMP_LOGDEBUG, "%s, received: invoke %lu bytes", __FUNCTION__,packet->m_nBodySize);RTMP_LogPrintf("处理命令消息 (typeID=20,AMF0编码)\n");/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */if (HandleInvoke(r, packet->m_body, packet->m_nBodySize) == 1)bHasMediaPacket = 2;break;//RTMP消息类型ID=22case 0x16:{/* go through FLV packets and handle metadata packets */unsigned int pos = 0;uint32_t nTimeStamp = packet->m_nTimeStamp;while (pos + 11 < packet->m_nBodySize){uint32_t dataSize = AMF_DecodeInt24(packet->m_body + pos + 1);	/* size without header (11) and prevTagSize (4) */if (pos + 11 + dataSize + 4 > packet->m_nBodySize){RTMP_Log(RTMP_LOGWARNING, "Stream corrupt?!");break;}if (packet->m_body[pos] == 0x12){HandleMetadata(r, packet->m_body + pos + 11, dataSize);}else if (packet->m_body[pos] == 8 || packet->m_body[pos] == 9){nTimeStamp = AMF_DecodeInt24(packet->m_body + pos + 4);nTimeStamp |= (packet->m_body[pos + 7] << 24);}pos += (11 + dataSize + 4);}if (!r->m_pausing)r->m_mediaStamp = nTimeStamp;/* FLV tag(s) *//*RTMP_Log(RTMP_LOGDEBUG, "%s, received: FLV tag(s) %lu bytes", __FUNCTION__, packet.m_nBodySize); */bHasMediaPacket = 1;break;}default:RTMP_Log(RTMP_LOGDEBUG, "%s, unknown packet type received: 0x%02x", __FUNCTION__,packet->m_packetType);
#ifdef _DEBUGRTMP_LogHex(RTMP_LOGDEBUG, (const uint8_t *)packet->m_body, packet->m_nBodySize);
#endif}return bHasMediaPacket;
}


前文已经分析过当消息类型ID为0x14(20)的时候,即AMF0编码的命令消息的时候,会调用HandleInvoke()进行处理。

参考:RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)

这里就不再对这种类型ID的消息进行分析了,分析一下其他类型的消息,毕竟从发起一个RTMP连接到接收视音频数据这个过程中是要处理很多消息的。

参考:RTMP流媒体播放过程

下面我们按照消息ID从小到大的顺序,看看接收到的各种消息都是如何处理的。

消息类型ID是0x01的消息功能是“设置块(Chunk)大小”,处理函数是HandleChangeChunkSize(),可见函数内容很简单。

static void
HandleChangeChunkSize(RTMP *r, const RTMPPacket *packet)
{if (packet->m_nBodySize >= 4){r->m_inChunkSize = AMF_DecodeInt32(packet->m_body);RTMP_Log(RTMP_LOGDEBUG, "%s, received: chunk size change to %d", __FUNCTION__,r->m_inChunkSize);}
}


消息类型ID是0x03的消息功能是“致谢”,没有处理函数。

消息类型ID是0x04的消息功能是“用户控制(UserControl)”,处理函数是HandleCtrl(),这类的消息出现的频率非常高,函数体如下所示。具体用户控制消息的作用这里就不多说了,有相应的文档可以参考。

注:该函数中间有一段很长的英文注释,英语好的大神可以看一看

//处理用户控制(UserControl)消息。用户控制消息是服务器端发出的。
static void
HandleCtrl(RTMP *r, const RTMPPacket *packet)
{short nType = -1;unsigned int tmp;if (packet->m_body && packet->m_nBodySize >= 2)//事件类型(2B)nType = AMF_DecodeInt16(packet->m_body);RTMP_Log(RTMP_LOGDEBUG, "%s, received ctrl. type: %d, len: %d", __FUNCTION__, nType,packet->m_nBodySize);/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */if (packet->m_nBodySize >= 6){//不同事件类型做不同处理switch (nType){//流开始case 0://流IDtmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Stream Begin %d", __FUNCTION__, tmp);break;//流结束case 1://流IDtmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Stream EOF %d", __FUNCTION__, tmp);if (r->m_pausing == 1)r->m_pausing = 2;break;//流枯竭case 2://流IDtmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Stream Dry %d", __FUNCTION__, tmp);break;//是录制流case 4:tmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Stream IsRecorded %d", __FUNCTION__, tmp);break;//Ping客户端case 6:		/* server ping. reply with pong. */tmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Ping %d", __FUNCTION__, tmp);RTMP_SendCtrl(r, 0x07, tmp, 0);break;/* FMS 3.5 servers send the following two controls to let the client* know when the server has sent a complete buffer. I.e., when the* server has sent an amount of data equal to m_nBufferMS in duration.* The server meters its output so that data arrives at the client* in realtime and no faster.** The rtmpdump program tries to set m_nBufferMS as large as* possible, to force the server to send data as fast as possible.* In practice, the server appears to cap this at about 1 hour's* worth of data. After the server has sent a complete buffer, and* sends this BufferEmpty message, it will wait until the play* duration of that buffer has passed before sending a new buffer.* The BufferReady message will be sent when the new buffer starts.* (There is no BufferReady message for the very first buffer;* presumably the Stream Begin message is sufficient for that* purpose.)** If the network speed is much faster than the data bitrate, then* there may be long delays between the end of one buffer and the* start of the next.** Since usually the network allows data to be sent at* faster than realtime, and rtmpdump wants to download the data* as fast as possible, we use this RTMP_LF_BUFX hack: when we* get the BufferEmpty message, we send a Pause followed by an* Unpause. This causes the server to send the next buffer immediately* instead of waiting for the full duration to elapse. (That's* also the purpose of the ToggleStream function, which rtmpdump* calls if we get a read timeout.)** Media player apps don't need this hack since they are just* going to play the data in realtime anyway. It also doesn't work* for live streams since they obviously can only be sent in* realtime. And it's all moot if the network speed is actually* slower than the media bitrate.*/case 31:tmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Stream BufferEmpty %d", __FUNCTION__, tmp);if (!(r->Link.lFlags & RTMP_LF_BUFX))break;if (!r->m_pausing){r->m_pauseStamp = r->m_channelTimestamp[r->m_mediaChannel];RTMP_SendPause(r, TRUE, r->m_pauseStamp);r->m_pausing = 1;}else if (r->m_pausing == 2){RTMP_SendPause(r, FALSE, r->m_pauseStamp);r->m_pausing = 3;}break;case 32:tmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Stream BufferReady %d", __FUNCTION__, tmp);break;default:tmp = AMF_DecodeInt32(packet->m_body + 2);RTMP_Log(RTMP_LOGDEBUG, "%s, Stream xx %d", __FUNCTION__, tmp);break;}}if (nType == 0x1A){RTMP_Log(RTMP_LOGDEBUG, "%s, SWFVerification ping received: ", __FUNCTION__);if (packet->m_nBodySize > 2 && packet->m_body[2] > 0x01){RTMP_Log(RTMP_LOGERROR,"%s: SWFVerification Type %d request not supported! Patches welcome...",__FUNCTION__, packet->m_body[2]);}
#ifdef CRYPTO/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); *//* respond with HMAC SHA256 of decompressed SWF, key is the 30byte player key, also the last 30 bytes of the server handshake are applied */else if (r->Link.SWFSize){RTMP_SendCtrl(r, 0x1B, 0, 0);}else{RTMP_Log(RTMP_LOGERROR,"%s: Ignoring SWFVerification request, use --swfVfy!",__FUNCTION__);}
#elseRTMP_Log(RTMP_LOGERROR,"%s: Ignoring SWFVerification request, no CRYPTO support!",__FUNCTION__);
#endif}
}


消息类型ID是0x05的消息功能是“窗口致谢大小(Window Acknowledgement Size,翻译的真是挺别扭)”,处理函数是HandleServerBW()。在这里注意一下,该消息在Adobe官方公开的文档中叫“Window Acknowledgement Size”,但是在Adobe公开协议规范之前,破解RTMP协议的组织一直管该协议叫“ServerBW”,只是个称呼,倒是也无所谓~处理代码很简单:

static void
HandleServerBW(RTMP *r, const RTMPPacket *packet)
{r->m_nServerBW = AMF_DecodeInt32(packet->m_body);RTMP_Log(RTMP_LOGDEBUG, "%s: server BW = %d", __FUNCTION__, r->m_nServerBW);
}


消息类型ID是0x06的消息功能是“设置对等端带宽(Set Peer Bandwidth)”,处理函数是HandleClientBW()。与上一种消息一样,该消息在Adobe官方公开的文档中叫“Set Peer Bandwidth”,但是在Adobe公开协议规范之前,破解RTMP协议的组织一直管该协议叫“ClientBW”。处理函数也不复杂:

static void
HandleClientBW(RTMP *r, const RTMPPacket *packet)
{r->m_nClientBW = AMF_DecodeInt32(packet->m_body);if (packet->m_nBodySize > 4)r->m_nClientBW2 = packet->m_body[4];elser->m_nClientBW2 = -1;RTMP_Log(RTMP_LOGDEBUG, "%s: client BW = %d %d", __FUNCTION__, r->m_nClientBW,r->m_nClientBW2);
}


消息类型ID是0x08的消息用于传输音频数据,在这里不处理。

消息类型ID是0x09的消息用于传输音频数据,在这里不处理。

消息类型ID是0x0F-11的消息用于传输AMF3编码的命令。

消息类型ID是0x12-14的消息用于传输AMF0编码的命令。

注:消息类型ID是0x14的消息很重要,用于传输AMF0编码的命令,已经做过分析。


rtmpdump源代码(Linux):http://download.csdn.net/detail/leixiaohua1020/6376561

rtmpdump源代码(VC 2005 工程):http://download.csdn.net/detail/leixiaohua1020/6563163











转载于:https://my.oschina.net/leixiaohua1020/blog/302233

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

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

相关文章

Dapr 助力应用架构的可持续性

在文章亚马逊可持续软件工程实践[1] 有这么一段我们为什么要关注“可持续发展”&#xff1a;联合国于 2015 年制定了一个全球框架《巴黎协定》[2]&#xff0c;随后各缔约国纷纷制定了“碳中和”路径和目标&#xff0c;对地球环境的健康发展做出承诺。今年两会&#xff0c;中国也…

计算机无法创建新文件夹,无法创建文件,教您无法新建文件夹怎么办

在使用电脑的过程中&#xff0c;都遇到过电脑出现各种故障的情况&#xff0c;让不少的用户感到懊恼的时刻时有发生&#xff0c;造成非常大的不便&#xff0c;该怎么解决这个烦恼呢&#xff1f;下面&#xff0c;小编给大家分享无法新建文件夹的解决经验。相信在使用电脑是一定会…

我的技术回顾那些与ABP框架有关的故事-2018年

我的技术回顾那些与ABP框架有关的故事-2018年今天准备想写18年的&#xff0c;但是发现我从19年开始就在逐渐淡出社区&#xff0c;因为生活、工作的缘故吧。所以也没什么特别罗列的&#xff0c;就合并下吧。时间真的是可以磨平太多东西了&#xff0c;如果我不去整理资料的话&…

wpa_supplicant 无线网络配置

为什么80%的码农都做不了架构师&#xff1f;>>> 安装wpa_supplicant后&#xff0c;修改服务&#xff0c;编辑 /usr/share/dbus-1/system-services/fi.epitest.hostap.WPASupplicant.service 将下面的 [D-BUS Service] Namefi.epitest.hostap.WPASupplicant Exec/s…

Avalonia跨平台入门第六篇之Grid动态分割

前面玩耍了ListBox多选,今天在他的基础上对Grid进行动态分割;这个效果其实在Xamarin中已经实现过了;其实都没太大区别;直接看效果吧:在ListBox中选择了具体的布局后进行Grid布局分割:具体分割的方法和原来在移动端没啥区别:下一篇就是控件的拖放了;最终简单的效果先这样吧;以后…

使用ABBYY FineReader进行自动图像预处理

2019独角兽企业重金招聘Python工程师标准>>> 扫描图像和数码照片中常见的扭曲文本行、歪斜、噪声和其他缺陷可能会降低识别质量&#xff0c;ABBYY FineReader可自动消除这些缺陷&#xff0c;也允许手动消除。 ABBYY FineReader有几个图像预处理功能&#xff0c;如果…

Avalonia跨平台入门第五篇之ListBox多选

前面我也提前预告了今天要实现的效果;不过中间被卡了一下;今天赶紧去弥补上次要做的效果,其实在WPF效果中已经实现过了,看效果吧:ListBox的前台布局代码:使用的附加属性和WPF好相似:子项模板(绑定写法简化了好多):多值转换器(少了一个ConvertBack):最终简单的效果先这样吧;以后…

Android之android.os.NewWorkOnMainThreadException解决办法

1、问题 用别人提供的的sdk的Demo出现android.os.NewWorkOnMainThreadException 2、解决办法 网络请求开启一个线程请求或者开启异步任务都行 3、总结 1、 之前就看到过这种android.os.NewWorkOnMainThreadException异常&#xff0c;时间很久了&#xff0c;自己也没反映过来&a…

金蝶K/3 WISE 12.3订单跟踪SQL报表

金蝶K3一直缺少完整的跟踪报表&#xff0c;所以我们开发了一张完整的跟踪报表&#xff0c;通过查询分析工具直接生成。代码&#xff08;WIN2008 R2SQL 2008 R2环境&#xff0c;K3 WISE 12.3&#xff09;&#xff1a;select t1.fname1 客户,t1.fname2 业务员,t1.f_102 款号,t1.f…

开源绘画应用 Pinta 已移植到GTK 3和.NET 6

Pinta [1]是一款开源绘画应用&#xff0c;适用于 Linux、Windows 和 macOS。你可以用它来进行自由手绘/素描。你也可以用它在现有的图片上添加箭头、方框、文字等。年初发布了 Pinta 2.0.2 版本&#xff0c;这是我看到的第一款桌面的dotnet6 和 gtksharp的 应用。Pinta是一个小…

Druid源码分析系列1:dataSource.init()的准备工作

2019独角兽企业重金招聘Python工程师标准>>> 本节&#xff0c;讲解 dataSource.init(); 打断点在 stop in com.alibaba.druid.pool.DruidDataSource.init 好&#xff0c;开始研究代码 public void init() throws SQLException {// 首先确定没有initedif (inited) {r…

软件测试推荐专业,软件测试专业老师推荐信

尊敬的领导&#xff1a;您好&#xff01;首先感谢您在百忙之中抽出时间来阅读我学生XX的推荐信&#xff01;该生是XX大学软件测试专业应届毕业生&#xff0c;自进入XX大学以来&#xff0c;凭借自身扎实的基础和顽强拼搏的奋斗精神&#xff0c;经过几年不断的学习&#xff0c;在…

[转]Android中pendingIntent的深入理解

转自;here pendingIntent字面意义&#xff1a;等待的&#xff0c;未决定的Intent。要得到一个pendingIntent对象&#xff0c;使用方法类的静态方法 getActivity(Context, int, Intent, int),getBroadcast(Context, int, Intent, int),getService(Context, int, Intent, int) 分…

叮,您有一份ML.NET 速查手册请查收!

点击上方蓝字关注我们&#xff08;本文阅读时间&#xff1a;7分钟&#xff09;本篇文章简要介绍 ML.NET 背景和面向 .NET 开发的特色功能&#xff0c;以及典型的机器学习编码示例&#xff0c;并分享自己整理的 ML.NET API 速查手册。微软MVP实验室研究员项斌微软全球最有价值专…

老师计算机传帮带工作总结,传帮带工作总结范文

传帮带工作总结范文一段时间的工作在不知不觉间已经告一段落了&#xff0c;回首这段不平凡的时间&#xff0c;有欢笑&#xff0c;有泪水&#xff0c;有成长&#xff0c;有不足&#xff0c;让我们好好总结下&#xff0c;并记录在工作总结里。那么如何把工作总结写出新花样呢&…

[Usaco2007 Demo][BZOJ1628] City skyline

1628: [Usaco2007 Demo]City skyline Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 320 Solved: 260[Submit][Status][Discuss]Description Input 第一行给出N&#xff0c;W第二行到第N1行:每行给出二个整数x,y&#xff0c;输入的x严格递增&#xff0c;并且第一个x总是1Out…

负载均衡(LB)集群 dr

LB、LVS介绍LB集群是load balance 集群的简写&#xff0c;翻译成中文就是负载均衡集群 LVS是一个实现负载均衡集群的开源软件项目 LVS架构从逻辑上可分为调度层(Director)、server集群层(Real server)和共享存储层LVS可分为三种工作模式: NAT(调度器将请求的目标ip即vip地址改为…

ASP.NET Core启动地址配置方法及优先级顺序 | .NET 6 版本

前言上次&#xff0c;我们讨论了如何通过配置或代码方式修改启动地址&#xff1a;《ASP.NET Core启动地址配置方法及优先级顺序》。不过是基于 .NET 5 版本的。由于 .NET 6 使用了最小 WEB API, 配置方式已经部分发生了变化。设置方法1. applicationUrl 属性launchSettings.jso…

Avalonia跨平台入门第七篇之RadioButton的模板

前面其实已经玩耍过单选按钮,只不过一直好意思分享出来;今天终于可以正大光明的分享出来了,直接看效果吧:第一次使用然后的傻傻的版本(根据单选按钮的选中状态来切换二个图片);真的好Low:样式写法和WPF没太大区别:类似WPF中的触发器,使用了附加属性:前台具体使用方式:最终简单的…

更强的压缩比!PostgreSQL开始支持Zstd

文 | 局长出品 | OSC开源社区&#xff08;ID&#xff1a;oschina2013&#xff09;PostgreSQL 现已通过其 TOAST 存储技术提供压缩支持&#xff0c;并且在过去的一年里构建了 LZ4 压缩支持——用于压缩 WAL、备份压缩以及其他用途&#xff0c;现在 PostgreSQL 开发者正准备通过 …