rtmplib rtmp协议过程分析

转自:http://chenzhenianqing.cn/articles/1009.html

写的很好,收藏如下,向作者致敬!

没事碰到了librtmp库,这个库是ffmpeg的依赖库,用来接收,发布RTMP协议格式的数据。

代码在这里:git clone git://git.ffmpeg.org/rtmpdump

先看一段通过librtmp.so库下载RTMP源发布的数据的例子,从rtmpdump中抽取出来。使用的大体流程如下:

  1. RTMP_Init主要就初始化了一下RTMP*rtmp变量的成员。
  2. RTMP_SetupURL 函数将rtmp源地址的端口,app,等url参数进行解析,设置到rtmp变量中。比如这样的地址: rtmp://host[:port]/path swfUrl=url tcUrl=url 。
  3. RTMP_SetBufferMS 函数设置一下缓冲大小;
  4. RTMP_Connect函数完成了连接的建立,一级RTMP协议层的应用握手,待会介绍。
  5. RTMP_ConnectStream总的来说,完成了一个流的创建,以及打开,触发服务端发送数据过来,返回后,服务端应该就开始发送数据了。
  6. Download 其实是RTMP_Read函数的封装,后者读取服务端的数据返回。

[html] view plain copy

  1. RTMP_Init(&rtmp);//初始化RTMP参数
  2. //指定了-i 参数,直接设置URL
  3. if (RTMP_SetupURL(&rtmp, fullUrl.av_val) == FALSE) {
  4. RTMP_Log(RTMP_LOGERROR, "Couldn‘t parse URL: %s", fullUrl.av_val);
  5. return RD_FAILED;
  6. }
  7. rtmp.Link.timeout = timeout ;
  8. /* Try to keep the stream moving if it pauses on us */
  9. if (!bLiveStream )
  10. rtmp.Link.lFlags |= RTMP_LF_BUFX;
  11. while (!RTMP_ctrlC)
  12. {
  13. RTMP_Log(RTMP_LOGDEBUG, "Setting buffer time to: %dms", DEF_BUFTIME);
  14. RTMP_SetBufferMS(&rtmp, DEF_BUFTIME);//告诉服务器帮我缓存多久
  15. RTMP_LogPrintf("Connecting ...\n");
  16. if (!RTMP_Connect(&rtmp, NULL)) {//建立连接,发送"connect"
  17. nStatus = RD_NO_CONNECT;
  18. break;
  19. }
  20. RTMP_Log(RTMP_LOGINFO, "Connected...");
  21. //处理服务端返回的各种控制消息包,比如收到connect的result后就进行createStream,以及发送play(test)消息
  22. if (!RTMP_ConnectStream(&rtmp, 0)) {//一旦返回,表示服务端开始发送数据了.可以play了
  23. nStatus = RD_FAILED;
  24. break;
  25. }
  26. nStatus = Download(&rtmp, file, bStdoutMode, bLiveStream );
  27. if (nStatus != RD_INCOMPLETE || !RTMP_IsTimedout(&rtmp) || bLiveStream)
  28. break;
  29. }

一、建立协议连接

下面来详细介绍下RTMP_Connect函数的工作。

先看代码,下面RTMP_Connect的工作是连接对端,进行握手,并且发送”connect” 控制消息,附带一些app,tcurl等参数。其实时调用了2个函数完成工作的:RTMP_Connect0, RTMP_Connect1 。

[html] view plain copy

  1. int  RTMP_Connect(RTMP *r, RTMPPacket *cp)
  2. {//连接对端,进行握手,并且发送"connect" 控制消息,附带一些app,tcurl等参数
  3. struct sockaddr_in service;
  4. if (!r->Link.hostname.av_len)
  5. return FALSE;
  6. memset(&service, 0, sizeof(struct sockaddr_in));
  7. service.sin_family = AF_INET;
  8. if (r->Link.socksport)
  9. {
  10. /* Connect via SOCKS */
  11. if (!add_addr_info(&service, &r->Link.sockshost, r->Link.socksport))
  12. return FALSE;
  13. }
  14. else
  15. {
  16. /* Connect directly */
  17. if (!add_addr_info(&service, &r->Link.hostname, r->Link.port))
  18. return FALSE;
  19. }
  20. if (!RTMP_Connect0(r, (struct sockaddr *)&service))//建立一个socket连接
  21. return FALSE;
  22. r->m_bSendCounter = TRUE;
  23. return RTMP_Connect1(r, cp);//进行C0-2/S0-2协议握手,发送connect命令
  24. }

