2.2 RTSP协议
我们先把github上的项目文件先下载下来,根据代码,调试输出信息及抓包数据结合分析,这样更容易看清楚整个协议。
传送门:Hi3518+RTSP
用wireshark软件打开0106.pcapng,发现有一堆的抓包数据,我们点击上面的过滤栏里右边的表达式,选择RTSP,再点击旁边的箭头,就会过滤出RTSP包,界面如下图:
我们发现RTSP协议还是很简单很有规律的,其握手过和就是客户端(VLC)发个请求,服务器(HI3518)回应一下。手画握手过程如下:
以下内容是我学习RTSP协议的时候找到的资料,学习对比时发现总结得很好,但出处忘记来自哪里了,可以借鉴以下。
Live555库是一个使用开放标准协议如RTP/RTCP、RTSP、SIP等实现多媒体流式传输的开源C 库集。这些函数库可以在Unix、Windows、QNX等操作系统下编译使用,基于此建立RTSP/SIP服务器和客户端来实现多媒体流的传输。下面给出具体实现过程[4]:
(1)客户端发起RTSP OPTION请求,目的是得到服务器提供什么方法。RTSP提供的方法一般包括OPTIONS、DESCRIBE、SETUP、TEARDOWN、PLAY、PAUSE、SCALE、GET_PARAMETER。
(2)服务器对RTSP OPTION回应,服务器实现什么方法就回应哪些方法。在此系统中,我们只对DESCRIBE、SETUP、TEARDOWN、PLAY方法做了实现。
(3)客户端发起RTSP DESCRIBE请求,服务器收到的信息主要有媒体的名字,解码类型,视频分辨率等描述,目的是为了从服务器那里得到会话描述信息(SDP)。
(4)服务器对RTSP DESCRIBE响应,发送必要的媒体参数,在传输H.264文件时,主要包括SPS/PPS、媒体名、传输协议等信息。
(5)客户端发起RTSP SETUP请求,目的是请求会话建立并准备传输。请求信息主要包括传输协议和客户端端口号。
(6)服务器对RTSP SETUP响应,发出相应服务器端的端口号和会话标识符。
(7)客户端发出了RTSP PLAY的请求,目的是请求播放视频流。
(8)服务器对RTSP PLAY响应,响应的消息包括会话标识符,RTP包的***,时间戳。此时服务器对H264视频流封装打包进行传输。
(9)客户端发出RTSP TEARDOWN请求,目的是关闭连接,终止传输。
(10)服务器关闭连接,停止传输。
以上我们知道RTSP协议的握手顺序,具体分析还要看代码。我们前面说过main线程里EventLoop()函数进行检查是否有RTSP连接请求。
主函数循环:接受客户端请求 + 处理ScheduleConnections
//输入为之前s32MainFd = tcp_listen(SERVER_RTSP_PORT_DEFAULT);创建的服务器套接字
void EventLoop(int s32MainFd)
{
// static unsigned int s32ChdCnt=0;
static int s32ConCnt = 0;//已经连接的客户端数
int s32Fd = -1;
static RTSP_buffer *pRtspList=NULL;
RTSP_buffer *p=NULL;
unsigned int u32FdFound;
/*接收连接,创建一个新的socket*/
if (s32ConCnt!=-1){
/*接收新连接,创建一个新的socket,返回其描述符*/
//无阻塞查询是否有连接,有连接时返回值>0,无连接时返回值<0
s32Fd= tcp_accept(s32MainFd); //非阻塞
}
/*处理新创建的连接*/
if (s32Fd >= 0){
/*查找列表中是否存在此连接的socket*/
for (u32FdFound=0,p=pRtspList; p!=NULL; p=p->next)
{
if (p->fd == s32Fd)
{
u32FdFound=1;
break;
}
}
if (!u32FdFound)
{
/*创建一个连接,增加一个客户端*/
if (s32ConCnt<MAX_CONNECTION){
++s32ConCnt;
//将新连接添加到链表里,并初始化其会话
AddClient(&pRtspList,s32Fd);
}
else
{
fprintf(stderr, "exceed the MAX client, ignore this connecting\n");
return;
}
num_conn++;
fprintf(stderr, "%s Connection reached: %d\n", __FUNCTION__, num_conn);
}
}
/*对已有的连接进行调度*/
ScheduleConnections(&pRtspList,&s32ConCnt);
}
EventLoop()
1、如果由tcp_accept调用网络编程accept函数(无阻塞),如果不在列表中就AddClient添加进客户端列表(malloc添加到链表尾部)
2、ScheduleConnections(&pRtspList,&s32ConCnt);
ScheduleConnections函数
void ScheduleConnections(RTSP_buffer **rtsp_list, int *conn_count)
{
int res;
RTSP_buffer *pRtsp=*rtsp_list,*pRtspN=NULL;
RTP_session *r=NULL, *t=NULL;
#ifdef RTSP_DEBUG
// fprintf(stderr, "%s\n", __FUNCTION__);
#endif
while (pRtsp!=NULL)
{
if ((res = RtspServer(pRtsp))!=ERR_NOERROR)
{
if (res==ERR_CONNECTION_CLOSE || res==ERR_GENERIC)
{
/*连接已经关闭*/
if (res==ERR_CONNECTION_CLOSE)
fprintf(stderr,"fd:%d,RTSP connection closed by client.\n",pRtsp->fd);
else
fprintf(stderr,"fd:%d,RTSP connection closed by server.\n",pRtsp->fd);
/*客户端在发送TEARDOWN 之前就截断了连接,但是会话却没有被释放*/
if (pRtsp->session_list!=NULL)
{
r=pRtsp->session_list->rtp_session;
/*释放所有会话*/
while (r!=NULL)
{
t = r->next;
RtpDelete((unsigned int)(r->hndRtp));
schedule_remove(r->sched_id);
r=t;
}
/*释放链表头指针*/
free(pRtsp->session_list);
pRtsp->session_list=NULL;
g_s32DoPlay--;
if (g_s32DoPlay == 0)
{
printf("user abort! no user online now resetfifo\n");
ringreset;
/* 重新将所有可用的RTP端口号放入到port_pool[MAX_SESSION] 中 */
RTP_port_pool_init(RTP_DEFAULT_PORT);
}
fprintf(stderr,"WARNING! fd:%d RTSP connection truncated before ending operations.\n",pRtsp->fd);
}
// wait for
close(pRtsp->fd);
--*conn_count;
num_conn--;
/*释放rtsp缓冲区*/
if (pRtsp==*rtsp_list)
{
//链表第一个元素就出错,则pRtspN为空
printf("first error,pRtsp is null\n");
*rtsp_list=pRtsp->next;
free(pRtsp);
pRtsp=*rtsp_list;
}
else
{
//不是链表中的第一个,则把当前出错任务删除,并把next任务存放在pRtspN(上一个没有出错的任务)
//指向的next,和当前需要处理的pRtsp中.
printf("dell current fd:%d\n",pRtsp->fd);
pRtspN->next=pRtsp->next;
free(pRtsp);
pRtsp=pRtspN->next;
printf("current next fd:%d\n",pRtsp->fd);
}
/*适当情况下,释放调度器本身*/
if (pRtsp==NULL && *conn_count<0)
{
fprintf(stderr,"to stop cchedule_do thread\n");
stop_schedule=1;
}
}
else
{
printf("current fd:%d\n",pRtsp->fd);
pRtsp = pRtsp->next;
}
}
else
{
//printf("6\r\n");
//没有出错
//上一个处理没有出错的list存放在pRtspN中,需要处理的任务放在pRtst中
pRtspN = pRtsp;
pRtsp = pRtsp->next;
}
}
}
函数后面部分代码主要是连接中断后对rtsp连接的处理及清理工作,重要的还是RtspServer函数,代码如下:
int RtspServer(RTSP_buffer *rtsp)
{
fd_set rset,wset; /*读写I/O描述集*/
struct timeval t;
int size;
static char buffer[RTSP_BUFFERSIZE+1]; /* +1 to control the final '\0'*/
int n;
int res;
struct sockaddr ClientAddr;
#ifdef RTSP_DEBUG
// fprintf(stderr, "%s, %d\n", __FUNCTION__, __LINE__);
#endif
// memset((void *)&ClientAddr,0,sizeof(ClientAddr));
if (rtsp == NULL)
return ERR_NOERROR;
/*变量初始化*/
FD_ZERO(&rset);
FD_ZERO(&wset);
t.tv_sec=0; /*select 时间间隔*/
t.tv_usec=100000;
FD_SET(rtsp->fd,&rset);
/*调用select等待对应描述符变化*/
if (select(g_s32Maxfd+1,&rset,0,0,&t)<0)
{
fprintf(stderr,"select error %s %d\n", __FILE__, __LINE__);
send_reply(500, NULL, rtsp);
return ERR_GENERIC; //errore interno al server
}
/*有可供读进的rtsp包*/
if (FD_ISSET(rtsp->fd,&rset))
{
memset(buffer,0,sizeof(buffer));
size=sizeof(buffer)-1; /*最后一位用于填充字符串结束标识*/
/*读入数据到缓冲区中*/
#ifdef RTSP_DEBUG
// fprintf(stderr, "tcp_read, %d\n", __LINE__);
#endif
//读取客户端RTSP包的IP,PORT及判断RTSP包的大小和内容
//返回0:关闭 返回负数:错误 大于0:表示接收数据的大小
n= tcp_read(rtsp->fd, buffer, size, &ClientAddr);
if (n==0){
return ERR_CONNECTION_CLOSE;
}
if (n<0){
fprintf(stderr,"read() error %s %d\n", __FILE__, __LINE__);
send_reply(500, NULL, rtsp); //服务器内部错误消息
return ERR_GENERIC;
}
//检查读入的数据是否产生溢出
if (rtsp->in_size+n>RTSP_BUFFERSIZE)
{
fprintf(stderr,"RTSP buffer overflow (input RTSP message is most likely invalid).\n");
send_reply(500, NULL, rtsp);
return ERR_GENERIC;//数据溢出错误
}
#ifdef RTSP_DEBUG
//fprintf(stderr,"INPUT_BUFFER was:%s\n", buffer);
#endif
/*填充数据*/
memcpy(&(rtsp->in_buffer[rtsp->in_size]),buffer,n);
rtsp->in_size+=n;
//清空buffer
memset(buffer, 0, n);
//添加客户端地址信息
memcpy( &rtsp->stClientAddr, &ClientAddr, sizeof(ClientAddr));
//对接收到的RTSP包进行方法判断,然后根据方法进行状态机处理
if ((res=RTSP_handler(rtsp))==ERR_GENERIC)
{
fprintf(stderr,"Invalid input message.\n");
return ERR_NOERROR;
}
}
/*有发送数据*/
if (rtsp->out_size>0)
{
//将数据发送出去
n= tcp_write(rtsp->fd,rtsp->out_buffer,rtsp->out_size);
printf("5\r\n");
if (n<0)
{
fprintf(stderr,"tcp_write error %s %i\n", __FILE__, __LINE__);
send_reply(500, NULL, rtsp);
return ERR_GENERIC; //errore interno al server
}
#ifdef RTSP_DEBUG
//fprintf(stderr,"OUTPUT_BUFFER length %d\n%s\n", rtsp->out_size, rtsp->out_buffer);
#endif
//清空发送缓冲区
memset(rtsp->out_buffer, 0, rtsp->out_size);
rtsp->out_size = 0;
}
//如果需要RTCP在此出加入对RTCP数据的接收,并存放在缓存中。
//继而在schedule_do线程中对其处理。
//rtcp控制处理,检查读入RTCP数据报
return ERR_NOERROR;
}
在这个函数里用select进行多路IO复用,没有RTSP请求时休眠,有RTSP请求时tcp_read请求包(实际会调用网络编程recv函数),如果读到的RTSP请求包正常就会调用RTSP_handler函数用状态机进行握手连接,如果正常就会将Reply信息填充到rtsp->out_buffer区域,然后调用tcp_write(其实是调用网络编程的send函数)将Reply信息发送出去。
RTSP_handler函数分两个步
1、将包内容分离并判断????调用RTSP_validate_method函数
2、根据分离的信息用状态机状态迁移完成通信
int RTSP_validate_method(RTSP_buffer * pRtsp)
{
char method[32], hdr[16];
char object[256];
char ver[32];
unsigned int seq;
int pcnt; /* parameter count */
int mid = ERR_GENERIC;
char *p; //=======增加
char trash[255]; //===增加
*method = *object = '\0';
seq = 0;
printf("");
/*按照请求消息的格式解析消息的第一行*/
//sscanf,读取格式化的字符串中的数据,失败返回0 ,否则返回格式化的参数个数
// 将pRtsp->in_buffer字符串格式化到method, object, ver, hdr中
//分隔标志:以空格分隔或最大长度分隔(二者满足一个即可)
//OPTION rtsp://192.168.1.10 RTST/1.0 CSeq: 2 User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
// if ( (pcnt = sscanf(pRtsp->in_buffer, " %31s %255s %31s\n%15s", method, object, ver, hdr, &seq)) != 5){
if ( (pcnt = sscanf(pRtsp->in_buffer, " %31s %255s %31s\n%15s", method, object, ver, hdr)) != 4){
printf("========\n%s\n==========\n",pRtsp->in_buffer);
printf("%s ",method);
printf("%s ",object);
printf("%s ",ver);
printf("hdr:%s\n",hdr);
return ERR_GENERIC;
}
/*如果没有头标记,则错误*/
/*
if ( !strstr(hdr, HDR_CSEQ) ){
printf("no HDR_CSEQ err_generic");
return ERR_GENERIC;
}
*/
//===========加
if ((p = strstr(pRtsp->in_buffer, "CSeq")) == NULL) {
return ERR_GENERIC;
}else {
if(sscanf(p,"%254s %d",trash,&seq)!=2){
return ERR_GENERIC;
}
}
//==========
//RTST协议顺序:看RTSP_ID_ANNOUNCE等宏定义顺序
/*根据不同的方法,返回响应的方法ID*/
if (strcmp(method, RTSP_METHOD_DESCRIBE) == 0) {
mid = RTSP_ID_DESCRIBE;
}
if (strcmp(method, RTSP_METHOD_ANNOUNCE) == 0) {
mid = RTSP_ID_ANNOUNCE;
}
if (strcmp(method, RTSP_METHOD_GET_PARAMETERS) == 0) {
mid = RTSP_ID_GET_PARAMETERS;
}
if (strcmp(method, RTSP_METHOD_OPTIONS) == 0) {
mid = RTSP_ID_OPTIONS;
}
if (strcmp(method, RTSP_METHOD_PAUSE) == 0) {
mid = RTSP_ID_PAUSE;
}
if (strcmp(method, RTSP_METHOD_PLAY) == 0) {
mid = RTSP_ID_PLAY;
}
if (strcmp(method, RTSP_METHOD_RECORD) == 0) {
mid = RTSP_ID_RECORD;
}
if (strcmp(method, RTSP_METHOD_REDIRECT) == 0) {
mid = RTSP_ID_REDIRECT;
}
if (strcmp(method, RTSP_METHOD_SETUP) == 0) {
mid = RTSP_ID_SETUP;
}
if (strcmp(method, RTSP_METHOD_SET_PARAMETER) == 0) {
mid = RTSP_ID_SET_PARAMETER;
}
if (strcmp(method, RTSP_METHOD_TEARDOWN) == 0) {
mid = RTSP_ID_TEARDOWN;
}
/*设置当前方法的请求****/
pRtsp->rtsp_cseq = seq;
return mid;
}
在这个函数里用sscanf分离发来的数据包,用strcmp来跟特定字符比较判断发来的是什么数据包。这两个函数用法可百度,也可看代码注释,里面有很详细的注释并且举例说明,不再赘述。第二步是调用RTSP_state_machine函数,这个函数里面用case语句判断状态,并作不同的处理,代码简单,也不再详细赘述。这里面添加了一些调试打印信息,在session0106.log也有输出,更易于理解。
总的来说,程序就是在线程里接收到数据包,然后分离数据包,再根据状态机进行不同处理,再发送应答,结合RTSP协议图和抓包数据理解起来一般是没什么问题。