RTMP握手过程
RTMP协议握手过程中,服务端和客户端都会发送三个包,客户端是C0、C1、C2,服务端是S0、S1、S2
握手的顺序如下:
1、客户端开始发送C0,C1
2、客户端必须收到S1后,才发送C2
3、客户端必须收到S2后才开始发送其他信息(控制信息和音视频数据)
4、服务器要等收到C0才能发送S0和S1
5、服务器必须等C1后才能发送S2
6、服务器必须等收到C2之后才能发送其他数据(控制信息和音视频数据)
简化如下
-
client–> server : 发送一个创建流的请求 (C0、C1)
-
server–> client : 返回一个流的索引号 (S0、S1、S2)
-
client–> server : 开始发送 (C2)
-
client–> server : 发送音视频数据
其中 C0 和 S0 的格式如下
C1 C2 和 S1 S2的格式如下
数据规律:C1 = S2、C2 = S1
ZLMediaKit中RTMP握手过程
最初在此函数中接收数据
void RtmpSession::onRecv(const Buffer::Ptr &buf) {
_ticker.resetTime();
try {
_total_bytes += buf->size();
onParseRtmp(buf->data(), buf->size());
} catch (exception &ex) {
shutdown(SockException(Err_shutdown, ex.what()));
}
}
将所有接收的数据保存在_remain_data
然后通过 onSearchPacketTail
来处理数据(根据数据类型的不同,有不同的处理方式 比如 handle_C0C1
、handle_C2
)
void HttpRequestSplitter::input(const char *data,size_t len) {
const char *ptr = data;
if(!_remain_data.empty()){
_remain_data.append(data,len);
data = ptr = _remain_data.data();
len = _remain_data.size();
}
splitPacket:
/*确保ptr最后一个字节是0,防止strstr越界
*由于ZLToolKit确保内存最后一个字节是保留未使用字节并置0,
*所以此处可以不用再次置0
*但是上层数据可能来自其他渠道,保险起见还是置0
*/
char &tail_ref = ((char *) ptr)[len];
char tail_tmp = tail_ref;
tail_ref = 0;
//数据按照请求头处理
const char *index = nullptr;
_remain_data_size = len;
while (_content_len == 0 && _remain_data_size > 0 && (index = onSearchPacketTail(ptr,_remain_data_size)) != nullptr) {
if (index == ptr) {
break;
}
if (index < ptr || index > ptr + _remain_data_size) {
throw std::out_of_range("上层分包逻辑异常");
}
//_content_len == 0,这是请求头
const char *header_ptr = ptr;
ssize_t header_size = index - ptr;
ptr = index;
_remain_data_size = len - (ptr - data);
_content_len = onRecvHeader(header_ptr, header_size);
}
if(_remain_data_size <= 0){
//没有剩余数据,清空缓存
_remain_data.clear();
return;
}
/*
* 恢复末尾字节
* 移动到这来,目的是防止HttpRequestSplitter::reset()导致内存失效
*/
tail_ref = tail_tmp;
if(_content_len == 0){
//尚未找到http头,缓存定位到剩余数据部分
_remain_data.assign(ptr,_remain_data_size);
return;
}
//已经找到http头了
if(_content_len > 0){
//数据按照固定长度content处理
if(_remain_data_size < (size_t)_content_len){
//数据不够,缓存定位到剩余数据部分
_remain_data.assign(ptr, _remain_data_size);
return;
}
//收到content数据,并且接受content完毕
onRecvContent(ptr,_content_len);
_remain_data_size -= _content_len;
ptr += _content_len;
//content处理完毕,后面数据当做请求头处理
_content_len = 0;
if(_remain_data_size > 0){
//还有数据没有处理完毕
_remain_data.assign(ptr,_remain_data_size);
data = ptr = (char *)_remain_data.data();
len = _remain_data.size();
goto splitPacket;
}
_remain_data.clear();
return;
}
//_content_len < 0;数据按照不固定长度content处理
onRecvContent(ptr,_remain_data_size);//消费掉所有剩余数据
_remain_data.clear();
}
经过下述函数中的next_step_func
进入函数handle_C0C1
处理客户端发送过来的C0 C1
const char *RtmpProtocol::onSearchPacketTail(const char *data,size_t len){
//移动拷贝提高性能
auto next_step_func(std::move(_next_step_func));
//执行下一步
auto ret = next_step_func(data, len);
if (!_next_step_func) {
//为设置下一步,恢复之
next_step_func.swap(_next_step_func);
}
return ret;
}
for server
const char * RtmpProtocol::handle_C0C1(const char *data, size_t len) {
if (len < 1 + C1_HANDSHARK_SIZE) {
//need more data!
return nullptr;
}
if (data[0] != HANDSHAKE_PLAINTEXT) {
throw std::runtime_error("only plaintext[0x03] handshake supported");
}
// \x00 == 0x00 表示两位16进制
if (memcmp(data + 5, "\x00\x00\x00\x00", 4) == 0) {
//simple handsharke
handle_C1_simple(data);
} else {
#ifdef ENABLE_OPENSSL
//complex handsharke
handle_C1_complex(data);
#else
WarnL << "未打开ENABLE_OPENSSL宏,复杂握手采用简单方式处理,flash播放器可能无法播放!";
handle_C1_simple(data);
#endif//ENABLE_OPENSSL
}
return data + 1 + C1_HANDSHARK_SIZE;
}
上述函数检查如果数据长度、C0 和C1中的版本号正确就进入函数 handle_C1_simple
中处理,即回复S0 S1 S2 并将下一步的处理函数设置为 handle_C2
void RtmpProtocol::handle_C1_simple(const char *data){
//发送S0
char handshake_head = HANDSHAKE_PLAINTEXT;
onSendRawData(obtainBuffer(&handshake_head, 1));
//发送S1
RtmpHandshake s1(0);
onSendRawData(obtainBuffer((char *) &s1, C1_HANDSHARK_SIZE));
//发送S2
onSendRawData(obtainBuffer(data + 1, C1_HANDSHARK_SIZE));
//等待C2
_next_step_func = [this](const char *data, size_t len) {
//握手结束并且开始进入解析命令模式
return handle_C2(data, len);
};
}
后面来的数据就会进入函数handle_C2
处理,至此握手结束,最后会将数据的处理函数设置为handle_rtmp
,用来处理后续的消息和数据(例如控制消息和音视频帧)
const char* RtmpProtocol::handle_C2(const char *data, size_t len) {
if (len < C1_HANDSHARK_SIZE) {
//need more data!
return nullptr;
}
_next_step_func = [this](const char *data, size_t len) {
return handle_rtmp(data, len);
};
//握手结束,进入命令模式
return handle_rtmp(data + C1_HANDSHARK_SIZE, len - C1_HANDSHARK_SIZE);
}