其中RTMP_Connect0 函数比较简单,标准的socket, conect 流程,另外设置了一下TCP_NODELAY选项,方便小包发送等。以及SO_RCVTIMEO读超时,这部分属于基本的TCP层面的连接;

RTMP_Connect1 函数则完成类似HTTP层面的RTMP协议的连接建立过程。首先是HandShake 握手。RTMP的握手是通过客户端跟服务端互相发送数据包来完成的,每人3个数据包,名之为C0,C1,C2 以及S0,S1, S2。其发送数据有严格的限制的。因为互相依赖。这个在官方文档中有详细的介绍,不多说。

对于librtmp来说,可能的一种流程是:

CLIENT                          SERVER

C0,C1             —>

<—         S0, S1,S2

C2                    –>

具体看一下代码,比较长。

[html] view plain copy

  1. static int HandShake(RTMP *r, int FP9HandShake)
  2. {//C0,C1 -- S0, S1, S2 -- C2 消息握手协议
  3. int i;
  4. uint32_t uptime, suptime;
  5. int bMatch;
  6. char type;
  7. char clientbuf[RTMP_SIG_SIZE + 1], *clientsig = clientbuf + 1;
  8. char serversig[RTMP_SIG_SIZE];
  9. clientbuf[0] = 0x03;//C0, 一个字节。03代表协议版本号为3     /* not encrypted */
  10. uptime = htonl(RTMP_GetTime());//这是一个时间戳,放在C1消息头部
  11. memcpy(clientsig, &uptime, 4);
  12. memset(&clientsig[4], 0, 4);//后面放4个字节的空数据然后就是随机数据
  13. //后面是随机数据,总共1536字节的C0消息
  14. #ifdef _DEBUG
  15. for (i = 8; i < RTMP_SIG_SIZE; i++)
  16. clientsig[i] = 0xff;
  17. #else
  18. for (i = 8; i < RTMP_SIG_SIZE; i++)
  19. clientsig[i] = (char)(rand() % 256);//发送C0, C1消息
  20. #endif
  21. if (!WriteN(r, clientbuf, RTMP_SIG_SIZE + 1))
  22. return FALSE;
  23. //下面读一个字节也就是S0消息,看协议是否一样
  24. if (ReadN(r, &type, 1) != 1)    /* 0x03 or 0x06 */
  25. return FALSE;
  26. RTMP_Log(RTMP_LOGDEBUG, "%s: Type Answer   : %02X", __FUNCTION__, type);
  27. if (type != clientbuf[0])//C/S版本不一致
  28. RTMP_Log(RTMP_LOGWARNING, "%s: Type mismatch: client sent %d, server answered %d",  __FUNCTION__, clientbuf[0], type);
  29. //读取S1消息,里面有服务器运行时间
  30. if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
  31. return FALSE;
  32. /* decode server response */
  33. memcpy(&suptime, serversig, 4);
  34. suptime = ntohl(suptime);
  35. RTMP_Log(RTMP_LOGDEBUG, "%s: Server Uptime : %d", __FUNCTION__, suptime);
  36. RTMP_Log(RTMP_LOGDEBUG, "%s: FMS Version   : %d.%d.%d.%d", __FUNCTION__, serversig[4], serversig[5], serversig[6], serversig[7]);
  37. /* 2nd part of handshake */
  38. if (!WriteN(r, serversig, RTMP_SIG_SIZE))//发送C2消息,内容就等于S1消息的内容。
  39. return FALSE;
  40. //读取S2消息
  41. if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
  42. return FALSE;
  43. bMatch = (memcmp(serversig, clientsig, RTMP_SIG_SIZE) == 0);
  44. if (!bMatch)//服务端返回的S2消息必须跟C1消息一致才行
  45. {
  46. RTMP_Log(RTMP_LOGWARNING, "%s, client signature does not match!", __FUNCTION__);
  47. }
  48. return TRUE;
  49. }

