一个简单的RTMP服务器实现 --- RTMP复杂握手(Complex Handshake)
PS:要转载请注明出处,本人版权所有。
PS: 这个只是基于《我自己》的理解,
如果和你的原则及想法相冲突,请谅解,勿喷。
前置说明
本文作为本人csdn blog的主站的备份。(BlogID=078)
本文发布于 2019-01-09 16:57:55,现用MarkDown+图床做备份更新。blog原图已丢失,使用csdn所存的图进行更新。(BlogID=078)
环境说明
无
背景
参考前置文章:《一个简单的RTMP服务器实现 --- RTMP与H264》https://blog.csdn.net/u011728480/article/details/85770696
前置知识
《一个简单的RTMP服务器实现 --- RTMP与H264》:https://blog.csdn.net/u011728480/article/details/85770696
《一个简单的RTMP服务器实现 --- RTMP与FLV》:https://blog.csdn.net/u011728480/article/details/85780974
《一个简单的RTMP服务器实现 --- RTMP实现要点》:https://blog.csdn.net/u011728480/article/details/86010851
为啥需要RTMP复杂握手
曾经的我,那么的天真,我按照官方文档以及官方文档的相关资料进行了RTMP的服务器的实现。奈何,前端使用的是Video.js,需要调用flash进行rtmp流播放,但是就是播放不起来。
在许多次尝试后,flash还是不能够播放我传输的rtmp流。我就开始了怀疑人生了。因为vlc和plotplayer都是能播放的,但是flash不能够播放。于是乎,我就猜想是flash播放器有什么特殊的毛病,需要我去治一治。
又过了许久,我搜遍了网络,我第一次看见这个问题的可能答案是在某论坛的一篇过期的帖子里面。
原因是flash播放器需要特殊的握手方式才能够播放h264+aac的rtmp流(具体表现为:播放器连接上了rtmp服务器,但是没有任何图像和声音)。于是我知道了,我需要改变自己的服务器的握手流程。
经过前期在RTMP中的折腾,我知道我要找的东西是RTMP复杂握手流程。于是就去网上找了找。经过查找,发现了一篇不错的文章:https://blog.csdn.net/win_lin/article/details/13006803 (这篇文章貌似是srs的作者写的,里面就有我现在这个问题的答案,这里及其感谢大佬的文章。)
本文内容是上文提到的大佬的文章的一个补充,希望大家结合着一起看,实现自己的RTMP服务器。
RTMP复杂握手
下图为S1,C1。S2,C2的结构图
上图中一眼就可以看懂相关包的结构。其中需要注意的是一个c1_s1-joined的结构。他是把s1,c1中的digest的32bytes去除后,然后把剩下的内容拼接在一起形成的一个数组。
下面这两个数组是一个常量,在后面的计算会用到。
private static final byte[] FP_KEY = {
(byte) 0x47, (byte) 0x65, (byte) 0x6E, (byte) 0x75, (byte) 0x69, (byte) 0x6E, (byte) 0x65, (byte) 0x20,
(byte) 0x41, (byte) 0x64, (byte) 0x6F, (byte) 0x62, (byte) 0x65, (byte) 0x20, (byte) 0x46, (byte) 0x6C,
(byte) 0x61, (byte) 0x73, (byte) 0x68, (byte) 0x20, (byte) 0x50, (byte) 0x6C, (byte) 0x61, (byte) 0x79,
(byte) 0x65, (byte) 0x72, (byte) 0x20, (byte) 0x30, (byte) 0x30, (byte) 0x31, // Genuine Adobe Flash Player 001
(byte) 0xF0, (byte) 0xEE, (byte) 0xC2, (byte) 0x4A, (byte) 0x80, (byte) 0x68, (byte) 0xBE, (byte) 0xE8,
(byte) 0x2E, (byte) 0x00, (byte) 0xD0, (byte) 0xD1, (byte) 0x02, (byte) 0x9E, (byte) 0x7E, (byte) 0x57,
(byte) 0x6E, (byte) 0xEC, (byte) 0x5D, (byte) 0x2D, (byte) 0x29, (byte) 0x80, (byte) 0x6F, (byte) 0xAB,
(byte) 0x93, (byte) 0xB8, (byte) 0xE6, (byte) 0x36, (byte) 0xCF, (byte) 0xEB, (byte) 0x31, (byte) 0xAE};
private static final byte FMSKey[] = {
(byte)0x47, (byte)0x65, (byte)0x6e, (byte)0x75, (byte)0x69, (byte)0x6e, (byte)0x65, (byte)0x20,
(byte)0x41, (byte)0x64, (byte)0x6f, (byte)0x62, (byte)0x65, (byte)0x20, (byte)0x46, (byte)0x6c,
(byte)0x61, (byte)0x73, (byte)0x68, (byte)0x20, (byte)0x4d, (byte)0x65, (byte)0x64, (byte)0x69,
(byte)0x61, (byte)0x20, (byte)0x53, (byte)0x65, (byte)0x72, (byte)0x76, (byte)0x65, (byte)0x72,
(byte)0x20, (byte)0x30, (byte)0x30, (byte)0x31, // Genuine Adobe Flash Media Server 001
(byte)0xf0, (byte)0xee, (byte)0xc2, (byte)0x4a, (byte)0x80, (byte)0x68, (byte)0xbe, (byte)0xe8,
(byte)0x2e, (byte)0x00, (byte)0xd0, (byte)0xd1, (byte)0x02, (byte)0x9e, (byte)0x7e, (byte)0x57,
(byte)0x6e, (byte)0xec, (byte)0x5d, (byte)0x2d, (byte)0x29, (byte)0x80, (byte)0x6f, (byte)0xab,
(byte)0x93, (byte)0xb8, (byte)0xe6, (byte)0x36, (byte)0xcf, (byte)0xeb, (byte)0x31, (byte)0xae
};
C1,S1 伪代码实现
下面会列出c1的伪代码实现
unsigned char * const C1 = new unsigned char[1536];//s1,c1,s2,c2大小都是1536字节
Random(C1);//把C1每个字节随机化
C1[0-3] = current_time();
C1[4-7] = {0x80, 0x00, 0x07, 0x02};//这个是定值
//自行决定使用哪种schema结构,定义c1_schema_type
int key_offset = Random(0-632);//在取值范围[0-632) (764-4-128,观察key结构可知)中随机一个值赋值给key_offset(key-data的相对位置).
int digest_offset = Random(0-728);//在取值范围[0-728)(764-32-4,观察digest结构可知)中随机一个值赋值给digest_offset(digest-data的相对位置).
int absolute_key_offset = CalAbsoluteKeyOffset(key_offset, c1_schema_type );//根据c1_schema_type,按照相同类型获取key结构中,key-data在数组中的绝对位置
int absolute_digest_offset = CalAbsoluteDigestOffset(digest_offset , c1_schema_type );//根据c1_schema_type,按照相同类型获取digest结构中,digest-data在数组中的绝对位置
SetC1KeyOffset(key_offset, c1_schema_type);//把keyoffset设置到C1中,注意,key结构中这里的offset 的4个字节的值必须加起来等于key_offset的值。
SetC1DigestOffset(digest_offset , c1_schema_type);//把digest_offset 设置到C1中,注意,digest结构中这里的offset 的4个字节的值必须加起来等于digest_offset 的值。
c1_s1_joined_array = GetC1JoinedArray(absolute_digest_offset);//这里是根据绝对digest偏移,然后去掉digest32个字节,然后返回digest的前后组合得到的数组结果(1536-32)
c1_digest = HMACsha256(c1_s1_joined_array , FP_KEY, 30);//调用openssl中的Hmacsha256算法计算秘钥
SetC1Digest(c1_digest , c1_schema_type);//把计算出来的digest设置到c1中去。然后c1构造完成,即可发送给服务器
注意:C1中的key值就是我们的随机值即可。严格按照顺序计算,设置时间,版本,key_offset,digest_offset,然后才能够计算c1_s1_joined_array,最后得到c1_digest。
下面会列出s1的伪代码实现
unsigned char * const S1 = new unsigned char[1536];//s1,c1,s2,c2大小都是1536字节
Random(S1);//把S1每个字节随机化
S1[0-3] = current_time();
S1[4-7] = {0x04, 0x05, 0x00, 0x01};//这个是定值版本。
c1_schema_type = GetC1SchemaType();//获取C1的schema type。得出是key-digest structure 还是 digest-key structure
int key_offset = Random(0-632);//在取值范围[0-632) (764-4-128,观察key结构可知)中随机一个值赋值给key_offset(key-data的相对位置).
int digest_offset = Random(0-728);//在取值范围[0-728)(764-32-4,观察digest结构可知)中随机一个值赋值给digest_offset(digest-data的相对位置).
int absolute_key_offset = CalAbsoluteKeyOffset(key_offset, c1_schema_type );//根据c1_schema_type,按照相同类型获取key结构中,key-data在数组中的绝对位置
int absolute_digest_offset = CalAbsoluteDigestOffset(digest_offset , c1_schema_type );//根据c1_schema_type,按照相同类型获取digest结构中,digest-data在数组中的绝对位置
SetS1KeyOffset(key_offset, c1_schema_type);//把keyoffset设置到S1中,注意,key结构中这里的offset 的4个字节的值必须加起来等于key_offset的值。
SetS1DigestOffset(digest_offset , c1_schema_type);//把digest_offset 设置到S1中,注意,digest结构中这里的offset 的4个字节的值必须加起来等于digest_offset 的值。
unsigned char * const c1_key = GetC1Key();//获取C1的key
//这里参考srs开源框架的实现,调用openssl的DH算法中相关内容。
//根据上文那篇srs作者的文章的评论部分内容,这里相当于是要算出一个128公钥作为s1的key就可以了。不需要得到共享秘钥。
unsigned char * const s1_key = GetDHPublicKey();
SetS1Key(s1_key , c1_schema_type);//设置s1 key 到s1数组中,方便计算c1_s1-joined结构。
c1_s1_joined_array = GetS1JoinedArray(absolute_digest_offset);//这里是根据绝对digest偏移,然后去掉digest32个字节,然后返回digest的前后组合得到的数组结果(1536-32)
s1_digest = HMACsha256(c1_s1_joined_array , FMSKey, 36);//调用openssl中的Hmacsha256算法计算秘钥
SetS1Digest(s1_digest , c1_schema_type);//把计算出来的digest设置到s1中去。然后s1构造完成,即可发送给客户端
这里必须严格按照流程走:
把1536字节buffer随机化,设置时间,版本信息。设置s1_key,s1_key_offset信息,然后设置s1_digest_offset信息。以上都做完以后,计算出c1_s1_joined_array 。根据c1_s1_joined_array ,FMSKey计算出s1_digest,把s1_digest设置到s1buffer中。到了这一步,即可把s1发送出去了。
C2 S2的伪代码实现
C2伪代码实现
unsigned char * const C2 = new unsigned char[1536];//s1,c1,s2,c2大小都是1536字节
Random(C2);//把C2每个字节随机化
s1_digest = GetS1Digest();//得到s1的digest
tmp_key = HMACsha256(s1_digest, FPKey, 62);
c2_random_data = GetC2RandomData();//得到c2[0~1503]
c2_digest = HMACsha256(c2_random_data, tmp_key, 32);
SetC2Digest(c2_digest);
就是连续计算秘钥得到C2 key即可
S2伪代码实现
unsigned char * const S2 = new unsigned char[1536];//s1,c1,s2,c2大小都是1536字节
Random(S2);//把S2每个字节随机化
c1_digest = GetC1Digest();//得到c1的digest
tmp_key = HMACsha256(c1_digest, FMSKey, 68);
s2_random_data = GetS2RandomData();//得到s2[0~1503]
s2_digest = HMACsha256(s2_random_data, tmp_key, 32);
SetS2Digest(s2_digest);
后记
总结:
- 就是按照别人推断出来的东西进行代码实现,同时明确一下一些字段定义。
- 按照大佬文中评论,验证了一下,public_key和share_key对RTMP没有影响。
本系列文末总结
- 按照一个文档,实现了相关功能是一件有成就感的事情。
- 现在已经是2019年了,你要相信,你要做的事情,极有可能是别人做过的,可以去多查查资料以备参考。
- 静下心来做事,我发现有些时候我很浮躁。同时敢怀疑别人,并且思索怀疑的理由。
参考文献
- 无
打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)
PS: 请尊重原创,不喜勿喷。
PS: 要转载请注明出处,本人版权所有。
PS: 有问题请留言,看到后我会第一时间回复。
一个简单的RTMP服务器实现 --- RTMP复杂握手(Complex Handshake)的更多相关文章
- 自己动手模拟开发一个简单的Web服务器
开篇:每当我们将开发好的ASP.NET网站部署到IIS服务器中,在浏览器正常浏览页面时,可曾想过Web服务器是怎么工作的,其原理是什么?“纸上得来终觉浅,绝知此事要躬行”,于是我们自己模拟一个简单的W ...
- 一个简单的web服务器
写在前面 新的一年了,新的开始,打算重新看一遍asp.net本质论这本书,再重新认识一下,查漏补缺,认认真真的过一遍. 一个简单的web服务器 首先需要引入命名空间: System.Net,关于网络编 ...
- [置顶] 在Ubuntu下实现一个简单的Web服务器
要求: 实现一个简单的Web服务器,当服务器启动时要读取配置文件的路径.如果浏览器请求的文件是可执行的则称为CGI程序,服务器并不是将这个文件发给浏览器,而是在服务器端执行这个程序,将它的标准输出发给 ...
- Tomcat剖析(二):一个简单的Servlet服务器
Tomcat剖析(二):一个简单的Servlet服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三) ...
- Tomcat剖析(一):一个简单的Web服务器
Tomcat剖析(一):一个简单的Web服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器 ...
- 自己模拟的一个简单的web服务器
首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...
- java实现一个简单的Web服务器
注:本段内容来源于<JAVA 实现 简单的 HTTP服务器> 1. HTTP所有状态码 状态码 状态码英文名称 中文描述 100 Continue 继续.客户端应继续其请求 101 Swi ...
- 响应式编程笔记三:一个简单的HTTP服务器
# 响应式编程笔记三:一个简单的HTTP服务器 本文我们将继续前面的学习,但将更多的注意力放在用例和编写实际能用的代码上面,而非基本的APIs学习. 我们会看到Reactive是一个有用的抽象 - 对 ...
- 转:【专题十二】实现一个简单的FTP服务器
引言: 休息一个国庆节后好久没有更新文章了,主要是刚开始休息完心态还没有调整过来的, 现在差不多进入状态了, 所以继续和大家分享下网络编程的知识,在本专题中将和大家分享如何自己实现一个简单的FTP服务 ...
- 使用gitblit搭建一个简单的局域网服务器
使用gitblit搭建一个简单的局域网服务器 1.使用背景 现在很多使用github管理代码,但是github需要互联网的支持,而且私有的git库需要收费.有一些项目的代码不能外泄,所以,搭建一个局域 ...
随机推荐
- Java 数字 默认是 Integer类型的问题,System.currentTimeMillis() + (180 * 24 * 60 * 60 * 1000)的问题,剖析、Long + Integer的问题
最终结论: (180 * 24 * 60 * 60) 这种计算表达式在 Java中是默认以 Integer类型来的,若不超过 Integer的最大值则没有问题,若超过则必须用 (180 * 24 * ...
- 《ASP.NET Core 微服务实战》-- 读书笔记(第8章)
第 8 章 服务发现 面对大量服务,为了简化配置和管理工作,我们需要了解"服务发现"概念 回顾云原生特性 配置外置 将 URL 和登录凭证移到配置文件和 C# 代码之外,放到环境变 ...
- CentOS7环境源码安装python3.9
操作系统 : CentOS7.6.1810_x64 Python 版本 : 3.9.12 1.获取源代码 python官方网址: https://www.python.org/ 源码下载地址: 或者直 ...
- NC50959 To the Max
题目链接 题目 题目描述 Given a two-dimensional array of positive and negative integers, a sub-rectangle is any ...
- NC16850 [NOI1998]免费馅饼
题目链接 题目 题目描述 SERKOI最新推出了一种叫做"免费馅饼"的游戏:游戏在一个舞台上进行.舞台的宽度为W格,天幕的高度为H格,游戏者占一格.开始时游戏者站在舞台的正中央,手 ...
- SATA 中ATA与AHCI的区别
SATA中ATA和AHCI有什么区别? 1.ACHI是针对SATA2设计的,可以卡其NCQ功能,表面上没有速度的优势,但是因为算法不同,可以有效的保护硬盘.ATA 是硬件模拟IDE的一种方法.表面 ...
- 阿里OSS+PicGo+Typora图床设置使用
1.首先下载Typroa和PicGo Typroa地址: 链接:https://pan.baidu.com/s/1M6_5o1PZyaGqOhSb4yvtmA 提取码:a1aj PicGo地址: 链接 ...
- Jsp+Servlet实现文件上传下载(四)--下载文件
接着上一篇讲: Jsp+Servlet实现文件上传下载(三)--删除上传文件 点击打开链接 本章来实现一下下载文件功能,同时优化了一下上一章中的代码. 废话少说,上代码 ------------ ...
- 从 vs 的 rc 文件中获取版本号
更新项目版本号时,需要与 rc 文件的 version 同步,比较方便的方法是直接从 rc 文件中获取版本号,并应用到程序中 // 删除日志检查 bool GetVersion() { // get ...
- golang常用库包:redis操作库go-redis使用(01)-Redis数据类型简介和连接Redis的几种方式
第一篇:go-redis使用,介绍Redis基本数据结构和其他特性,以及 go-redis 连接到Redis(本篇) https://www.cnblogs.com/jiujuan/p/1720716 ...