linux网络编程之——RTSP实时传输协议

 

socket信息数据结构

(1)数据结构:

struct sockaddr_in

{

 short int sa_family; /*地址族*/   地址族,区分是IPV4协议和IPV6协议(AF_INET和AF_INET6) 

unsigned short int sin_port; /*端口号*/

struct in_addr sin_addr; /*IP地址*/

unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/

};

struct in_addr

{

unsigned long int  s_addr; /* 32位IPv4地址,网络字节序 */

};

(2)数据存储优先顺序的转换

头文件:#include <arpa/inet.h>

htons(),ntohs(),htonl()和ntohl().

如果称某个系统所采用的字节序为主机字节序,则它可能是小端模式的,也可能是大端模式的。而端口号和IP地址都是以网络字节序存储的,不是主机字节序,网络字节序都是大端模式。要把主机字节序和网络字节序相互对应起来,需要对这两个字节存储优先顺序进行相互转化。这四个函数分别实现网络字节序和主机字节序的转化

eg:对于内存中存放的数0x12345678来说

如果是采用大端模式存放的,则其真实的数是:0x12345678

如果是采用小端模式存放的,则其真实的数是:0x78563412

例如 servaddr.sin_addr.s_addr = htonl(INADDR_ANY)     网络字节序定义一个值时,是大端模式定义的0x12345678,如果主机是小端模式,存入0x12345678,要用htonl函数会转换数据后变成0x78563412放入小端存储的主机内存,这样下次采用小端模式从内存里读出来的值才是0x12345678

(3)数据结构使用

注意:在绑定套接字前,通常定义一个结构体struct sockaddr_in变量,用bzero函数对结构体内容清0,在对每个成员进行初始化,最后再强制类型转换为(struct sockaddr *)的指针传入绑定参数即可。具体初始化过程如下,以服务器为例

struct sockadddr_in servaddr;

bzero(&;servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;                                  // IPV4协议

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);     // INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址表示“任意地址”。

servaddr.sin_port = htons(554);                                  //端口号

inet_addr、inet_ntoa

(1)函数原型:unsigned long int inet_addr(const char *cp);

参数:

cp:放置如“192.168.1.12”的点分十进制地址(字符串形式)

返回值:执行成功,返回对应的二进制数;执行出错,返回-1

使用:servaddr.sin_addr = inet_addr("192.168.1.111");

(2)函数原型char* inet_ntoa(struct in_addr in);

使用:inet_ntoa(servaddr.sin_addr)

细节:为了简化编程一般将IP地址设置为INADDR_ANY,但是如果需要使用特定的IP地址则需要使用inet_addr 和inet_ntoa完成字符串和in_addr结构体的互换,in_addr是sockaddr_in成员,其代表IP地址。

2、socket编程

前言:开启服务器线程pthread_create(&threadId, NULL, RtspServerListen, NULL);

RtspServerListen线程函数建立服务器与客户端的连接

①socket:生成一个套接口描述符

函数原型:int socket(int domain,int type,int protocol);

头文件:#include <sys/socket.h>

函数的作用:创建新的socket套接字

参数:

domain:表示使用的地址类型,AF_INET表示IPV4,AF_INET6表示IPV6

type:①SOCK_STREAM:面向数据流(TCP)

        ②SOCK_DGRAM:使用不连续不可信赖的数据包连接(UDP)

        ③SOCK_RAW:提供原始网络协议存取

protocol:指定协议,0,表示使用默认的协议,但是要是使用raw socket协议,protocol就不能简单设为0,要与type参数匹配.

返回值:执行成功,返回套接字的描述符;执行失败,返回-1

使用:s32Socket = socket(AF_INET, SOCK_STREAM, 0);

②bind:用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联

函数原型:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

头文件:#include <sys/socket.h>

               #include <sys/types.h>

函数的作用:将第一步创建的s32Socket绑定IP地址和端口号

参数:

sockfd:套接字描述符

addr:服务器或者客户端自己的地址信息(协议族、IP、端口号)

addrlen:struct sockaddr结构的长度

返回值:执行成功,返回0;执行出错,返回-1.

使用:bind(s32Socket, (struct sockaddr *)&servaddr, sizeof(struct sockaddr_in));

③listen:使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接

函数原型:int listen(int sockfd,int backlog);

头文件:#include <sys/socket.h>

函数的作用:监听网络,等待连接

参数:

sockfd:套接字描述符

backlog:允许接入的客户端数目

返回值:执行从成功 ,返回0;执行出错,返回-1.

注意:listen并没有开始接收连线,只是设置socket的listenz模式,真正连线的是在accept中

 listen(s32Socket, 50); 

accept:接受网络连接,客户端连接和三次握手在accept中发生

函数原型:int accept(int sockfd,struct sockaddr *addr,int *addrlen);