握手的目的其实是互相沟通一下支持的协议版本号,服务器时间戳等。确保连接的对端真的是RTMP支持的。

发送请求给服务端。

然后就是SendConnectPacket的工作了。总结一句其功能是成一个“connect消息以及其app,tcurl等参数,然后调用RTMP_SendPacket函数将其数据发送出去。

到这里连接建立完成了。

二、准备数据通道

RTMP_ConnectStream完成了通道的建立。其处理服务端返回的各种控制消息包,比如收到connect的result后就进行createStream,以及发送play(test)消息。一旦返回,表示服务端开始发送数据了.可以play了。

函数本身比较简单,就是一个while循环,不断的调用RTMP_ReadPacket读取服务端发送过来的数据包进行相应的处理。直到m_bPlaying变老变为TRUE为止,也就是可以播放的时候为止。数据包的处理函数为RTMP_ClientPacket。

[html] view plain copy

  1. int RTMP_ConnectStream(RTMP *r, int seekTime)
  2. {//循环读取服务端发送过来的各种消息,比如window ack**, set peer bandwidth, set chunk size, _result等
  3. //直到接收到了play
  4. RTMPPacket packet = { 0 };
  5. /* seekTime was already set by SetupStream / SetupURL.
  6. * This is only needed by ReconnectStream.
  7. */
  8. if (seekTime > 0)
  9. r->Link.seekTime = seekTime;
  10. r->m_mediaChannel = 0;
  11. //一个个包的读取,直到服务端告诉我说可以play了为止
  12. while (!r->m_bPlaying && RTMP_IsConnected(r) && RTMP_ReadPacket(r, &packet))
  13. {
  14. if (RTMPPacket_IsReady(&packet))//是否读取完毕。((a)->m_nBytesRead == (a)->m_nBodySize)
  15. {
  16. if (!packet.m_nBodySize)
  17. continue;
  18. if ((packet.m_packetType == RTMP_PACKET_TYPE_AUDIO) ||
  19. (packet.m_packetType == RTMP_PACKET_TYPE_VIDEO) ||
  20. (packet.m_packetType == RTMP_PACKET_TYPE_INFO))
  21. {
  22. RTMP_Log(RTMP_LOGWARNING, "Received FLV packet before play()! Ignoring.");
  23. RTMPPacket_Free(&packet);
  24. continue;
  25. }
  26. RTMP_ClientPacket(r, &packet);//处理一下这个数据包,其实里面就是处理服务端发送过来的各种消息等。直到接受到了play/publish
  27. RTMPPacket_Free(&packet);
  28. }
  29. }
  30. //返回当前是否接收到了play/publish 或者stopd等
  31. return r->m_bPlaying;
  32. }

RTMP_ReadPacket 跟Send类似,函数比较长,基本是处理RTMP数据包RTMPPacket的包头,包体的读写等碎碎代码。真正处理事件的函数为RTMP_ClientPacket。

RTMP_ClientPacket函数是一个很大的数据包分发器。负责将不同类型m_packetType的数据包传递给对应的函数进行处理。比如:

  1. RTMP_PACKET_TYPE_CHUNK_SIZE 块大小设置消息 HandleChangeChunkSize;
  2. RTMP_PACKET_TYPE_CONTROL 控制消息   HandleCtrl ;
  3. RTMP_PACKET_TYPE_AUDIO    音频消息    HandleAudio;
  4. RTMP_PACKET_TYPE_INFO   元数据设置消息  HandleMetadata;
  5. RTMP_PACKET_TYPE_INVOKE 远程过程调用   HandleInvoke;

