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);

后记


  总结:

  1. 就是按照别人推断出来的东西进行代码实现,同时明确一下一些字段定义。
  2. 按照大佬文中评论,验证了一下,public_key和share_key对RTMP没有影响。

本系列文末总结

  1. 按照一个文档,实现了相关功能是一件有成就感的事情。
  2. 现在已经是2019年了,你要相信,你要做的事情,极有可能是别人做过的,可以去多查查资料以备参考。
  3. 静下心来做事,我发现有些时候我很浮躁。同时敢怀疑别人,并且思索怀疑的理由。

参考文献


打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)

PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。

一个简单的RTMP服务器实现 --- RTMP复杂握手(Complex Handshake)的更多相关文章

  1. 自己动手模拟开发一个简单的Web服务器

    开篇:每当我们将开发好的ASP.NET网站部署到IIS服务器中,在浏览器正常浏览页面时,可曾想过Web服务器是怎么工作的,其原理是什么?“纸上得来终觉浅,绝知此事要躬行”,于是我们自己模拟一个简单的W ...

  2. 一个简单的web服务器

    写在前面 新的一年了,新的开始,打算重新看一遍asp.net本质论这本书,再重新认识一下,查漏补缺,认认真真的过一遍. 一个简单的web服务器 首先需要引入命名空间: System.Net,关于网络编 ...

  3. [置顶] 在Ubuntu下实现一个简单的Web服务器

    要求: 实现一个简单的Web服务器,当服务器启动时要读取配置文件的路径.如果浏览器请求的文件是可执行的则称为CGI程序,服务器并不是将这个文件发给浏览器,而是在服务器端执行这个程序,将它的标准输出发给 ...

  4. Tomcat剖析(二):一个简单的Servlet服务器

    Tomcat剖析(二):一个简单的Servlet服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三) ...

  5. Tomcat剖析(一):一个简单的Web服务器

    Tomcat剖析(一):一个简单的Web服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器 ...

  6. 自己模拟的一个简单的web服务器

    首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...

  7. java实现一个简单的Web服务器

    注:本段内容来源于<JAVA 实现 简单的 HTTP服务器> 1. HTTP所有状态码 状态码 状态码英文名称 中文描述 100 Continue 继续.客户端应继续其请求 101 Swi ...

  8. 响应式编程笔记三:一个简单的HTTP服务器

    # 响应式编程笔记三:一个简单的HTTP服务器 本文我们将继续前面的学习,但将更多的注意力放在用例和编写实际能用的代码上面,而非基本的APIs学习. 我们会看到Reactive是一个有用的抽象 - 对 ...

  9. 转:【专题十二】实现一个简单的FTP服务器

    引言: 休息一个国庆节后好久没有更新文章了,主要是刚开始休息完心态还没有调整过来的, 现在差不多进入状态了, 所以继续和大家分享下网络编程的知识,在本专题中将和大家分享如何自己实现一个简单的FTP服务 ...

  10. 使用gitblit搭建一个简单的局域网服务器

    使用gitblit搭建一个简单的局域网服务器 1.使用背景 现在很多使用github管理代码,但是github需要互联网的支持,而且私有的git库需要收费.有一些项目的代码不能外泄,所以,搭建一个局域 ...

随机推荐

  1. WebAssembly核心编程[3]: Module 与 Instance

    WebAssembly程序总是以模块来组织,模块是基本的部署.加载和编译单元.在JavaScript编程接口中,模块通过WebAssembly.Module类型表示.WebAssembly.Modul ...

  2. 执行orachk检查数据库环境

    Exadata环境巡检需要执行专有的exachk,而普通Oracle环境可以通过执行orachk来检查集群和数据库相关健康状况. 1.使用orachk检查健康状态 使用root用户执行,期间可能需要多 ...

  3. Windows、MacOs上 gif 录像软件 LICEcap

    LICEcap 官网:https://www.cockos.com/licecap/ 适用于 windows macos 我自己保持的一版,下载地址:https://files-cdn.cnblogs ...

  4. 虚拟机 VMware Workstation 16 PRO 的网络配置

    原文地址: https://blog.csdn.net/weixin_41905135/article/details/123858658 (一)VMware编辑虚拟网络 ​ 默认情况下,VMware ...

  5. Linux IOS镜像中查看Kernel 版本号

    开ISO镜像,到rpm包的目录里面去找到kernel-********.RPM这个包,中间的星号那一段就是内核版本

  6. lombok-ex 编译时注解框架,性能完爆 AOP

    lombok-ex lombok-ex 是一款类似于 lombok 的编译时注解框架. 主要补充一些 lombok 没有实现,且自己会用到的常见工具. 编译时注解性能无任何损失,一个注解搞定一切,无三 ...

  7. SpringBoot 2.6 和 JUnit 5 的测试用例注解和排序方式

    JUnit5 的测试注解 在JUnit5中, 不再使用 @RunWith 注解, 改为使用 @ExtendWith(SpringExtension.class) @ExtendWith(SpringE ...

  8. 【OpenGL ES】绘制立方体

    1 前言 ​ 本文主要介绍使用 OpenGL ES 绘制立方体,读者如果对 OpenGL ES 不太熟悉,请回顾以下内容: 绘制三角形 绘制彩色三角形 绘制正方形 绘制圆形 ​ 在绘制立方体的过程中, ...

  9. centos7创建MySQL自动备份脚本

    说明 最近需要给wordpress站点搞一个定时备份mysql数据库,所以记录一下. 操作步骤 1.创建备份脚本 这一步最重要,创建目录:/home/wpblog_backup,然后在目录下创建she ...

  10. ORA-12514问题解决

    版本:11.2.0.1.0 - 64bit 本机安装Oracle后链接测试发现以下情况: sqlplus scott/tiger 正常登陆 sqlplus scott/tiger@orcl  登陆失败 ...