项目--基于RTSP协议的简易服务器开发(2)

一、项目创立初衷:

由于之前学过计算机网络的相关知识,了解了计算机网络的基本工作原理,对于主流的协议有一定的了解。但对于应用层的协议还知之甚少,因此我去了解了下目前主要的应用层传输协议,发现RTSP(实时传输协议)在实时传输方面有很大的贡献,于是我开始了对于该协议的学习、研究,并创建了该项目。
 

二、总体设计:

 服务器处理流程简述:

        创建tcp连接套接字;绑定地址;监听;

        再建立通信套接字,接收客户端连接-->处理客户端请求-->处理完毕,释放资源、关闭套接字。

三、细部设计

1.rtp数据包的设计及相关函数:

要想发送rtp数据包,首先要定义rtp包结构体(在rtp.h文件中设置):

rtp首部字段:

struct RtpHeader
{
    //rtp协议头部
    /* byte 0 */
    uint8_t csrcLen : 4;//CSRC计数器,占4位,指示CSRC 标识符的个数。
    uint8_t extension : 1;//占1位,如果X=1,则在RTP报头后跟有一个扩展报头。
    uint8_t padding : 1;//填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
    uint8_t version : 2;//RTP协议的版本号,占2位,当前协议版本号为2。

    /* byte 1 */
    uint8_t payloadType : 7;//有效载荷类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等。
    uint8_t marker : 1;//标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。

    /* bytes 2,3 */
    uint16_t seq;//占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。接收者通过序列号来检测报文丢失情况,重新排序报文,恢复数据。

    /* bytes 4-7 */
    uint32_t timestamp;//占32位,时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。

    /* bytes 8-11 */
    uint32_t ssrc;//占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。

   /*标准的RTP Header 还可能存在 0-15个特约信源(CSRC)标识符

   每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源

   */
};

rtp包结构体:

struct RtpPacket
{   
    //头部字段
    struct RtpHeader rtpheader;
    //信息载体
    uint8_t payload [0];
};

定义初始化rtp包的函数、通过udp发送rtp数据包的函数(rtp.cpp文件中):

//初始化数据包

void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
    uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
    uint16_t seq, uint32_t timestamp, uint32_t ssrc)

//通过udp发送

int rtpSendPacketOverUdp(int serverRtpSockfd, const char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize)

2.处理客户端流程:

1.根据创建的通信套接字,接收客户端发送的数据(rtsp数据包的负载),存储到接收缓冲区

2.通过接收的数据,判断客户端的请求类型:

由于连接时,双方发送的rtsp信息以 \r\n结尾:

eg:

所以可以用strtok函数,将接收的数据分行存储,并逐次判断:

const char* sep = "\n";
        //1.分解接受缓冲区中数据,根据不同类型做出相应
char* line = strtok(rBuf, sep);     //以sep分割字符串rBuf,并存储在line中

再以用strstr函数,判断是哪种请求(RTSP的请求:OPTIONS、DESCRIBE、SETUP、PLAY、TERADOWN),若不是请求字段,则根据其数据格式,考察其他字段。

strstr(line,"OPTIONOS");

eg:

根据接收的消息,找到关于客户端的相关信息,为数据包的发送做准备。 

   //考察CSeq字段
            else if (strstr(line, "CSeq")) {
                if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1) {     //将服务器收到的CSeq赋值给C传入的CSeq参数
                    // error
                }
            }

            //考察Transport字段,
            else if (!strncmp(line, "Transport:", strlen("Transport:"))) {
                // 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) {    //从请求消息中获取客户端 ip、port
                    // error
                    printf("parse Transport error \n");
                }
            }
            line = strtok(NULL, sep);   //将line(客户端发送的数据)进行到下一行
        }

3.根据其请求的类型,做出相应的处理:

static int handleCmd_OPTIONS(char* result, int cseq)
{
    sprintf(result, "RTSP/1.0 200 OK\r\n"
        "CSeq: %d\r\n"
        "Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n"
        "\r\n",
        cseq);

    return 0;
}