其中比较重要的是HandleInvoke 远程过程调用。其里面实际是个状态机。

前面说过,建立连接握手的时候,客户端回发送connect字符串以及必要的参数给服务端。然后服务端会返回_result消息。当客户端收到_result消息后,会从消息里面取出其消息号,从而在r->m_methodCalls[i].name 中找到对应发送的消息是什么消息。从而客户端能够确认发送的那条消息被服务端处理了。进而可以进行后续的处理了。来看HandleInvoke开头的代码。

[html] view plain copy

  1. static int HandleInvoke(RTMP *r, const char *body, unsigned int nBodySize){
  2. AMFObject obj;
  3. AVal method;
  4. double txn;
  5. int ret = 0, nRes;
  6. nRes = AMF_Decode(&obj, body, nBodySize, FALSE);
  7. if (nRes < 0){
  8. RTMP_Log(RTMP_LOGERROR, "%s, error decoding invoke packet", __FUNCTION__);
  9. return 0;
  10. }
  11. AMF_Dump(&obj);
  12. AMFProp_GetString(AMF_GetProp(&obj, NULL, 0), &method);
  13. txn = AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 1));
  14. RTMP_Log(RTMP_LOGDEBUG, "%s, server invoking <%s>", __FUNCTION__, method.av_val);
  15. if (AVMATCH(&method, &av__result))
  16. {//接收到服务端返回的一个_result包,所以我们需要找到这个包对应的那条命令,从而处理这条命令的对应事件。
  17. //比如我们之前发送了个connect给服务端,服务端必然会返回_result,然后我们异步收到result后,会调用
  18. //RTMP_SendServerBW,RTMP_SendCtrl,以及RTMP_SendCreateStream来创建一个stream
  19. AVal methodInvoked = {0};
  20. int i;
  21. for (i=0; i<r->m_numCalls; i++) {//找到这条指令对应的触发的方法
  22. if (r->m_methodCalls[i].num == (int)txn) {
  23. methodInvoked = r->m_methodCalls[i].name;
  24. AV_erase(r->m_methodCalls, &r->m_numCalls, i, FALSE);
  25. break;
  26. }
  27. }

上面可以看出,librtmp发送出一条需要得到服务端返回结果的消息的时候,会将消息名称记录在m_methodCalls数组上面,其下标就是告诉服务端的消息id。从而每次收到_result的时候就能知道对那个的是哪条消息methodInvoked。

然后就可以进行对应的处理了,举个例子:在之前发送connect的时候,body部分的第二个元素为一个整数,代表一个唯一ID,这里是1,如下图:

服务端对此数据包的回包会是如下的样子:

注意蓝底的Number 1, 他会跟上面的connect(live)消息对应的。因此methodInvoked变量就能等于connect,所以HandleInvoke函数会进入到如下的分支:

[html] view plain copy

  1. //下面根据不同的方法进行不同的处理
  2. if (AVMATCH(&methodInvoked, &av_connect))
  3. {
  4. if (r->Link.protocol & RTMP_FEATURE_WRITE)
  5. {
  6. SendReleaseStream(r);
  7. SendFCPublish(r);
  8. }
  9. else
  10. {//告诉服务端,我们的期望是什么,窗口大小,等
  11. RTMP_SendServerBW(r);
  12. RTMP_SendCtrl(r, 3, 0, 300);
  13. }
  14. RTMP_SendCreateStream(r);//因为服务端同意了我们的connect,所以这里发送createStream创建一个流
  15. //创建完成后,会再次进如这个函数从而走到下面的av_createStream分支,从而发送play过去
  16. if (!(r->Link.protocol & RTMP_FEATURE_WRITE))
  17. {
  18. /* Authenticate on Justin.tv legacy servers before sending FCSubscribe */
  19. if (r->Link.usherToken.av_len)
  20. SendUsherToken(r, &r->Link.usherToken);
  21. /* Send the FCSubscribe if live stream or if subscribepath is set */
  22. if (r->Link.subscribepath.av_len)
  23. SendFCSubscribe(r, &r->Link.subscribepath);
  24. else if (r->Link.lFlags & RTMP_LF_LIVE)
  25. SendFCSubscribe(r, &r->Link.playpath);
  26. }
  27. }
  28. else if (AVMATCH(&methodInvoked, &av_createStream))

上面的分支在服务端同意客户端的connect请求后,客户端调用。

根据流的配置类型不同,进行不同的处理,比如如果是播放的话,那么就会调用SendReleaseStream,以及SendFCPublish发送publish消息;

否则会调用RTMP_SendServerBW设置缓冲大小,也就是图中的“Window Acknowledgement Size 5000000” 。 然后就是RTMP_SendCtrl设置缓冲时间;

之后就会调用RTMP_SendCreateStream函数,发送注明的流创建过程。发送createStream消息给服务端,创建数据传输通道。当然这里只是发送了数据,什么时候能够确定创建成功呢?答案很简单:当接收到服务端的数据包后,如果其为过程调用,且为_result,并且AVMATCH(&methodInvoked, &av_createStream)的时候,就代表创建成功。看如下代码:

[html] view plain copy

  1. else if (AVMATCH(&methodInvoked, &av_createStream))
  2. {
  3. r->m_stream_id = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));
  4. if (r->Link.protocol & RTMP_FEATURE_WRITE)
  5. {//如果是要发送,那么高尚服务端,我们要发数据
  6. SendPublish(r);
  7. }
  8. else
  9. {//否则告诉他我们要接受数据
  10. if (r->Link.lFlags & RTMP_LF_PLST)
  11. SendPlaylist(r);
  12. SendPlay(r);//发送play过去,
  13. RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);//以及我们的buf大小
  14. }
  15. }
  16. else if (AVMATCH(&methodInvoked, &av_play) ||
  17. AVMATCH(&methodInvoked, &av_publish))
  18. {//接收到了play的回复,那么标记为play
  19. r->m_bPlaying = TRUE;
  20. }
  21. free(methodInvoked.av_val);

