深入理解rtmp(三)之手把手实现握手协议
RTMP是基于TCP协议的应用层协议,默认通信端口1935.实现握手协议前先了解一下rtmp握手协议吧!!!
握手过程
要建立一个有效的RTMP Connection链接,首先要“握手”:客户端要向服务器发送C0,C1,C2(按序)三个chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进行有效的信息传输。RTMP协议本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这几点如下:
- 客户端要等收到S1之后才能发送C2
- 客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据)
- 服务端要等到收到C0之后发送S1
- 服务端必须等到收到C1之后才能发送S2
- 服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据)
用图形可以表示为:
+-------------+ +-------------+
| Client | TCP/IP Network | Server |
+-------------+ | +-------------+
| | |
Uninitialized | Uninitialized
| C0 | |
|------------------->| C0 |
| |-------------------->|
| C1 | |
|------------------->| S0 |
| |<--------------------|
| | S1 |
Version sent |<--------------------|
| S0 | |
|<-------------------| |
| S1 | |
|<-------------------| Version sent
| | C1 |
| |-------------------->|
| C2 | |
|------------------->| S2 |
| |<--------------------|
Ack sent | Ack Sent
| S2 | |
|<-------------------| |
| | C2 |
| |-------------------->|
Handshake Done | Handshake Done
| | |
Pictorial Representation of Handshake
总结一下:
- 握手开始于客户端发送C0、C1块。服务器收到C0或C1后发送S0和S1。
- 当客户端收齐S0和S1后,开始发送C2。当服务器收齐C0和C1后,开始发送S2。
- 当客户端和服务器分别收到S2和C2后,握手完成。
注意事项: 在实际工程应用中,一般是客户端先将C0, C1块同时发出,服务器在收到C1 之后同时将S0, S1, S2发给客户端。S2的内容就是收到的C1块的内容。之后客户端收到S1块,并原样返回给服务器,简单握手完成。按照RTMP协议个要求,客户端需要校验C1块的内容和S2块的内容是否相同,相同的话才彻底完成握手过程,实际编写程序用一般都不去做校验。
RTMP握手的这个过程就是完成了两件事:
- 校验客户端和服务器端RTMP协议版本号
- 发了一堆随机数据,校验网络状况。
握手包格式
简单握手
C0和S0:1个字节,包含了RTMP版本, 当前RTMP协议的版本为 3
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
| version |
+-+-+-+-+-+-+-+-+
C0 and S0 bits
C1和S1:4字节时间戳,4字节的0,1528字节的随机数
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| zero (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random bytes |
| (cont) |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
C1 and S1 bits
- C1/S1 长度为 1536B。主要目的是确保握手的唯一性。
- 格式为 time + zero + random
- time 发送时间戳,长度 4 byte
- zero 保留值 0,长度 4 byte
- random 随机值,长度 1528 byte,保证此次握手的唯一性,确定握手的对象
C2和S2:4字节时间戳,4字节从对端读到的时间戳,1528字节随机数
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time2 (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random echo |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random echo |
| (cont) |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
C2 and S2 bits
- C2/S2 的长度也是 1536B。相当于就是 S1/C1 的响应值,对应 C1/S1 的 Copy 值,在于字段有点区别
- time, C2/S2 发送的时间戳,长度 4 byte
- time2, S1/C1 发送的时间戳,长度 4 byte
- random,S1/C1 发送的随机数,长度为 1528B
携带上内容的流程图:
复杂握手
介绍复杂模式前,先介绍一个哈希签名算法,即hmac-sha256算法。复杂模式会使用它做一些签名运算和验证。
简单来说,这个算法的输入为一个key(长度可以为任意)和一个input字符串(长度可以为任意),经过hmac-sha256运算后得到一个32字节的签名串。
key和input固定时,hmac-sha256运算结果也是固定唯一的。
相对于简单握手,复杂握手增加了严格的验证,主要是 random 字段上进行更细化的划分
1528Bytes随机数的部分平均分成两部分,一部分764Bytes存储public key(公共密钥),另一部分764Bytes存储digest(密文,32字节)。
c0
固定为0x03
c1
格式如下:
| 4字节时间戳time | 4字节模式串 | 1528字节复杂二进制串 |
time字段参照简单模式下time的说明。
4字节模式串, 使用的是[0x0C, 0x00, 0x0D, 0x0E]。
1528字节复杂二进制串生成规则如下:
步骤一,将1528字节复杂二进制串进行随机化处理。
步骤二,在1528字节随机二进制串中写入32字节的digest签名。
digest的位置
先说明digest的位置如何确定。digest的位置可以在前半部分,也可以在后半部分。
digest在前半部分
当digest在前半部分时,digest的位置信息(以下简称offset)保存在前半部分的起始位置。
c1格式展开如下:
| 4字节time | 4字节模式串 | 4字节offset | left[...] | 32字节digest | right[...] | 后半部分764字节 |
offset = (c1[8] + c1[9] + c1[10] + c1[11]) % 728 + 12
几点说明
- 计算出的offset是相对于整个c1的起始位置而言的。
- 为什么要取余728呢,因为前半部分的764字节要减去offset字段的4字节,再减去digest的32字节。
- 为什么要加12呢,是因为要跳过4字节time+4字节模式串+4字节offset。
- offset的取值范围为[12,740)。
- 当offset=12时, left 部分就不存在,当offset=739时, right 部分就不存在。
digest在后半部分
当digest在后半部分时,offset保存在后半部分的起始位置。
c1格式展开如下:
| 4字节time | 4字节模式串 | 前半部分764字节 | 4字节offset | left[...] | 32字节digest | right[...] |
offset = (c1[8+764] + c1[8+764+1] + c1[8+764+2] + c1[8+764+3]) % 728 + 8 + 764 + 4
几点说明:
- 计算出的offset依赖是相对于c1的其实位置而言的。
- 为什么要取余728呢,因为后半部分的764字节要减去offset字段的4字节,再减去digest的32字节。
- 为什么加8加764加4呢,是因为要跳过4字节time+4字节模式串+前半部分764字节+4字节offset。
- offset的取值范围为[776,1504)。
- 当offset=776时, left 部分就不存在,当offset=1503时, right 部分就不存在。
digest如何生成
说完digest的位置,再说digest如何生成。
即将c1 digest左边部分拼接上c1 digest右边部分(如果右边部分存在的话)作为hmac-sha256的input(整个大小是1536-32),以下大小为30字节固定key作为hmac-sha256的key,进过hmac-sha256计算得出32字节的digest填入c1中digest字段中。
'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
'F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ',
'0', '0', '1',
服务端在收到c1后,首先通过c1中的模式串,初步判断是否为复杂模式,如果是复杂模式,则通过c1重新digest,看计算得出的digest和c1中的包含的digest字段是否相同来确定握手是否为复杂模式。
注意,由于服务端无法直接得知客户端是将digest放在前半部分还是后半部分,所以服务端只能先验证其中一种,如果验证失败,再验证另外一种,如果都失败了,就考虑回退使用简单模式和客户端继续握手。
s0
固定为0x03
s1
s1的构造方法和c1相同。
只不过将模式串换成了 [0x0D, 0x0E, 0x0A, 0x0D]。
并且将hmac-sha256的key换成了如下36字节固定key
'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
'F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ',
'S', 'e', 'r', 'v', 'e', 'r', ' ',
'0', '0', '1',
s2
格式如下:
| 4字节时间戳time | 4字节time2 | 1528字节随机二进制串 |
其中time和time2字段参考简单模式下s2的说明。
1528字节随机二进制串中也需要填入digest。
将32字节digest直接填入s2的尾部,也即没有设置相应的offset ,digest的计算方法是,使用digest的左边部分作为hmac-sha256的input(大小是1536-32), 使用c1中的digest作为hmac-sha256的key ,通过hmac-sha256计算得出digest。
c2
c2的构造方法和s2相同。
只不过它是用s2中的digest作为hmac-sha256的key。
握手实现
我们继续写代码,实现简单握手协议.
协议实现相关的代码我们放到protocol文件夹下,我们先定义一个rtmp_stack.hpp文件,用来存放我们后面封装的rtmp相关数据结构,rtmp_stack.hpp中,增加HandshakeBytes类来存放握手相关的数据
// store the handshake bytes,
class HandshakeBytes
{
public:
// For RTMP proxy, the real IP.
uint32_t proxy_real_ip;
// [1+1536]
char* c0c1;
// [1+1536+1536]
char* s0s1s2;
// [1536]
char* c2;
public:
HandshakeBytes();
virtual ~HandshakeBytes();
public:
virtual void dispose();
public:
virtual error_t read_c0c1(SimpleSocketStream* io);
virtual error_t read_s0s1s2(SimpleSocketStream* io);
virtual error_t read_c2(SimpleSocketStream* io);
virtual error_t create_c0c1();
virtual error_t create_s0s1s2(const char* c1 = NULL);
virtual error_t create_c2();
};
我们作为客户端只实现c0,c1,c2的生成发送和s0,s1,s2的读取即可:
HandshakeBytes::HandshakeBytes()
{
c0c1 = s0s1s2 = c2 = NULL;
proxy_real_ip = 0;
}
HandshakeBytes::~HandshakeBytes()
{
dispose();
}
void HandshakeBytes::dispose()
{
freepa(c0c1);
freepa(s0s1s2);
freepa(c2);
}
error_t HandshakeBytes::read_s0s1s2(SimpleSocketStream* io)
{
error_t err = srs_success;
if (s0s1s2) {
return err;
}
ssize_t nsize;
s0s1s2 = new char[3073];
if ((err = io->read_fully(s0s1s2, 3073, &nsize)) != srs_success) {
return error_wrap(err, "read s0s1s2");
}
return err;
}
error_t HandshakeBytes::create_c0c1()
{
error_t err = srs_success;
if (c0c1) {
return err;
}
c0c1 = new char[1537];
random_generate(c0c1, 1537);
// plain text required.
SBuffer stream(c0c1, 9);
stream.write_1bytes(0x03);
stream.write_4bytes((int32_t)::time(NULL));
stream.write_4bytes(0x00);
return err;
}
error_t HandshakeBytes::create_c2()
{
error_t err = srs_success;
if (c2) {
return err;
}
c2 = new char[1536];
srs_random_generate(c2, 1536);
// time
SBuffer stream(c2, 8);
stream.write_4bytes((int32_t)::time(NULL));
// c2 time2 copy from s1
if (s0s1s2) {
stream.write_bytes(s0s1s2 + 1, 4);
}
return err;
}
random_generate实现:
//rand()随机数生成
void random_generate(char* bytes, int size)
{
static bool _random_initialized = false;
if (!_random_initialized) {
srand(0);
_random_initialized = true;
}
for (int i = 0; i < size; i++) {
// the common value in [0x0f, 0xf0]
bytes[i] = 0x0f + (rand() % (256 - 0x0f - 0x0f));
}
}
最基本的客户端握手协议就实现了,服务端的实现也类似.
接下来我们把握手封装到一个类里面:
//rtmp_handshake.hpp
class SimpleHandshake
{
public:
SimpleHandshake();
virtual ~SimpleHandshake();
public:
// Simple handshake.
virtual srs_error_t handshake_with_client(HandshakeBytes* hs_bytes, SimpleSocketStream* io);
virtual srs_error_t handshake_with_server(HandshakeBytes* hs_bytes, SimpleSocketStream* io);
};
实现(同样的我们先只实现客户端连接服务端):
SimpleHandshake::SimpleHandshake()
{
}
SimpleHandshake::~SimpleHandshake()
{
}
error_t SimpleHandshake::handshake_with_server(HandshakeBytes* hs_bytes, SimpleSocketStream* io)
{
error_t err = srs_success;
ssize_t nsize;
// simple handshake
if ((err = hs_bytes->create_c0c1()) != success) {
return error_wrap(err, "create c0c1");
}
if ((err = io->write(hs_bytes->c0c1, 1537, &nsize)) != srs_success) {
return error_wrap(err, "write c0c1");
}
if ((err = hs_bytes->read_s0s1s2(io)) != srs_success) {
return error_wrap(err, "read s0s1s2");
}
// plain text required.
if (hs_bytes->s0s1s2[0] != 0x03) {
return error_new(ERROR_RTMP_HANDSHAKE, "handshake failed, plain text required, version=%X", (uint8_t)hs_bytes->s0s1s2[0]);
}
if ((err = hs_bytes->create_c2()) != success) {
return srs_error_wrap(err, "create c2");
}
memcpy(hs_bytes->c2, hs_bytes->s0s1s2 + 1, 1536);
if ((err = io->write(hs_bytes->c2, 1536, &nsize)) != success) {
return error_wrap(err, "write c2");
}
std::cout << "simple handshake success." << std::endl;
return err;
}
接口封装及测试
我们在实现rtmpsdk.hpp对外暴露接口前,先封装一个上下文环境的Context:
struct Context
{
// The original RTMP url.
std::string url;
// Parse from url.
std::string tcUrl;
std::string host;
std::string vhost;
std::string app;
std::string stream;
std::string param;
// Parse ip:port from host.
std::string ip;
int port;
SimpleSocketStream* skt;
HandshakeBytes* hhb;
// user set timeout, in ms.
int64_t stimeout;
int64_t rtimeout;
Context() : port(0) {
skt = NULL;
}
virtual ~Context() {
srs_freep(skt);
}
};
下面我们按前文步骤深入理解rtmp(二)之C++脚手架搭建封装接口步骤在rtmpsdk.hhp统一封装统一对外暴露接口
1.实现rtmp_create
rtmp_t rtmp_create(const char* url)
{
int ret = ERROR_SUCCESS;
Context* context = new Context();
context->url = url;
// create socket
freep(context->skt);
context->skt = new SimpleSocketStream();
if ((ret = context->skt->create_socket(context->url)) != ERROR_SUCCESS) {//调用SimpleSocketStream的create_socket方法
printf("Create socket failed, ret=%d", ret);
// free the context and return NULL
freep(context);
return NULL;
}
return context;
}
2.封装rtmp_handshake
int rtmp_handshake(rtmp_t rtmp)
{
int ret = ERROR_SUCCESS;
if ((ret = rtmp_dns_resolve(rtmp)) != ERROR_SUCCESS) {
return ret;
}
if ((ret = rtmp_connect_server(rtmp)) != ERROR_SUCCESS) {
return ret;
}
if ((ret = rtmp_do_simple_handshake(rtmp)) != ERROR_SUCCESS) {
return ret;
}
return ret;
}
握手我们分三步执行:
- dns解析
- 连接服务
- 进行握手
rtmp_dns_resolve
rtmp_dns_resolve我们又拆分成了解析uri和解析host:
int rtmp_dns_resolve(rtmp_t rtmp)
{
int ret = ERROR_SUCCESS;
assert(rtmp != NULL);
Context* context = (Context*)rtmp;
// parse uri
if ((ret = librtmp_context_parse_uri(context)) != ERROR_SUCCESS) {
return ret;
}
// resolve host
if ((ret = librtmp_context_resolve_host(context)) != ERROR_SUCCESS) {
return ret;
}
return ret;
}
解析uri:
int librtmp_context_parse_uri(Context* context)
{
int ret = ERROR_SUCCESS;
std::string schema;
//1.通过最后边的斜线"/"将url拆分成tcUrl和stream两部分
parse_rtmp_url(context->url, context->tcUrl, context->stream);
// when connect, we only need to parse the tcUrl
//2.将tcUrl拆分成scheme, host, 虚拟host,app,stream和端口
srs_discovery_tc_url(context->tcUrl,
schema, context->host, context->vhost, context->app, context->stream, context->port,
context->param);
return ret;
}
解析host:
int librtmp_context_resolve_host(Context* context)
{
int ret = ERROR_SUCCESS;
// connect to server:port
int family = AF_UNSPEC;
进行dns解析,将host解析成ip
context->ip = dns_resolve(context->host, family);
if (context->ip.empty()) {
return ERROR_SYSTEM_DNS_RESOLVE;
}
return ret;
}
dns解析:
string dns_resolve(string host, int& family)
{
addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = family;
addrinfo* r = NULL;
if(getaddrinfo(host.c_str(), NULL, &hints, &r)) {
return "";
}
char shost[64];
memset(shost, 0, sizeof(shost));
if (getnameinfo(r->ai_addr, r->ai_addrlen, shost, sizeof(shost), NULL, 0, NI_NUMERICHOST)) {
return "";
}
family = r->ai_family;
return string(shost);
}
rtmp_connect_server
int librtmp_context_connect(Context* context)
{
int ret = ERROR_SUCCESS;
srs_assert(context->skt);
std::string ip = context->ip;
if ((ret = context->skt->connect(ip.c_str(), context->port)) != ERROR_SUCCESS) {
return ret;
}
return ret;
}
int rtmp_connect_server(rtmp_t rtmp)
{
int ret = ERROR_SUCCESS;
assert(rtmp != NULL);
Context* context = (Context*)rtmp;
// set timeout if user not set.
if (context->stimeout == SRS_UTIME_NO_TIMEOUT) {
context->stimeout = SRS_SOCKET_DEFAULT_TMMS;
context->skt->set_send_timeout(context->stimeout * SRS_UTIME_MILLISECONDS);
}
if (context->rtimeout == SRS_UTIME_NO_TIMEOUT) {
context->rtimeout = SRS_SOCKET_DEFAULT_TMMS;
context->skt->set_recv_timeout(context->rtimeout * SRS_UTIME_MILLISECONDS);
}
if ((ret = librtmp_context_connect(context)) != ERROR_SUCCESS) {
return ret;
}
return ret;
}
设置完超时等参数后,调用SimpleSocketStream的connect连接服务器
rtmp_do_simple_handshake
调用我们上面封装的handshake_with_server与rtmp server进行握手
int rtmp_do_simple_handshake(rtmp_t rtmp)
{
int ret = ERROR_SUCCESS;
srs_error_t err = srs_success;
srs_assert(rtmp != NULL);
Context* context = (Context*)rtmp;
srs_assert(context->skt != NULL);
// simple handshake
srs_freep(context->hhb);
context->hhb = new HandshakeBytes();
srs_assert(context->hhb);
SimpleHandshake simple_hs;
if ((err = simple_hs.handshake_with_server(context->hhb, context->skt)) != srs_success) {
return -1;
}
context->hhb->dispose();
cout << "handshake success..." << endl;
return ret;
}
3.main中测试
改造我们上一篇的main方法:
int main(int argc,char* argv[])
{
std::cout << "Hello rtmp server!" << std::endl;
rtmp_t client = rtmp_create("rtmp://127.0.0.1:1935/live/livestream");
int ret = rtmp_handshake(client);
return 0;
}
最终日志输出:
$ ./rtmpsdk
Hello rtmp server!
simple handshake success.
handshake success...
srs服务端日志输出:
[2020-01-21 11:06:17.237][Trace][7503][531] RTMP client ip=172.17.0.1, fd=10
[2020-01-21 11:06:17.240][Trace][7503][531] simple handshake success.
[2020-01-21 11:06:17.240][Warn][7503][531][104] client disconnect peer. ret=1007
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。