欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

2.2 RTSP协议

程序员文章站 2022-07-06 22:29:01
...

我们先把github上的项目文件先下载下来,根据代码,调试输出信息及抓包数据结合分析,这样更容易看清楚整个协议。

传送门:Hi3518+RTSP

用wireshark软件打开0106.pcapng,发现有一堆的抓包数据,我们点击上面的过滤栏里右边的表达式,选择RTSP,再点击旁边的箭头,就会过滤出RTSP包,界面如下图:
2.2 RTSP协议
我们发现RTSP协议还是很简单很有规律的,其握手过和就是客户端(VLC)发个请求,服务器(HI3518)回应一下。手画握手过程如下:

2.2 RTSP协议

以下内容是我学习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协议图和抓包数据理解起来一般是没什么问题。

相关标签: 海思项目