createStream消息确认收到后,客户端就是发送SendPlay 请求开始接收数据,或者SendPublish请求开始发布数据;

此后再经过几次简短的消息传输,比如:onStatus(‘NetStream.Play.Start’) | |RtmpSampleAccess() | onMetaData() 等,真正的数据就能够开始接收了。也就是服务端开始发送数据了。通信的信道已经建立好。

三、读取数据

连接经过漫长的过程建立起来后,数据读取比较简短,只需要调用nRead = RTMP_Read(rtmp, buffer, bufferSize)函数不断的读取数据就行。这些数据就是发送方放入RTMP通道里面的数据了。

所以这部分其实就等于:通道已经建立,读使用RTMP_Read,发送使用RTMP_SendPacket等。

介绍的差不多了,再细致的后续有时间再补上。基本框架就在这里。过段时间看看nginx_rtmp_module模块学习一下。

时间: 2024-10-05 12:57:18

rtmplib rtmp协议过程分析的相关文章

nginx搭建rtmp协议流媒体服务器总结

最近在 ubuntu12.04+wdlinux(centos)上搭建了一个rtmp服务器,感觉还挺麻烦的,所以记录下. 大部分都是参考网络上的资料. 前提: 在linux下某个目录中新建一个nginx目录. 然后进入该目录去下载搭建环境所需要的一些资源包. 此处在 /root/  目录下新建一个nginx目录即: /root/softsource/ 注意:依赖包和工具包需要下载,请在良好的网络环境下安装,否则在网速不好的情况下容易下漏掉,造成后面安装失败 ====================

调试libRTMP代码来分析RTMP协议

RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写.该协议基于TCP,是一个协议族,常用在视频直播领域.RTMP协议的默认端口是1935. 学习一个协议最好的方法就是调试其通信过程,期间还可以使用wireshark抓包分析.本人在libRTMP的基础上添加了推流部分,并且使得整个过程变得可调试,学习其协议就变得简单多了.配置好的VS2010可调试的libRTMP工程:https://github.com/jiayayao/librtmp.该工程可以