头文件:#include <sys/socket.h>

函数的作用:接受网络连接,客户端连接和三次握手在accept中发生

参数:

sockfd:套接字描述符

addr:连接成功,用于填充客户端的地址

addrlen:struct sockaddr的长度

返回值:执行成功,返回新的套接字描述符;执行失败,返回-1

nAddrLen = sizeof(struct sockaddr_in);

s32CSocket = accept(s32Socket, (struct sockaddr*)&addrAccept, &nAddrLen)

补充:(1)接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求(只要我们客户端VLC输入rtsp//……点击播放,就会连上,因为VLC软件有rtsp维护软件代码,与我们server开启的rtsp协议严格遵守规则,所以点击播放内核编写的accept就能成功并获得client的基本信息,然后rec去得到client发送的请求字符串),此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,会返回一个新的s32CSocket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的s32Socket也可以继续使用,继续监听其它客户机的连接请求。(对应过来,客户请求连接我们的服务器,我们服务器先做了一些绑定和监听等等操作之后,如果允许连接,则调用accept函数产生一个新的套接字,然后用这个新的套接字跟我们的客户进行收发数据。也就是说,服务器跟一个客户端连接成功,会有两个套接字。)

(2)为什么要三次握手,假设一种异常情况:A客户端发出一个请求,在某个网络很差的时间点,增多了网络传输节点比如10个,没有发送到B服务器,B没有回复,但是请求没有丢失,然后A开始重发,现在网络好了,可以通过5个网络节点再去连接B,然后B回应了,建立连接,等待A发送数据过来(是需要耗费内存和cpu资源的),接收后才会结束等待,回收资源,A以后不发连接请求,B就不会耗费资源等,这时,A第一次发的请求在,通过10个网络节点过来了,B以为A又想发送数据,并一直等,可是A并不知道,也没有去连接,所以采用三次握手,可以防止上述现象发生

 

①②③④示例流程代码:

void * RtspServerListen(void*pParam)

{

s32Socket = socket(AF_INET, SOCK_STREAM, 0);     //tcp协议用来后面传输命令数据

bind(s32Socket, (struct sockaddr *)&servaddr, sizeof(struct sockaddr_in));

listen(s32Socket, 1);                //后面数字表示监听个数

while ((s32CSocket = accept(s32Socket, (struct sockaddr*)&addrAccept, &nAddrLen)) >= 0)

{

    //填充客户端数据结构体g_rtspClients

                memset(&g_rtspClients,0,sizeof(RTSP_CLIENT));           
                g_rtspClients.index = 0;                                                 //从0开始
                g_rtspClients.socket = s32CSocket; //新的s32CSocket,命令数据通过这个新的标识符来相互传输
                g_rtspClients.status = RTSP_CONNECTED ; //能accept成功说明连接上了,所以将它变成连接态
                g_rtspClients.sessionid = nSessionId++;
                strcpy(g_rtspClients.IP,inet_ntoa(addrAccept.sin_addr));//哪个客户端accept成功后,ip自动填充到addrAccept

               //创建各自客户端线程,去响应请求,有一 个clinet各一个server,server处于被动响应过程,clinet主动发起请求,server就会回应给clinet一个anser

                pthread_create(&threadIdlsn, NULL, RtspClientMsg, &g_rtspClients); //将填充好的g_rtspClients传入线程函数

}

}

 

 

 

 

3 、RtspClientMsg线程函数——server与clinet之间的响应请求

⑤connect:客户端通过connect函数与服务端连接,进行通信

函数原型:int connect (int sockfd,struct sockaddr *serv_addr,int addrlen);

头文件:#include <sys/socket.h>

函数的作用:用来请求连接远程服务器,将参数sockfd 的socket 连至参数serv_addr 指定的服务器IP和端口号上去,有时候是服务器连接客户端,比如pc做client,开发板做server

参数:

sockfd:套接字描述符

serv_addr:为结构体指针变量,存储着远程服务器的IP与端口号信息。

addrlen:struct sockaddr的长度

返回值:执行成功,返回0;执行出错,返回-1.

补充:(1)

⑥send:用新的套接字发送数据给指定的远端主机

函数原型:int send(int sockfd,const void *msg,int len,unsigned int falgs);

头文件:#include <sys/socket.h>

函数的作用:经过socket向对方发送数据

参数:

sockfd:accept产生的新的套接字描述符

msg:要发送的数据存放的地址空间首地址

len:数据的长度

falgs:一般设置为0

返回值:执行成功,返回实际发送数据的字节数;执行失败,返回-1.

使用:

⑦recv:用新的套接字来接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间

函数原型:int recv(int sockfd,void *buf,int len,int falgs);

头文件:#include <sys/socket.h>

函数的作用:通过socket接收数据

参数:

sockfd:accept产生的新的套接字描述符

buf:读取到的数据存放空间的首地址

len:接收数据的最多长度

falgs:设置为0

返回值:执行成功,返回实际接收到的数据的字节数;执行失败,返回-1

使用: recv(pClient->socket, pRecvBuf, RTSP_RECV_SIZE,0);

void * RtspClientMsg(void*pParam)            //传入结构体g_rtspClients的地址
{
           pthread_detach(pthread_self());         //设置线程自己回收自己
           char pRecvBuf[RTSP_RECV_SIZE];  //用来接收client发送给server的请求字符串
           RTSP_CLIENT * pClient = (RTSP_CLIENT*)pParam;
           memset(pRecvBuf,0,sizeof(pRecvBuf));

           while(pClient->status != RTSP_IDLE)    //判断状态,accept后值为RTSP_CONNECTED
           {       

                     //当我们打开VLC播放器,按照选择rtsp格式打开并点击播放按钮,所以的信息都在recv时传给pRecvBuf
                     nRes = recv(pClient->socket, pRecvBuf, RTSP_RECV_SIZE,0);  //见recv介绍函数
                     if(nRes < 1)                    //清理g_rtspClients
                     {
                             g_rtspClients.status = RTSP_IDLE;
                             g_rtspClients.seqnum = 0;
                             g_rtspClients.tsvid = 0;
                             g_rtspClients.tsaud = 0;
                             close(pClient->socket);
                             break;
                     }

                     ParseRequestString(/*将包含多个请求相关信息的一个字符串各自分解出来*/);cmdName、urlPreSuffix、cseq等

                     if(strstr(cmdName, "OPTIONS"))                    //strstr,看OPTIONS字符串是否被cmdName包含
                     {   

                                 //根据client发送Option请求,这里的代码就是server响应后发送一些数据给client
                                 OptionAnswer(cseq,pClient->socket); //函数里面用send发送的
                     }

                    else if(strstr(cmdName, "DESCRIBE"))  

                    {

                                //只要连接好,recv后,里面的东西就是client发送的多个请求

                                //这里面的代码就是我们对各种请求作出的响应,比如发送sdp文件格式,告诉VLC用什么格式去解析

                    }

                    else if(strstr(cmdName, "SETUP"))  

                    {

                               //比如解析后的信息里面有客户端的端口号等,我们在根据端口号等信息将视频发送到确定的进程VLC

                    }

                    else if(strstr(cmdName, "PLAY"))   

                    {    // 将命令解析到PLAY时,前面通过之前建立的socket,TCP协议发送完所需命令,这里再次建立一个套接字udp,为了做到实时流,发送视频数据,专门开启的udp,这样就不会丢包重发包,代码在服务器里,我们去填充client,并连接它,connect是默认阻塞,所以会等待连接成功,成功后status = RTSP_SENDING,变成发送模式

                             int reg = send(s32CSocket, buf,strlen(buf),0);    //之前建立的tcp协议用s32CSocket去发送

                             udpfd = socket(AF_INET,SOCK_DGRAM,0);//新建UDP协议    
                             struct sockaddr_in server;
                             server.sin_family=AF_INET;
                             server.sin_port=htons(g_rtspClients[0].rtpport[0]);          
                             server.sin_addr.s_addr=inet_addr(g_rtspClients[0].IP);
                             connect(udpfd,(struct sockaddr *)&server,sizeof(server));

                             g_rtspClients[pClient->index].status = RTSP_SENDING;

                    }

                   else if(strstr(cmdName, "PAUSE"))   

                   {

                          因为这是一个线程,下次VLC按下停止时,又会有一包数据过来,里面就有PAUSE,而没有PLAY

                   }

}

4、VENC_Sent

HI_S32 VENC_Sent(char *buffer,int buflen)
{
        int heart = g_rtspClients[is].seqnum % 10000;
        
        char* nalu_payload;
        int nAvFrmLen = 0;
        int nIsIFrm = 0;
        int nNaluType = 0;
        char sendbuf[500*1024+32];

    
        nAvFrmLen = buflen;

        struct sockaddr_in server;          //sendto发送到的client的端口号,IP
        server.sin_family=AF_INET;  
        server.sin_port=htons(g_rtspClients[is].rtpport[0]);          
        server.sin_addr.s_addr=inet_addr(g_rtspClients[is].IP);
        int    bytes=0;
        unsigned int timestamp_increse=0;   
        //接收端使用时间戳可准确知道应当在什么时间还原哪一个数据块,从而消除传输中的抖动。时间戳还可用来使视频应用中声音和图像同步。
        timestamp_increse=(unsigned int)(90000.0 / 25);   //相邻两个RTP包之间的时间差

        // RTP_FIXED_HEADER是RTP封装头格式的结构体占12个字节

        rtp_hdr =(RTP_FIXED_HEADER*)&sendbuf[0];    //前12个字节,是添加RTP协议所需要的头封装
         
        rtp_hdr->payload     = RTP_H264;                         //详细看RTP协议发送数据前对视频数据的封装
        rtp_hdr->version     = 2;                       //
        rtp_hdr->marker    = 0;           
        rtp_hdr->ssrc      = htonl(10);   

        if(nAvFrmLen<=nalu_sent_len)        //如果一帧数据是整包发送就是nalu的头占13个字节
        {
            rtp_hdr->marker=1;                      //marker为1表示这包数据为一帧数据的最后一包
            rtp_hdr->seq_no     = htons(g_rtspClients[is].seqnum++);   //应该是用来标记发送所以包的顺序
            nalu_hdr =(NALU_HEADER*)&sendbuf[12];  //第13个字节,是添加NALU单元头占一个字节

            //naul的头从裸流get的帧里面也可以获取,这里是人为直接赋值   
            nalu_hdr->F=0;                                   //                                                                   
            nalu_hdr->NRI=  nIsIFrm; 
            nalu_hdr->TYPE=  nNaluType;           // 类型
            nalu_payload=&sendbuf[13];                                         //之后的就是RBSP数据
            memcpy(nalu_payload,buffer,nAvFrmLen);
                    g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;            
            rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);
            bytes=nAvFrmLen+ 13 ;                
            sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
        }
        else if(nAvFrmLen>nalu_sent_len)  //如果一帧数据分包,就是rtp的头+fu_ind+fu_hdr头,占14个字节
        {       //fu头的值在nalu头类型(5个位表示的意义)解释里面有,赋值28表示为分片
            int k=0,l=0;
            k=nAvFrmLen/nalu_sent_len;
            l=nAvFrmLen%nalu_sent_len;
            int t=0;        

            g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;
            rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);            
            while(t<=k)     
            {
                rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++);
                if(t==0)
                {
                    rtp_hdr->marker=0;
                    fu_ind =(FU_INDICATOR*)&sendbuf[12];
                    fu_ind->F= 0; 
                    fu_ind->NRI= nIsIFrm;  //这个值表示帧的重要性,一般从sps中提取的,我们直接赋值0,表示不重要,随便点
                    fu_ind->TYPE=28;        //fu头的值在nalu头类型(5个位表示的意义)解释里面有,赋值28表示为分片
    
                    fu_hdr =(FU_HEADER*)&sendbuf[13];
                    fu_hdr->E=0;   //  最后一片
                    fu_hdr->R=0;   //保留位,随便
                    fu_hdr->S=1;   //如果是分片情况,s为1表示开始的第一个分片
                    fu_hdr->TYPE=nNaluType;   //本来也要从SPS,PPS等数据位里面提取,并解析,然后赋值
    
                    nalu_payload=&sendbuf[14];
                    memcpy(nalu_payload,buffer,nalu_sent_len);
    
                    bytes=nalu_sent_len+14;              //RTP分包格式就是前14个字节为头,播放器读取前面的参数会识别      
                    sendto( udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
    
                }
                else if(k==t)   //进入分片里面的最后一片
                {
                    rtp_hdr->marker=1;
                    fu_ind =(FU_INDICATOR*)&sendbuf[12]; 
                    fu_ind->F= 0 ;
                    fu_ind->NRI= nIsIFrm ;
                    fu_ind->TYPE=28;

                    fu_hdr =(FU_HEADER*)&sendbuf[13];
                    fu_hdr->R=0;
                    fu_hdr->S=0;   
                    fu_hdr->TYPE= nNaluType;
                    fu_hdr->E=1;
                    nalu_payload=&sendbuf[14];
                    memcpy(nalu_payload,buffer+t*nalu_sent_len,l);
                    bytes=l+14;        
                    sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
                }
                else if(t<k && t!=0)  //中间片的处理
                {

                    rtp_hdr->marker=0;

                    fu_ind =(FU_INDICATOR*)&sendbuf[12]; 
                    fu_ind->F=0; 
                    fu_ind->NRI=nIsIFrm;
                    fu_ind->TYPE=28;
                    fu_hdr =(FU_HEADER*)&sendbuf[13];
                    //fu_hdr->E=0;
                    fu_hdr->R=0;
                    fu_hdr->S=0;  // 中间片所以为0
                    fu_hdr->E=0;  // 中间片所以为0
                    fu_hdr->TYPE=nNaluType;
                    nalu_payload=&sendbuf[14];
                    memcpy(nalu_payload,buffer+t*nalu_sent_len,nalu_sent_len);
                    bytes=nalu_sent_len+14;    
                    sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
                    t++;
                }
            }
        }

    }

    //------------------------------------------------------------
}

 

猜你喜欢

转载自blog.csdn.net/qq_40334837/article/details/81283645