# C++实现RTSP服务器 **Repository Path**: zhongYYcode/c-implementation---rtsp-server ## Basic Information - **Project Name**: C++实现RTSP服务器 - **Description**: 线上实习用于学习C++网络编程的实战项目 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2023-07-18 - **Last Updated**: 2023-07-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # C++实现RTSP服务器-学习笔记 ## 1.初步了解rtsp ### 什么是rtsp rtsp是一个协议(实时传输流协议),与之相关的还有rtp协议,rtcp协议(rtp用于传输媒体数据,rtcp用于传输质量监控和会话成员管理,而rtsp只是用于命令控制) ### 项目要求 创建一个rtsp服务器,并通过vlc播放器作为客户端进行验证。这时我意识到,想要完成项目,至少还有一个rtp协议也需要实现,但rtcp可以不实现,虽然会丧失音视频同步播放的功能且同一时间只能有一个客户端。 ### 了解rtsp rtsp客户端和服务端进行交互,消息格式分为: 1. 请求消息(由客户端发起,C->S) 2. 回应消息(由服务端发起,S->C) 具体有如下几个消息: | 方法 | 描述 | | -------- | ---------------------------------- | | OPTIONS | 获取服务端提供的可用方法 | | DESCRIBE | 向服务端获取对应会话的媒体描述信息 | | SETUP | 向服务端发起建立请求,建立连接会话 | | PLAY | 向服务端发起播放请求 | | TEARDOWN | 向服务端发起关闭连接会话请求 | 交互过程如下(摘抄自CSDN--JT同学的博客): **OPTIONS** - C–>S ``` OPTIONS rtsp://192.168.31.115:8554/live RTSP/1.0\r\n CSeq: 2\r\n \r\n ``` 客户端向服务器请求可用方法 - S–>C ``` RTSP/1.0 200 OK\r\n CSeq: 2\r\n Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY\r\n \r\n ``` 服务端回复客户端,当前可用方法OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY **DESCRIBE** - C–>S ``` DESCRIBE rtsp://192.168.31.115:8554/live RTSP/1.0\r\n CSeq: 3\r\n Accept: application/sdp\r\n \r\n ​ 客户端向服务器请求媒体描述文件,格式为sdp - S–>C ``` RTSP/1.0 200 OK\r\n CSeq: 3\r\n Content-length: 146\r\n Content-type: application/sdp\r\n \r\n v=0\r\n o=- 91565340853 1 in IP4 192.168.31.115\r\n t=0 0\r\n a=contol:*\r\n m=video 0 RTP/AVP 96\r\n a=rtpmap:96 H264/90000\r\n a=framerate:25\r\n a=control:track0\r\n ``` 服务器回复了sdp文件,这个文件告诉客户端当前服务器有哪些音视频流,有什么属性,具体稍后再讲解 这里只需要直到客户端可以根据这些信息得知有哪些音视频流可以发送 **SETUP** - C–>S ``` SETUP rtsp://192.168.31.115:8554/live/track0 RTSP/1.0\r\n CSeq: 4\r\n Transport: RTP/AVP;unicast;client_port=54492-54493\r\n \r\n ``` 客户端发送建立请求,请求建立连接会话,准备接收音视频数据 解析一下Transport: RTP/AVP;unicast;client_port=54492-54493\r\n RTP/AVP:表示RTP通过UDP发送,如果是RTP/AVP/TCP则表示RTP通过TCP发送 unicast:表示单播,如果是multicast则表示多播 client_port=54492-54493:由于这里希望采用的是RTP OVER UDP,所以客户端发送了两个用于传输数据的端口,客户端已经将这两个端口绑定到两个udp套接字上,54492表示是RTP端口,54493表示RTCP端口(RTP端口为某个偶数,RTCP端口为RTP端口+1) - S–>C ``` RTSP/1.0 200 OK\r\n CSeq: 4\r\n Transport: RTP/AVP;unicast;client_port=54492-54493;server_port=56400-56401\r\n Session: 66334873\r\n \r\n ``` 服务端接收到请求之后,得知客户端要求采用RTP OVER UDP发送数据,单播,客户端用于传输RTP数据的端口为54492,RTCP的端口为54493 服务器也有两个udp套接字,绑定好两个端口,一个用于传输RTP,一个用于传输RTCP,这里的端口号为56400-56401 之后客户端会使用54492-54493这两端口和服务器通过udp传输数据,服务器会使用56400-56401这两端口和这个客户端传输数据 **PLAY** - C–>S ``` PLAY rtsp://192.168.31.115:8554/live RTSP/1.0\r\n CSeq: 5\r\n Session: 66334873\r\n Range: npt=0.000-\r\n \r\n ``` 客户端请求播放媒体 - S–>C ``` RTSP/1.0 200 OK\r\n CSeq: 5\r\n Range: npt=0.000-\r\n Session: 66334873; timeout=60\r\n \r\n ``` 服务器回复之后,会开始使用RTP通过udp向客户端的54492端口发送数据 **TEARDOWN** - C–>S ``` TEARDOWN rtsp://192.168.31.115:8554/live RTSP/1.0\r\n CSeq: 6\r\n Session: 66334873\r\n \r\n ``` - S–>C ``` RTSP/1.0 200 OK\r\n CSeq: 6\r\n \r\n ``` 这些请求就是一个个数据包,程序处理数据包数据后就会做出相应的反馈。 ## 2.其他需要了解的知识点 ### SDP格式 sdp(Session Description Protocol)会话描述协议。 那么它与RTSP协议有什么关系呢? RTSP协议中使用sdp进行媒体信息的描述(客户端发送describe后服务端以sdb方式回复) (接下来内容同样摘自CSDN-JT同学) sdp格式由多行的type=value组成 sdp会话描述由一个会话级描述和多个媒体级描述组成。会话级描述的作用域是整个会话,媒体级描述描述的是一个视频流或者音频流 会话级描述由v=开始到第一个媒体级描述结束 媒体级描述由m=开始到下一个媒体级描述结束 下面是上面示例的sdp文件,我们就来好好分析一下这个sdp文件 ``` v=0\r\n o=- 91565340853 1 in IP4 192.168.31.115\r\n t=0 0\r\n a=contol:*\r\n m=video 0 RTP/AVP 96\r\n a=rtpmap:96 H264/90000\r\n a=framerate:25\r\n a=control:track0\r\n ``` 这个示例的sdp文件包含`一个会话级描述`和`一个媒体级描述`,分别如下 - 会话级描述 ``` v=0\r\n o=- 91565340853 1 IN IP4 192.168.31.115\r\n t=0 0\r\n a=contol:*\r\n ``` v=0 表示sdp的版本 o=- 91565340853 1 IN IP4 192.168.31.115 格式为 o=<用户名> <会话id> <会话版本> <网络类型><地址类型> <地址> 用户名:- 会话id:91565340853,表示rtsp://192.168.31.115:8554/live请求中的live这个会话 会话版本:1 网络类型:IN,表示internet 地址类型:IP4,表示ipv4 地址:192.168.31.115,表示服务器的地址 - 媒体级描述 ``` m=video 0 RTP/AVP 96\r\n a=rtpmap:96 H264/90000\r\n a=framerate:25\r\n a=control:track0\r\n ``` m=video 0 RTP/AVP 96\r\n 格式为 m=<媒体类型> <端口号> <传输协议> <媒体格式 > 媒体类型:video 端口号:0,为什么是0?因为上面在SETUP过程会告知端口号,所以这里就不需要了 传输协议:RTP/AVP,表示RTP OVER UDP,如果是RTP/AVP/TCP,表示RTP OVER TCP 媒体格式:表示负载类型(payload type),一般使用96表示H.264 a=rtpmap:96 H264/90000 格式为a=rtpmap:<媒体格式><编码格式>/<时钟频率> a=framerate:25 表示帧率 a=control:track0 表示这路视频流在这个会话中的编号 ### RTP协议 #### 什么是RTP 实时传输协议RTP(Real-time Transport Protocol)是一个网络传输协议,它作为因特网标准在 [ RFC 3550 ] 有详细说明。 RTP协议规定了互联网上传递音频和视频的标准数据包格式,可用于多播协议,单播应用中。RTP协议常用于流媒体系统(配合RTSP协议)和RTCP一起使用,它是建立在用户数据报协议上的(UDP)。应用场景有视频会议和一键通(Push toTalk)系统。RTP标准定义了两个子协议 ,RTP和RTCP。数据传输协议RTP该协议提供的信息包括:时间戳(用于同步)、序列号(用于丢包和重排序检测)、以及负载格式(用于说明数据的编码格式)。控制协议RTCP,用于QoS反馈和同步媒体流。相对于RTP来说,RTCP所占的带宽非常小,通常只有5%。 #### 为什么用RTP 其实像tcp这些网络协议也是可以用来传输音视频数据的(vlc在推rtsp流,拉流的时候如果在同一个ip就是使用的tcp传输而不是rtp),那么为什么还会有RTP的诞生了?Tcp是可靠传输在网络环境差的情况下偶尔出现数据丢包从而导致检测重传等,这就会大大降低数据的传输实时性。 把tcp的检测重传机制去掉。RTP协议是一种基于UDP的传输协议,对于那些丢失的数据包,不存在由于超时检测而带来的延时,同时对于那些丢弃的数据也可以由上层根据其重要性来选择性的重传。比如,对于I帧、P帧、B帧数据,由于其重要性依次降低,故在网络状况不好的情况下,可以考虑在B帧丢失甚至P帧丢失的情况下不进行重传,这样,在客户端方面,虽然可能会有短暂的不清晰画面,但却保证了实时性的体验和要求。对于服务质量的监视与反馈、媒体间的同步,以及多播组中成员的标识,重传这些交给RTCP来做。 RTP目的传输地址由一个网络地址和一对端口组成,有两个端口:一个给RTP包,一个给RTCP包,使得RTP/RTCP数据能够正确发送。RTP数据发向偶数的UDP端口,而对应的控制信号RTCP数据发向相邻的奇数UDP端口(偶数的UDP端口+1),这样就构成一个UDP端口对。 #### RTP数据包 rtp包由rtp头部和rtp荷载构成 - **RTP头部** ![img](https://img-blog.csdnimg.cn/20190809200342276.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQ2MjIwMg==,size_16,color_FFFFFF,t_70) ​ 版本号(V):2Bit,用来标志使用RTP版本 填充位(P):1Bit,如果该位置位,则该RTP包的尾部就包含填充的附加字节 扩展位(X):1Bit,如果该位置位,则该RTP包的固定头部后面就跟着一个扩展头部 ​ CSRC技术器(CC):4Bit,含有固定头部后面跟着的CSRC的数据 标记位(M):1Bit,该位的解释由配置文档来承担 载荷类型(PT):7Bit,标识了RTP载荷的类型 序列号(SN):16Bit,发送方在每发送完一个RTP包后就将该域的 值增加1,可以由该域检测包的丢失及恢复包的序列。序列号的初始值是随机的 时间戳:32比特,记录了该包中数据的第一个字节的采样时刻 同步源标识符(SSRC):32比特,同步源就是RTP包源的来源。在同一个RTP会话中不能有两个相同的SSRC值 贡献源列表(CSRC List):0-15项,每项32比特,这个不常用 - rtp荷载 ​ rtp载荷通常为音频或者视频数据 ### H264编码 H264是一种视频的数据类型 将H264的数据进行RTP打包,即可实现视频的传输,这里对H264的详细知识不作解释,着重于如何进行H264的RTP打包。 #### H264的RTP打包 H.264有三种RTP打包方式: - **单NALU打包** 一个RTP包包含一个完整的NALU - **聚合打包** 对于较小的NALU,一个RTP包可包含多个完整的NALU - **分片打包** 对于较大的NALU,一个NALU可以分为多个RTP包发送 比较常用的是`单NALU打包`和`分片打包`,本文也只介绍这两种。 ##### 单NALU打包 所谓单NALU打包就是将一整个NALU的数据放入RTP包的载荷中 这是最简单的一种方式,无需过多的讲解 ##### 分片打包 每个RTP包都有大小限制的,因为RTP一般都是使用UDP发送,UDP没有流量控制,所以要限制每一次发送的大小,所以如果一个NALU的太大,就需要分成多个RTP包发送,如何分成多个RTP包,下面来好好讲一讲 首先要明确,RTP包的格式是绝不会变的,永远多是RTP头+RTP载荷 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2019081016271484.PNG) RTP头部是固定的,那么只能在RTP载荷中去添加额外信息来说明这个RTP包是表示同一个NALU 如果是分片打包的话,那么在RTP载荷开始有两个字节的信息,然后再是NALU的内容 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190810163356293.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQ2MjIwMg==,size_16,color_FFFFFF,t_70) 第一个字节位FU Indicator,其格式如下 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2019081214151983.PNG) 高三位:与NALU第一个字节的高三位相同 Type:28,表示该RTP包一个分片,为什么是28?因为H.264的规范中定义的,此外还有许多其他Type,这里不详讲 第二个字节位FU Header,其格式如下 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190810163844837.PNG) S:标记该分片打包的第一个RTP包 E:比较该分片打包的最后一个RTP包 Type:NALU的Type ##### H.264 RTP包的时间戳计算 RTP包的时间戳起始值是随机的 RTP包的时间戳增量怎么计算? 假设时钟频率为90000,帧率为25 频率为90000表示一秒用90000点来表示 帧率为25,那么一帧就是1/25秒 所以一帧有90000*(1/25)=3600个点来表示 因此每一帧数据的时间增量为3600 ## 3.项目实现计划 一个RTSP服务器最简单的包含两部分,一部分是RTSP的交互,一部分是RTP发送。 所以我会分两部分来完成,先完成RTSP的交互,再这之后,再加入H264的传输功能,如果还有时间,在考虑实现AAC(音频数据)的传输,以及多播(同时传输)的功能。 ## 4.第一部分——RTSP交互的实现 大概步骤如下: 1. 创建一个TCP服务器,用于与客户端进行连接 2. 创建一个UDP套接字用于RTP传输数据,并绑定端口号 3. 等待客户端连接 4. 当rtsp客户端连接成功后就会开始发送请求,服务器这是需要接收客户端请求并开始解析,再采取相应得操作,因此要解析上文提到的客户端发出的请求消息。 5. 根据解析出的请求消息,做出不同的响应 ## 5.第二部分——RTP传输H264数据 大概计划如下: 再上文的 void Rtsp::responseToClient(...)函数中,在判断请求消息类型时,若为PLAY请求,则开始进行RTP包的发送,大体框架如下: ``` //如果是PLAY请求,则开始发送RTP包 if (!strcmp(method, "PLAY")) { //声明定义 ... //循环内不断发送 while (1) { ... } //释放资源 ... } ``` ## 6.项目功能拓展 1. 支持多客户端访问 2. 支持实时视频流 3. 编写makefile来运行项目 ## 7.实现多播功能的小结 首先,什么是多播,即为一个服务器可以连接多个客户端,实现多个客户端可以同时播放视频。 那么,实现多播功能一共需要哪些步骤呢? 1. 服务器TCP需要支持可以连接多个客户端,因此需要select函数来管理多个套接字。 2. 需要创建多线程以达到发送数据包的函数不被其他线程的函数所阻塞 3. RTP需要支持向多播地址发送数据包。 ### 1.TCP支持多个客户端 实现这个功能最主要的函数就是select()。 而在该项目中,因为我们只是用于判断是否有新的客户端连接,所以我们只需要用到可读文件描述符集合,以及最后一个参数我们不设置最大超时时间,所以我们设置函数: ``` select(max_fd + 1, &rset, NULL, NULL, NULL); ``` 一开始我在思考,我们服务器原本有创建TCP(用于RTSP)以及两个UDP(用于RTP和RTCP),那么我们到底是要监听什么套接字呢。 那么就要追根溯源,思考我们使用select函数到底是要达成什么目的了。 我们使用select函数来管理多个套接字,是为了能够实现多个客户端连接服务器的目的,那么服务器与客户端之间的连接,是基于RTSP协议,也就是我们最开始建立的TCP套接字,而UDP是为了后续向客户端发送数据而创建的。 那么,我们向多个客户端发送数据,是否也要为了每一个客户端建立2个不同的UDP呢,那么是否也应该把它们加入select的文件描述符集合里来进行管理呢? 这里我们先按下不表,等到第三小点时再详细描述。 在测试之后,发现还是只能运行一个客户端,但当我把发送数据包的函数给注释掉之后,我发现竟然可以连接多个客户端了,所以经过分析,我认为应该是由于程序执行到向客户端发送数据的函数时,就一直停在那不断的发送数据,那么就无法进行select的管理。所以我认为,应该将发送数据的函数存放在线程里,实现多线程管理,才不会造成阻塞。 ### 2.将我们的running函数作为线程体函数,以达到可以同时播放的功能 这里就要面临一个问题了,我们C++来编写程序时,定义的是一个个的类,而函数是封装在一个个的类里面。 可是,一个普通的类成员函数,是无法作为线程函数的。除非将其设置为static函数,可是,这样不仅破坏了类的封装性,同时,该函数中的其他非static的成员变量和函数,还是无法被访问到。 所以,在经过一个早上查阅资料之后,我发现,由于static的效果是将类中的this指针给去掉,那么我们倘若不将我们的成员函数变为static的话,可以将this指针也传给线程创建的函数,这样就不会无法访问到我们的成员函数了,即 ``` std::thread t(&responseToClient, this, clientSockfd, clientIp, clientPort, serverRtpSockfd, serverRtcpSockfd); ``` 然而,在测试之后,我发现运行一个客户端时,视频可以正常播放,但运行两个客户端时,当我打开第二个客户端,第一个客户端就无法继续播放了,过了一段时间就自动退出了,说明第一个客户端一个在第二个客户端启动之后就没有再接收数据了。 那么,就迎来看我们的第三个问题。 ### 3.RTP支持多播 在这里,我突然回想到之前编写单播播放器时,所用的sdp文件,是设置过单播的,那么是否sdp也能设置多播呢,因此我上网查阅了一下多播的有关资料。 经学习,想要实现多播的效果,需要将我们的RTSP协议做一下改变。 原先,我们是向连接的客户端发送数据,而多播则是向一个网络上规定好的一组多播地址来发送数据,那么只要每个客户端接收的是多播地址的数据,既可以拿到数据,进行播放。 而多播的协议要在sdp文件里面更改,即我们的handle函数,我将handle函数进行修改,并设置了多播端口和多播地址之后,在运行。发现程序就没有任何的问题了。 关键函数如下: ``` int Rtsp::handleDescribe(char* result, int cseq, char* url) { char sdp[500]; char ip[100]; sscanf(url, "rtsp://%[^:]:", ip); //sdp格式 sprintf(sdp, "v=0\r\n" "o=- 9%ld 1 IN IP4 %s\r\n" "t=0 0\r\n" "a=control:*\r\n" "a=type:broadcast\r\n" "a=rtcp-unicast: reflection\r\n" "m=video %d RTP/AVP 96\r\n" "c=IN IP4 %s/255\r\n" "a=rtpmap:96 H264/90000\r\n" "a=control:track0\r\n", time(NULL), ip, MULTICAST_PORT, MULTICAST_IP); //输出回应消息 sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n" "Content-Base: %s\r\n" "Content-type: application/sdp\r\n" "Content-length: %ld\r\n\r\n" "%s", cseq, url, strlen(sdp), sdp); return 0; } ```