HTTP协议/RTSP协议/RTMP协议的区别

RTSP. RTMP.HTTP的共同点.区别 共同点: 1:RTSP RTMP HTTP都是在应用应用层. 2: 理论上RTSP RTMPHTTP都可以做直播和点播,但一般做直播用RTSP RTMP,做点播用HTTP.做视频会议的时候原来用SIP协议,现在基本上被RTMP协议取代了. 区别: 1:HTTP: 即超文本传送协议(ftp即文件传输协议). HTTP:(Real Time Streaming Protocol),实时流传输协议. HTTP全称Routing Table Maintena

nginx搭建支持http和rtmp协议的流媒体服务器之一

实验目的: 让Nginx支持flv和mp4格式文件,支持RTMP协议的直播和点播: 同时打开RTMP的HLS功能 ?资料: HTTP Live Streaming(缩写是 HLS)是一个由苹果公司提出的基于HTTP的流媒体网络传输协议. HLS只请求基本的HTTP报文,与实时传输协议(RTP)不同,HLS可以穿过任何允许HTTP数据通过的防火墙或者代理服务器. 它也很容易使用内容分发网络来传输媒体流. 使用ffmpeg来完成对flv.mp4.mp3等格式的转化(点播实验暂时不测试) 一.准备工作

(转载)RTMP 协议学习总结

RTMP协议是一个互联网TCP/IP五层体系结构中应用层的协议.RTMP协议中基本的数据单元称为消息(Message).当RTMP协议在互联网中传输数据的时候,消息会被拆分成更小的单元,称为消息块(Chunk). 1 消息 消息是RTMP协议中基本的数据单元.不同种类的消息包含不同的Message Type ID,代表不同的功能.RTMP协议中一共规定了十多种消息类型,分别发挥着不同的作用.例如,Message Type ID在1-7的消息用于协议控制,这些消息一般是RTMP协议自身管理要使用的

C++实现RTMP协议发送H.264编码及AAC编码的音视频,摄像头直播

C++实现RTMP协议发送H.264编码及AAC编码的音视频 RTMP(Real Time Messaging Protocol)是专门用来传输音视频数据的流媒体协议,最初由Macromedia 公司创建,后来归Adobe公司所有,是一种私有协议,主要用来联系Flash Player和RtmpServer,如FMS, Red5, crtmpserver等.RTMP协议可用于实现直播.点播应用,通过FMLE(Flash Media Live Encoder)推送音视频数据至RtmpServer,可

RTMP协议详解(转)

转自<RTMP协议详解(一) (二) (三) > Real Time Messaging Protocol(实时消息传送协议协议)是Adobe Systems公司为Flash播放器和服务器之间音频.视频和数据传输开发的私有协议. 具体使用RTMP的AS代码大概如下: var videoInstance:Video = your_video_instance; var nc:NetConnection = new NetConnection(); var connected:Boolean =

将OBS录制数据通过RTMP协议引入到自己的程序中

最近在window是平台下,做了一功能实现通过OBS采集音视频,并通过RTMP协议将其编码压缩后的数据接入到自己的程序中来,因OBS软件自带有很强大的游戏录制和桌面录制的功能,以及输入.输出音频设备数据的采集并混音的功能,目前斗鱼游戏直播也是使用的此软件作为录制工具. OBS软件由于使用了window sdk进行音频的采集,所以只支持window vista版本以上平台,故XP系统用户是使用不了此软件的,下载地址:https://obsproject.com/download OBS目前只支持R

C++实现RTMP协议发送H.264编码及AAC编码的音视频

转自:http://www.cnblogs.com/haibindev/archive/2011/12/29/2305712.html RTMP(Real Time Messaging Protocol)是专门用来传输音视频数据的流媒体协议,最初由Macromedia 公司创建,后来归Adobe公司所有,是一种私有协议,主要用来联系Flash Player和RtmpServer,如FMS, Red5, crtmpserver等.RTMP协议可用于实现直播.点播应用,通过FMLE(Flash Me