RTSP流媒体服务器的TCP(soket)通信(win系统)
RTSP是在TCP之上实现的应用层协议
TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致
TCP UDP 区别
- 连接方式:
TCP是面向连接的协议,在数据传输前,TCP会通过三次握手建立连接,确保双方都准备好进行通信。
UDP无连接 - 传输可靠性:
通过三次握手和四次挥手,重传机制,保证数据包无差错、不丢失、不重复且按顺序送达
UDP只管传输,别的由上层解决 - 数据结构:
TCP基于字节流,数据之间没有明确划分
UDP基于数据报,每个UDP数据报都是一个独立的信息单元,包含完整的源地址、目的地址以及数据 - 流量控制和拥塞控制
TCP:具有流量控制和拥塞控制机制,以避免网络拥塞和数据丢失。 - 连接对象个数
UDP:支持一对一、一对多、多对一、多对多的通信。
TCP:一对一通信,即一个TCP连接只能在两个端点之间进行。
什么时候使用TCP,什么时候使用UDP
用TCP:文件传输、邮件服务、HTTP 应用,比如发送微信消息,微信语音,就算网络不好,也能安全送到、
用UDP:视频流传输、音频传输、游戏通信,比如微信通话,微信视频通话
TCP的流量控制和拥塞控制机制
- 流量控制(Flow Control):流量控制是指在发送方和接收方之间控制数据传输速率,以便使接收方能够根据自己的处理能力来接收数据。如果接收方的缓冲区已满,它会发送一个或多个零窗口通知(Zero Window Notification)给发送方,告诉发送方暂停发送数据。当接收方处理完数据并有足够的缓冲空间时,它会发送一个窗口更新(Window Update)通知,告诉发送方可以继续发送数据。
- 拥塞控制(Congestion Control):拥塞控制是指在网络中控制数据传输速率,以避免网络拥塞。TCP 使用多种拥塞控制算法,例如:
慢启动(Slow Start):在刚开始发送数据时,TCP 会采用一个很小的初始值作为拥塞窗口(Congestion Window, cwnd),每收到一个 ACK(确认应答),拥塞窗口大小就加 1。当出现拥塞(例如,超时或三个重复 ACK)时,拥塞窗口会被重置为初始值。
拥塞避免(Congestion Avoidance):当拥塞窗口达到慢启动阈值(Slow Start Threshold, ssthresh)时,TCP 会进入拥塞避免阶段,每收到一个 ACK,拥塞窗口大小加 1/cwnd。
快重传(Fast Retransmit):当接收到三个重复 ACK 时,TCP 会假设网络出现了拥塞,并立即重传丢失的数据包,而不必等待超时。
快恢复(Fast Recovery):当进行快重传时,TCP 会将慢启动阈值设置为拥塞窗口的一半,并进入拥塞避免阶段,而不是返回慢启动阶段。
TCP的重传问题
发送方每发送一个数据段就启动一个定时器
如果发送方在定时器到期之前没有收到确认,它会认为数据段可能已经丢失,并触发重传机制。发送方将重传该数据段,并重新启动定时器。
调整定时器:TCP使用一种称为“自适应重传”的技术来调整重传定时器。通过测量往返时间(RTT)并使用这些测量值来估计一个合适的超时时间,TCP可以更准确地判断何时重传数据段。
最大重传次数:为了防止无限循环,TCP有一个最大重传次数(通常是6次),即使超时时间不断增长,超过这个次数后,TCP会认为数据段可能永远丢失,并停止重传
TCP粘包问题
多个数据包粘连到一起无法拆分,是我们的需求过于复杂造成的,不是TCP的问题
解决办法:
使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包
在发送数据块之前, 在数据块最前边添加一个固定大小的数据头, 这时候数据由两部分组成:数据头+数据块
soket通信流程
套接字是一套网络通信的接口,使用这套接口就可以完成网络通信。
字节序
因为在不同的计算机体系中,字节、字等的存储机制有所不同,在通信传输中,双方要达成一致使用什么顺序传输。
主机字节序(小端)
数据的低位字节存储到内存的低地址位, 数据的高位字节存储到内存的高地址位
我们使用的PC机,数据的存储默认使用的是小端
网络字节序(大端)
据的低位字节存储到内存的高地址位, 数据的高位字节存储到内存的低地址位
套接字通信过程中操作的数据都是大端存储的
IP地址转换
主机字节序的IP地址是字符串, 网络字节序IP地址是整形
将大端的整形数, 转换为小端的点分十进制的IP地址
通信中send和receive的为什么会阻塞
使用tcp协议进行通讯的双方,都各自有一个发送缓冲区和一个接收缓冲区。而缓冲区是有大小的,因此发生阻塞的本质原因是缓冲区满了,别的字节流消息无法进入缓冲区。
接收端接收数据的速度小于发送端发送数据的速度,导致接收端的读缓冲区填满,接收端发送报文给发送端告诉他我已经满了,先别发。这样发送端的写缓冲区被占满了,导致阻塞
receive阻塞是因为读缓冲区中没数据。
服务器端
1.创建sokcet。socket()
2.设置地址。struct sockaddr_in service_addr; //设置服务器通信端口
3.绑定。bind();
4.监听。listen();
5.链接。accept();
6.收发消息。read()/send()
7.关闭sokcet。close
客户端
1.创建socket。socket()
2.设置地址。struct sockaddr_in service_addr;
3.链接。connect()
4.收发消息。read()/send()
5.关闭链接。close()
三次握手和四次挥手
在服务器进入监听阻塞状态后,由客户端发送连接请求,开始建立连接,三次握手,对于程序猿来说只需要在客户端调用connect()函数,三次握手就自动进行了
结束连接,四次挥手
三次握手过程说明:
1、由客户端发送建立TCP连接的请求报文,其中报文中包含seq序列号,是由发送端随机生成的,并且将报文中的SYN字段置为1,表示需要建立TCP连接。(SYN=1,seq=x,x为随机生成数值)SYN是一个标志位(Flag),用于在建立TCP连接时进行同步步骤。
2、由服务端回复客户端发送的TCP连接请求报文,其中包含seq序列号,是由服务端随机生成的,并且将SYN置为1,而且会产生ACK字段,ACK字段数值是在客户端发送过来的序列号seq的基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP建立请求已得到验证。(SYN=1,ACK=x+1,seq=y,y为随机生成数值)这里的ack加1可以理解为是确认和谁建立连接。
3、客户端收到服务端发送的TCP建立验证请求后,会使自己的序列号加1表示,并且再次回复ACK验证请求,在服务端发过来的seq上加1进行回复。(SYN=1,ACK=y+1,seq=x+1)
简要说明:
- 有客户端发起TCP连接请求,发送一个随机序列号,和一个标志位
- 服务器回复客户端,发送一个确认字段,确认字段是客户端发的随机序列+1,还发送一个随机序列和和标志位
- 客户端收到验证请求回复服务器,发送服务器发的序列+1和自己的序列+1
为什么三次握手
当客户端发出第一个连接请求报文段时并没有丢失,而是在某个网络节点出现了长时间的滞留,以至于延误了连接请求在某个时间之后才到达服务器。这应该是一个早已失效的报文段。但是服务器在收到此失效的连接请求报文段后,以为是客户端的一个新请求,于是向客户端发出了确认报文段,同意建立连接。
这时新的连接已经建立了,但是由于客户端没有发出建立连接的请求,因此不会管服务器的确认,也不会向服务器发送数据,但服务器却以为新的运输连接已经建立,一直在等待,所以,服务器的资源就白白浪费掉了。
四次挥手过程说明:
1、客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成)
2、服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成)
3、服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1,ACK=x+1,seq=z,z由服务端随机生成)
4、客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq字段和ACK字段,ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)
至此TCP断开的4次挥手过程完毕
为什么要四次挥手
TCP是基于全双工通信的,所以双方都可以主动释放连接。
四次挥手的意义就在于,当客户端发送完最后一条数据之后,但可能B服务器还有未发送给客户端的数据。
所以客户端在发送完收据后可以请求释放连接,此时服务器给客户端响应,告诉客户端知道你想断开连接,此时客户端还可以继续接收服务器发送的信息。
在服务器处理完工作后,也请求释放连接。客户端同意后,就断开连接。
这样可以保证数据正常可靠的交互。
1.头文件和宏定义
#include<iostream>
#include <stdio.h> //标准输入输出的头文件
#include <stdlib.h> //标准库函数的头文件
#include <stdint.h> //标准整型数据类型的头文件
#include <string.h>
#include <time.h>
#include <sys/types.h> //和下面两个文件和文件描述符处理的头文件
#include <sys/stat.h>
#include <fcntl.h>
#include <WinSock2.h> //和下面一个 ,Windows 下网络编程的头文件
#include <WS2tcpip.h>
#include <windows.h> //Windows API 的头文件
#include <string>
#pragma comment(lib, "ws2_32.lib") //这行代码告诉链接器去包含 ws2_32.lib 这个库,这是进行 Winsock 编程必需的库
#pragma warning( disable : 4996 ) //禁止了对特定警告号码 4996 的警告,这个警告通常与使用被认为不安全的函数相关
#define SERVER_PORT 8554 //代码定义了一个宏,表示服务器的端口号
#define SERVER_RTP_PORT 55532 //定义了宏,表示服务器用于 RTP 的端口号
#define SERVER_RTCP_PORT 55533 //定义了宏,表示服务器用于 RTCP 的端口号
using namespace std;
2.套接字函数
static int createTcpSocket() //创建一个 TCP 套接字
{
int sockfd;//套接字描述符
int on = 1;
sockfd = socket(AF_INET, SOCK_STREAM, 0);//调用了 socket() 函数,创建一个 IPv4 地址族(AF_INET)的 TCP 套接字(SOCK_STREAM)
if (sockfd < 0)
return -1; //则返回 -1,表示创建套接字失败
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));//这一行调用了 setsockopt() 函数,设置套接字选项。
//具体来说,它启用了 SO_REUSEADDR 选项,允许在套接字关闭后立即重用该地址。这可以避免在关闭套接字后的一段时间内无法立即重新绑定相同地址的情况。
cout << "sockfd: " << sockfd << endl;
return sockfd;
}
static int bindSocketAddr(int sockfd, const char* ip, int port) //将套接字绑定到指定的 IP 地址和端口上 //接收sockfd 和 连接的客户端的 IP 地址和 端口号
{
struct sockaddr_in addr; //定义了一个 sockaddr_in 结构体类型的变量 addr,用于存储要绑定的 IP 地址和端口信息。
addr.sin_family = AF_INET; //设置了地址族为 IPv4(AF_INET)
addr.sin_port = htons(port); //将端口号转换为网络字节序(Big-endian),并存储在 addr.sin_port 中
addr.sin_addr.s_addr = inet_addr(ip); //这一行将 IP 地址字符串转换为网络字节序的二进制形式,并存储在 addr.sin_addr.s_addr 中
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr)) < 0) //调用 bind() 函数,将套接字绑定到指定的地址和端口上
//sockfd:套接字描述符 ;sockaddr 是一个通用的套接字地址结构 ;地址结构的大小
return -1; //绑定失败返回 -1
return 0;
}
static int acceptClient(int sockfd, char* ip, int* port) //监听套接字 sockfd 接受一个客户端连接,并获取该客户端的 IP 地址和端口号
{
int clientfd; //新接受的客户端套接字描述符
socklen_t len = 0;
struct sockaddr_in addr; //定义一个 IPv4 地址结构来存储客户端地址
memset(&addr, 0, sizeof(addr));//清零地址结构
len = sizeof(addr);
clientfd = accept(sockfd, (struct sockaddr*)&addr, &len); //accept 函数尝试接受一个连接请求,如果成功,返回一个新的套接字描述符(clientfd),用于与新连接的客户端通信。
if (clientfd < 0)//如果失败,返回值小于0
return -1;
strcpy(ip, inet_ntoa(addr.sin_addr));//将网络字节顺序的 IP 地址转换为点分十进制字符串形式,并复制到 ip 参数指向的位置
*port = ntohs(addr.sin_port);
return clientfd;
}
3.处理RTSP命令的函数
static int handleCmd_OPTIONS(char* result, int cseq) //用于处理 RTSP(实时流协议)服务器接收到的 OPTIONS 命令 //int cseq:代表客户端请求的序列号,这是 RTSP 协议中用于标识请求顺序的数字
{
sprintf(result, "RTSP/1.0 200 OK\r\n" //构建RTSP响应,sprintf 函数用于将格式化的数据写入字符串
"CSeq: %d\r\n"
"Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n" //表明此服务器支持的 RTSP 命令。这些命令分别用于:查询服务器功能(OPTIONS)、获取媒体描述(DESCRIBE)、设置媒体传输参数(SETUP)和开始媒体流播放(PLAY)
"\r\n",
cseq);
return 0;
}
static int handleCmd_DESCRIBE(char* result, int cseq, char* url)//处理 RTSP(实时流协议)服务器接收到的 DESCRIBE 命令
{
char sdp[500];
char localIp[100]; //定义两个字符数组 sdp 和 localIp,分别用于存储 SDP 信息和解析出的本地 IP 地址
sscanf(url, "rtsp://%[^:]:", localIp); //sscanf 函数从 url 中解析出主机部分的 IP 地址,并存储在 localIp 中
sprintf(sdp, "v=0\r\n" //构建 SDP 格式数据
"o=- 9%ld 1 IN IP4 %s\r\n"
"t=0 0\r\n"
"a=control:*\r\n"
"m=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=control:track0\r\n",
time(NULL), localIp);
sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n" //构建包含 SDP 数据的 RTSP 响应
"Content-Base: %s\r\n"
"Content-type: application/sdp\r\n"
"Content-length: %zu\r\n\r\n"
"%s",
cseq,
url,
strlen(sdp),
sdp);
return 0;
}
static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort) //处理 RTSP(实时流协议)服务器接收到的 SETUP 命令 //客户端用于 RTP(实时传输协议)的端口号
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
"Session: 66334873\r\n"
"\r\n",
cseq,
clientRtpPort,
clientRtpPort + 1,//客户端 PTP 和PTCP 端口
SERVER_RTP_PORT,
SERVER_RTCP_PORT); //服务器 PTP 和PTCP 端口
return 0;
}
static int handleCmd_PLAY(char* result, int cseq) //处理 RTSP(实时流协议)服务器接收到的 PLAY 命令
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Range: npt=0.000-\r\n"
"Session: 66334873; timeout=10\r\n\r\n",
cseq);
return 0;
}
static void doClient(int clientSockfd, const char* clientIP, int clientPort) //处理通过 RTSP(实时流协议)从客户端收到的命令
{
char method[40];
char url[100];
char version[40];
int CSeq;
int clientRtpPort, clientRtcpPort;
char* rBuf = (char*)malloc(10000);
char* sBuf = (char*)malloc(10000);
//定义用于存储解析结果的变量:方法名、URL、协议版本和命令序列号。
//定义 RTP 和 RTCP 端口号。
//分配用于接收和发送数据的缓冲区
while (true) {
int recvLen;
recvLen = recv(clientSockfd, rBuf, 2000, 0); //recv 函数返回接收到的字节数。如果客户端关闭了连接,recv 会返回0。如果发生错误,如网络中断等,recv 会返回-1
/*
函数recv(int sockfd, void *buf, size_t len, int flags);
sockfd:套接字文件描述符,用于标识一个已连接的套接字。
buf:指向一个缓冲区的指针,该缓冲区用于接收从套接字传入的数据。
len:缓冲区的长度,指定最大可以接收的数据量。
flags:通常设置为0,用于控制接收行为的各种选项
*/
if (recvLen <= 0) {
break; //如果接收长度小于等于 0(意味着客户端断开连接或发生错误),则跳出循环。
}
rBuf[recvLen] = '\0';//收到的数据结尾加null符
std::string recvStr = rBuf;
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
printf("%s rBuf = %s \n",__FUNCTION__,rBuf);//输出接收到的完整 RTSP 请求数据
const char* sep = "\n";
char* line = strtok(rBuf, sep); //将字符串 rBuf 根据分隔符 sep 进行分割
while (line) {
if (strstr(line, "OPTIONS") || //在字符串 line 中查找子字符串 "OPTIONS"。如果找到了,函数会返回该子字符串在原字符串中的位置;如果找不到,则返回 NULL
strstr(line, "DESCRIBE") ||
strstr(line, "SETUP") ||
strstr(line, "PLAY")) {
if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3)
{
//从字符串 line 中按照格式 "%s %s %s\r\n" 提取数据,并将提取的数据存储到 method、url 和 version 这三个字符串变量中
// %s 是格式控制符,表示提取一个字符串,遇到空格、制表符或换行符时停止
// sscanf() 函数在成功提取并赋值给参数的数量与指定的格式相匹配时,会返回成功提取并赋值的参数数量
// error
}
}
else if (strstr(line, "CSeq"))
{
if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1)
{
// error
}
}
else if (!strncmp(line, "Transport:", strlen("Transport:")))//strncmp 函数用于比较两个字符串的前 n 个字符int strncmp(const char *s1, const char *s2, size_t n);
{
// Transport: RTP/AVP/UDP;unicast;client_port=13358-13359
// Transport: RTP/AVP;unicast;client_port=13358-13359
if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n",
&clientRtpPort, &clientRtcpPort) != 2) {
// error
printf("parse Transport error \n");
}
}
line = strtok(NULL, sep);//根据分隔符 sep 进行分割
}
/*
对于每种 RTSP 命令(OPTIONS, DESCRIBE, SETUP, PLAY),服务器进行如下处理:
OPTIONS:通常返回服务器支持的方法列表。
DESCRIBE:返回请求的媒体资源的描述。
SETUP:建立会话和传输参数,准备数据传输。
PLAY:开始流媒体数据的发送。
*/
if (!strcmp(method, "OPTIONS"))//在 C 语言中,strcmp 函数用于比较两个字符串是否相等 true 被定义为 1,而 false 被定义为 0
//strcmp 函数会比较两个字符串,如果相等则返回 0。这里的条件 !strcmp(method, "OPTIONS") 表示如果 method 等于 "OPTIONS",则条件成立。
{
if (handleCmd_OPTIONS(sBuf, CSeq))//调用相应的处理函数,检查返回值
{
printf("failed to handle options\n");
break;
}
}
else if (!strcmp(method, "DESCRIBE")) {
if (handleCmd_DESCRIBE(sBuf, CSeq, url))
{
printf("failed to handle describe\n");
break;
}
}
else if (!strcmp(method, "SETUP")) {
if (handleCmd_SETUP(sBuf, CSeq, clientRtpPort))
{
printf("failed to handle setup\n");
break;
}
}
else if (!strcmp(method, "PLAY")) {
if (handleCmd_PLAY(sBuf, CSeq))
{
printf("failed to handle play\n");
break;
}
}
else {
printf("未定义的method = %s \n", method);
break;
}
printf("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
printf("%s sBuf = %s \n", __FUNCTION__, sBuf);// 输出并发送构建的响应
send(clientSockfd, sBuf, strlen(sBuf), 0); //将响应发送回客户端。
//开始播放,发送RTP包
if (!strcmp(method, "PLAY"))
{
printf("start play\n");
printf("client ip:%s\n", clientIP);
printf("client port:%d\n", clientRtpPort);
while (true) {
Sleep(40); //每次循环迭代之间会暂停40毫秒
//usleep(40000);//1000/25 * 1000
}
break;
}
memset(method,0,sizeof(method)/sizeof(char));
memset(url,0,sizeof(url)/sizeof(char));
CSeq = 0;
}
closesocket(clientSockfd);
free(rBuf);
free(sBuf);
}
4.主函数
int main(int argc, char* argv[])
{
// 启动windows socket start //初始化
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("PC Server Socket Start Up Error \n");
return -1;
}
// 启动windows socket end
int serverSockfd;
serverSockfd = createTcpSocket(); //创建套接字
if (serverSockfd < 0)
{
WSACleanup();
printf("failed to create tcp socket\n");
return -1;
}
if (bindSocketAddr(serverSockfd, "0.0.0.0", SERVER_PORT) < 0)//将套接字绑定到 IP 地址和端口。这里使用 "0.0.0.0" 表示监听所有可用网络接口
{
printf("failed to bind addr\n");
return -1;
}
if (listen(serverSockfd, 10) < 0)//使套接字进入监听状态,等待客户端连接。10 是挂起连接队列的最大长度
{
printf("failed to listen\n");
return -1;
}
printf("%s rtsp://127.0.0.1:%d\n", __FILE__, SERVER_PORT);//输出服务器的 RTSP URL 和端口,便于客户端知道如何连接到此服务器
while (true) // 循环接受客户端连接
{
int clientSockfd;
char clientIp[40];
int clientPort;
clientSockfd = acceptClient(serverSockfd, clientIp, &clientPort); // 接受一个客户端连接,并返回一个新的套接字用于与客户端通信。同时获取客户端的 IP 和端口
if (clientSockfd < 0)
{
printf("failed to accept client\n");
return -1;
}
printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);
doClient(clientSockfd, clientIp, clientPort);//对于每个连接的客户端,调用 doClient 函数进行进一步处理
}
closesocket(serverSockfd);//在服务器关闭时,清理打开的套接字资源
return 0;
}
5.结果
C:\Users\YDJ>ffplay -i rtsp://127.0.0.1:8554
以ffplay作为客户端向服务器发送请求,播放指定地址(127.0.0.1:8554)的 RTSP 流
对于每种 RTSP 命令(OPTIONS, DESCRIBE, SETUP, PLAY),服务器进行如下处理:
OPTIONS:通常返回服务器支持的方法列表。
DESCRIBE:返回请求的媒体资源的描述。
SETUP:建立会话和传输参数,准备数据传输。
PLAY:开始流媒体数据的发送。
F:\RTSP\BXC_RtspServer_study\study1\main.cpp rtsp://127.0.0.1:8554
accept client;client ip:127.0.0.1,client port:65121
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> //服务器接收
doClient rBuf = OPTIONS rtsp://127.0.0.1:8554 RTSP/1.0
CSeq: 1
User-Agent: Lavf60.3.100
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< //服务器发送
doClient sBuf = RTSP/1.0 200 OK
CSeq: 1
Public: OPTIONS, DESCRIBE, SETUP, PLAY
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
doClient rBuf = DESCRIBE rtsp://127.0.0.1:8554 RTSP/1.0
Accept: application/sdp
CSeq: 2
User-Agent: Lavf60.3.100
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
doClient sBuf = RTSP/1.0 200 OK
CSeq: 2
Content-Base: rtsp://127.0.0.1:8554
Content-type: application/sdp //application/sdp 表示发送的是SDP(Session Description Protocol)内容。SDP通常用于描述多媒体会话的参数
Content-length: 125
v=0
o=- 91716020758 1 IN IP4 127.0.0.1
t=0 0
a=control:*
m=video 0 RTP/AVP 96 //这表示这是一个视频流,这是一个udp连接,0表示RTP的端口
a=rtpmap:96 H264/90000 // 这说明了RTP流中标识符为96的媒体使用的编码格式是 H.264,采样率为90000。
a=control:track0 //一路视频流控制通道
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
doClient rBuf = SETUP rtsp://127.0.0.1:8554/track0 RTSP/1.0
Transport: RTP/AVP/UDP;unicast;client_port=30110-30111 //一路视频流要建立RTP和RTCP传输,所以要两个端口
CSeq: 3
User-Agent: Lavf60.3.100
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
doClient sBuf = RTSP/1.0 200 OK
CSeq: 3
Transport: RTP/AVP;unicast;client_port=30110-30111;server_port=55532-55533
Session: 66334873
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
doClient rBuf = PLAY rtsp://127.0.0.1:8554 RTSP/1.0
Range: npt=0.000-
CSeq: 4
User-Agent: Lavf60.3.100
Session: 66334873
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
doClient sBuf = RTSP/1.0 200 OK
CSeq: 4
Range: npt=0.000-
Session: 66334873; timeout=10
start play
client ip:127.0.0.1
client port:30110