static int handleCmd_DESCRIBE(char* result, int cseq, char* url)
{
    char sdp[500];
    char localIp[100];

    sscanf(url,"rtsp://%[^:]:", localIp);

    sprintf(sdp, "v=0\r\n"
        "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"
        "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;
}

//传入序号、Rtp端口参数,作为SETUP请求的回应
static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort)
{
    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,
        SERVER_RTP_PORT,
        SERVER_RTCP_PORT);

    return 0;
}

static int handleCmd_PLAY(char* result, int cseq)
{
    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 int handleTEARDOWN(char *result,int cseq)
{                               
    sprintf(result,"RTSP/1.0 200 OK\r\n"
            "CSeq: %d \r\n",cseq);
    return 0;
}

若为setup请求,需要开始建立连接,创建rtp、rtcp udp套接字,绑定地址,最后在传输阶段(PLAY),通过这两个套接字,以及接收到的客户端的端口,发送数据

4.在发送数据前,要先从本地的h264文件读取数据:

由于h264文件是由一个个NALU构成的,且每个NALU之间有固定的间隔符(00 00 00 01或 00 00 01),

static int getFrameFromH264File(FILE* fp, char* frame, int size) {
    int rSize, frameSize;
    char* nextStartCode;

    if (fp < 0)
        return -1;

    rSize = fread(frame, 1, size, fp);//每次读取的长度

    //h264由一个个NALU构成,每个NALU以 0001 0010隔开
    //根据分隔符0001 0010每次读取一个NALU
    if (!startCode3(frame) && !startCode4(frame))
        return -1;

    //找到第一个开始的编码
    nextStartCode = findNextStartCode(frame + 3, rSize - 3);    //减去分隔符,数据指针后移
    if (!nextStartCode)
    {
        //lseek(fd, 0, SEEK_SET);
        //frameSize = rSize;
        return -1;
    }
    else
    {
        //寻找成功
        frameSize = (nextStartCode - frame);
        //退回到
        fseek(fp, frameSize - rSize, SEEK_CUR);

    }

    return frameSize;
}

5.通过UDP协议发送数据

1.通过framesize大小判断该包应该是何种打包模式

 单一NALU单元打包模式:(framesize<rtp的最大限制)

 memcpy(rtpPacket->payload, frame, frameSize);
        ret = rtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, frameSize);
        if (ret < 0)
            return -1;

        rtpPacket->rtpHeader.seq++;
        sendBytes += ret;   
        if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS(只是编解码需要的数据,并非视频数据)就不需要加时间戳
            goto out;

分包模式:(framesize<rtp的最大限制)

nt remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小
        int i, pos = 1;

        // 发送完整的包
        for (i = 0; i < pktNum; i++)
        {
            //根据NALU包格式解析
            rtpPacket->payload[0] = (naluType & 0x60) | 28;
            rtpPacket->payload[1] = naluType & 0x1F;

            if (i == 0) //第一包数据
                rtpPacket->payload[1] |= 0x80; // start
            else if (remainPktSize == 0 && i == pktNum - 1) //最后一包数据
                rtpPacket->payload[1] |= 0x40; // end

            //从负载的第三个数据包开始拷贝
            memcpy(rtpPacket->payload + 2, frame + pos, RTP_MAX_PKT_SIZE);
            ret = rtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, RTP_MAX_PKT_SIZE + 2);
            if (ret < 0)
                return -1;

            rtpPacket->rtpHeader.seq++;
            sendBytes += ret;
            pos += RTP_MAX_PKT_SIZE;
        }

        // 发送剩余的数据
        if (remainPktSize > 0)
        {
            rtpPacket->payload[0] = (naluType & 0x60) | 28;
            rtpPacket->payload[1] = naluType & 0x1F;
            rtpPacket->payload[1] |= 0x40; //end

            memcpy(rtpPacket->payload + 2, frame + pos, remainPktSize + 2);
            ret = rtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, remainPktSize + 2);
            if (ret < 0)
                return -1;
                                            
            rtpPacket->rtpHeader.seq++;     //发送完毕之后进行序号的累加
            sendBytes += ret;
        }
    }

TCP连接后,开始RTSP交互过程 :

 

四、效果实现

meeting_01

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值