8-5-2_编写简单RTSP协议-处理客户端请求

编写简单RTSP协议-处理客户端请求

在RTSP(实时流协议)中,处理客户端请求步骤如下:

  1. 发送请求:客户端发送RTSP请求命令,常见的请求类型包括:

    • OPTIONS:请求服务器支持的方法,了解服务器可用的方法。
    • DESCRIBE:请求服务器发送媒体描述信息(通常是SDP格式),以了解可用的媒体流及其属性。
    • SETUP:请求建立会话连接,准备接收音视频数据。此请求会指定传输参数,如RTP/RTCP端口、传输协议(TCP或UDP)、单播或组播等。
    • PLAY:请求开始播放媒体流。
    • PAUSE:请求暂停播放。
    • TEARDOWN:请求终止会话并释放资源。
  2. 响应请求:服务器处理客户端的请求并返回响应。例如,对于SETUP请求,服务器会返回确认的传输参数和会话ID。

  3. 传输数据:一旦会话建立,媒体数据通过RTP(实时传输协议)进行传输,RTCP(实时传输控制协议)则用于传输控制信息和质量反馈。

1.接受客户端数据

使用 recv 函数从套接字 clientSockfd 接收数据,并将其存储在缓冲区 rBuf

recvLen = recv(clientSockfd, rBuf, BUF_MAX_SIZE, 0);
if(recvLen <= 0)
    goto out;

从客户端接收请求消息。

2.解析请求命令

从缓冲区获取数据,并从数据中提前方法url版本等信息。

        bufPtr = getLineFromBuf(rBuf, line);
        if(sscanf(line, "%s %s %s\r\n", method, url, version) != 3)
        {
            printf("parse err\n");
            goto out;
        }

getLineFromBuf函数的作用从缓冲区 buf 中读取一行数据,并将其存储到 line 中。

static char* getLineFromBuf(char* buf, char* line)
{
    while(*buf != '\n')//循环读取字符
    {
        *line = *buf; //复制字符
        line++;
        buf++;
    }

    *line = '\n';//添加换行符
    ++line;
    *line = '\0';//添加终止符

    ++buf; //更新缓冲区指针
    return buf; 
}

3.解析序列号

解析RTSP请求中的序列号(CSeq)。在RTSP协议中,CSeq用于标识请求的顺序,确保请求和响应能够正确匹配。

        /* 解析序列号 */
        bufPtr = getLineFromBuf(bufPtr, line);
        if(sscanf(line, "CSeq: %d\r\n", &cseq) != 1)
        {
            printf("parse err\n");
            goto out;
        }

4.解析客户端端口

解析 Transport 头部信息,以获取客户端的RTP和RTCP端口

        if(!strcmp(method, "SETUP"))//检查请求方法
        {
            while(1)//循环读取行
            {
                bufPtr = getLineFromBuf(bufPtr, line);//逐行读取数据
                if(!strncmp(line, "Transport:", strlen("Transport:"))) //检查当前行是否以 Transport: 开头。
                {
                    sscanf(line, "Transport: RTP/AVP;unicast;client_port=%d-%d\r\n",
                                    &clientRtpPort, &clientRtcpPort);//提取客户端的RTP和RTCP端口
                    break;
                }
            }
        }

Transport 头部包含传输参数,如传输协议、传输模式和端口信息。

5.根据不同方法响应客户端

5.1 OPTIONS方法响应

处理 OPTIONS 请求,OPTIONS 请求用于查询服务器支持的功能和方法

        if(!strcmp(method, "OPTIONS")) //检查RTSP请求的方法
        {
            if(handleCmd_OPTIONS(sBuf, cseq))//处理请求
            {
                printf("failed to handle options\n");
                goto out;
            }
        }

调用 handleCmd_OPTIONS 函数来处理 OPTIONS 请求。

static int handleCmd_OPTIONS(char* result, int cseq)/* result:存储生成的响应消息;cseq: 请求的序列号*/
{
	/*使用 sprintf 函数生成响应消息,并将其存储在 result 中*/
    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;
}

服务器响应 OPTIONS 请求,并响应客户端服务器支持的RTSP方法。

5.2 DESCRIBE方法响应

处理 DESCRIBE 请求,DESCRIBE 请求用于获取媒体资源的描述信息,通常是SDP(会话描述协议)格式。

else if(!strcmp(method, "DESCRIBE"))
        {
            if(handleCmd_DESCRIBE(sBuf, cseq, url))
            {
                printf("failed to handle describe\n");
                goto out;
            }
        }

调用handleCmd_DESCRIBE函数处理 DESCRIBE 请求。

static int handleCmd_DESCRIBE(char* result, int cseq, char* url)/* result:存储生成的响应消息;cseq: 请求的序列号;url:请求的媒体资源URL*/
{
    char sdp[500];//存储生成的SDP描述信息
    char localIp[100];//IP地址

    sscanf(url, "rtsp://%[^:]:", localIp);//从URL中提取本地IP地址

	/*生成SDP描述信息*/
    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: %d\r\n\r\n"
                    "%s",
                    cseq,
                    url,
                    strlen(sdp),
                    sdp);
    
    return 0;
}

SDP参数如下:

  • v=0版本号:SDP协议版本,目前固定为0。
  • o=- 9%ld 1 IN IP4 %s:会话所有者/创建者和会话标识符
    • -:用户名,表示不使用用户名。
    • 9%ld:会话ID,使用当前时间戳生成。
    • 1:会话版本号,通常为1。
    • IN:网络类型,表示Internet。
    • IP4:地址类型,表示IPv4地址。
    • %s:本地IP地址。
  • t=0 0时间描述:会话的起始和结束时间,0 0表示会话永不过期。
  • a=control:\*属性:会话级别的控制属性,*表示控制整个会话。
  • m=video 0 RTP/AVP 96:媒体描述
    • video:媒体类型,表示视频。
    • 0:端口号,0表示端口未指定。
    • RTP/AVP:传输协议,表示RTP协议下的音视频配置。
    • 96:媒体格式,动态负载类型96,通常用于H.264视频编码。
  • a=rtpmap:96 H264/90000属性:RTP映射,指定负载类型96对应H.264编码,时钟频率为90000Hz。
  • a=control:track0属性:媒体级别的控制属性,track0表示第一个媒体流。

响应消息的格式如下:

  • RTSP/1.0 200 OK\r\n:表示请求成功,返回状态码200。
  • CSeq: %d\r\n:包含请求的序列号(CSeq),用于匹配请求和响应。
  • Content-Base: %s\r\n:包含请求的URL。
  • Content-type: application/sdp\r\n:指定内容类型为SDP。
  • Content-length: %d\r\n\r\n:指定内容长度。
  • %s:包含生成的SDP描述信息。

5.3 SETUP方法响应

处理 SETUP 请求,SETUP 请求用于初始化一个媒体流会话,设置传输参数。

        else if(!strcmp(method, "SETUP"))//检查RTSP请求的方法
        {
            if(handleCmd_SETUP(sBuf, cseq, clientRtpPort))
            {
                printf("failed to handle setup\n");
                goto out;
            }
        }

调用 handleCmd_SETUP 函数来处理 SETUP 请求。

static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort)
{
	/生成一个RTSP响应消息
    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;
}
  • 状态行RTSP/1.0 200 OK,表示请求成功。

  • CSeq:序列号,用于匹配请求和响应。

  • Transport:传输参数,指定了RTP(实时传输协议)和RTCP(实时传输控制协议)的传输方式、客户端和服务器的端口号。

  • Session:会话ID,用于标识RTSP会话。

  • 设置传输端口:函数从参数中获取客户端的RTP端口(clientRtpPort),并设置服务器的RTP和RTCP端口(SERVER_RTP_PORTSERVER_RTCP_PORT)。

5.4 PLAY方法响应

处理 PLAY 请求,PLAY命令用于开始或恢复媒体流的播放。

        else if(!strcmp(method, "PLAY"))//检查命令类型
        {
            if(handleCmd_PLAY(sBuf, cseq))//处理PLAY请求
            {
                printf("failed to handle play\n");
                goto out;
            }
        }

调用handleCmd_PLAY函数处理PLAY请求。

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=60\r\n\r\n",
                    cseq);
    
    return 0;
}
  • 状态行RTSP/1.0 200 OK,表示请求成功。
  • CSeq:序列号,用于匹配请求和响应。
  • Range:指定播放范围,这里表示从头开始播放。
  • Session:会话ID和超时时间,用于标识RTSP会话并设置会话的超时时间。

6.发送RTP数据包

6.1 初始化RTP数据包头信息

RTP协议与RTSP(实时流协议)一起使用,用于传输视频数据。

            rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264, 0,
                            0, 0, 0x88923423);

调用rtpHeaderInit函数初始化一个RtpPacket结构体的RTP头部字段。函数参数包括CSRC长度、扩展标志、填充标志、版本号、负载类型、标记位、序列号、时间戳和同步源标识符(SSRC)。

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)
{
    rtpPacket->rtpHeader.csrcLen = csrcLen;//CSRC(贡献源)标识符的个数
    rtpPacket->rtpHeader.extension = extension;//扩展标志,指示是否有扩展头部。
    rtpPacket->rtpHeader.padding = padding;//充标志,指示是否有填充字节。
    rtpPacket->rtpHeader.version = version;//RTP协议版本号,通常为2。
    rtpPacket->rtpHeader.payloadType =  payloadType;//负载类型,指示传输的数据类型(例如音频或视频)。
    rtpPacket->rtpHeader.marker = marker;//标记位,用于标记重要事件(例如帧的结束)。
    rtpPacket->rtpHeader.seq = seq;//用于标识RTP数据包的顺序。
    rtpPacket->rtpHeader.timestamp = timestamp;//时间戳,用于同步音视频数据。
    rtpPacket->rtpHeader.ssrc = ssrc;//同步源标识符,用于标识数据流的来源。
}
  • SRC长度、扩展标志和填充标志均为0。
  • 版本号为2。
  • 负载类型为96(通常表示动态负载类型,没有预先定义的固定编码格式)。
  • 标记位为0。
  • 序列号和时间戳均为0。
  • SSRC为0x88923423,唯一标识发送端的媒体流。

6.2 从文件中读取一帧图像

读取fd文件句柄,读取的大小为500000字节,到缓冲区frame中。

        frameSize = getFrameFromH264File(fd, frame, 500000);
        if(frameSize < 0)
        {
            break;
        }

调用getFrameFromH264File函数从一个H.264文件中读取一帧数据

static int getFrameFromH264File(int fd, char* frame, int size)//从一个 H.264 文件中读取一帧数据
{
    int rSize, frameSize;
    char* nextStartCode;

    if(fd < 0)
        return fd;

    rSize = read(fd, frame, size);
    if(!startCode3(frame) && !startCode4(frame))//读取的数据必须以H.264的起始码(start code)开头。
        return -1;
    
    nextStartCode = findNextStartCode(frame+3, rSize-3);//查找下一帧的起始码位置
    if(!nextStartCode)
    {
        //lseek(fd, 0, SEEK_SET);
        //frameSize = rSize;
        return -1;
    }
    else
    {
        frameSize = (nextStartCode-frame);//计算当前帧的大小
        lseek(fd, frameSize-rSize, SEEK_CUR);//调整文件指针以准备读取下一帧
    }

    return frameSize;//返回当前帧的大小
}

6.3 检查起始码的长度

检查frame缓冲区中的数据是否以3字节/4字节的起始码。

if(startCode3(frame))
    startCode = 3;
else
    startCode = 4;

检查给定缓冲区中的数据是否以3字节的H.264起始码(0x000001)开头。

static inline int startCode3(char* buf)
{
    if(buf[0] == 0 && buf[1] == 0 && buf[2] == 1)//检查缓冲区buf的前三个字节是否为0x00 0x00 0x01
        return 1;
    else
        return 0;
}

6.4 调整帧的大小

减去起始码的长度(3或4字节),调整帧的实际大小。

frameSize -= startCode;

6.5 发送视频帧

将H.264视频帧通过RTP(实时传输协议)发送到客户端。

                rtpSendH264Frame(serverRtpSockfd, clientIP, clientRtpPort,
                                    rtpPacket, frame+startCode, frameSize);
  • serverRtpSockfd:服务器的RTP套接字文件描述符。
  • clientIP:客户端的IP地址。
  • clientRtpPort:客户端的RTP端口。
  • rtpPacket:包含RTP头部信息的数据包。
  • frame+startCode:指向视频帧数据的指针,跳过起始码。
  • frameSize:调整后的帧大小。

6.5.1 单一NAL单元模式

RTP(实时传输协议)数据包的封装和发送。

    if (frameSize <= RTP_MAX_PKT_SIZE) // nalu长度小于最大包场:单一NALU单元模式
    {
        /*
         *   0 1 2 3 4 5 6 7 8 9
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *  |F|NRI|  Type   | a single NAL unit ... |
         *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         */
        memcpy(rtpPacket->payload, frame, frameSize);//将视频帧数据复制到RTP数据包的负载部分
        ret = rtpSendPacket(socket, ip, port, rtpPacket, frameSize);//通过RTP套接字发送数据包
        if(ret < 0)
            return -1;

        rtpPacket->rtpHeader.seq++;//增加RTP头部的序列号
        sendBytes += ret;//累加发送的字节数
        if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加时间戳
            goto out;
    }

H.264视频帧封装到RTP数据包中,并通过网络发送到客户端。在单一NAL单元的情况,发送SPS和PPS帧时不增加时间戳。

6.5.2 分片模式

处理和发送大于RTP(实时传输协议)数据包最大负载大小的H.264视频帧。

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

        /* 发送完整的包 */
        for (i = 0; i < pktNum; i++)
        {
            rtpPacket->payload[0] = (naluType & 0x60) | 28; //设置FU Indicator字节,表示这是分片单元类型
            rtpPacket->payload[1] = naluType & 0x1F; //  设置FU Header字节,表示NALU类型
            
            if (i == 0) //第一包数据
                rtpPacket->payload[1] |= 0x80; // start 第8位设置1,表示分片单元的开始
            else if (remainPktSize == 0 && i == pktNum - 1) //最后一包数据,并且没有剩余数据
                rtpPacket->payload[1] |= 0x40; // end 第7位设置为1,表示这是分片单元的结束。

			//跳过FU Indicator 和 FU Header的前两个字节,复制视频帧数据到RTP负载。
            memcpy(rtpPacket->payload+2, frame+pos, RTP_MAX_PKT_SIZE);
            ret = rtpSendPacket(socket, ip, port, rtpPacket, RTP_MAX_PKT_SIZE+2);//发送RTP数据包
            if(ret < 0)
                return -1;

            rtpPacket->rtpHeader.seq++;//更新序列号
            sendBytes += ret; //统计数据量
            pos += RTP_MAX_PKT_SIZE; //计算下一个位置
        }

        /* 发送剩余的数据 */
        if (remainPktSize > 0)
        {
            rtpPacket->payload[0] = (naluType & 0x60) | 28; //设置 FU Indicator 字节
            rtpPacket->payload[1] = naluType & 0x1F; //设置 FU Header 字节
            rtpPacket->payload[1] |= 0x40; //设置结束位

            memcpy(rtpPacket->payload+2, frame+pos, remainPktSize);//复制剩余数据到RTP负载
            ret = rtpSendPacket(socket, ip, port, rtpPacket, remainPktSize);//发送RTP数据包
            if(ret < 0)
                return -1;

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

将大于RTP最大负载大小的H.264视频帧分片,并通过RTP协议发送到客户端。

6.5.3 通过UDP套接字发送RTP包

通过UDP套接字发送RTP数据包,并在发送前后正确处理字节序转换,以便在网络传输中保持数据的一致性。

int rtpSendPacket(int socket, const char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize)
{
    struct sockaddr_in addr; //存储目标地址信息
    int ret;

    addr.sin_family = AF_INET; //地址族,设置为 AF_INET 表示 IPv4
    addr.sin_port = htons(port); //目标端口
    addr.sin_addr.s_addr = inet_addr(ip);//目标 IP 地址

    //将 16 位和 32 位的主机字节序转换为网络字节序
    rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);//将RTP头部的序列号从主机字节序转换为网络字节序。
    rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);//将RTP头部的时间戳从主机字节序转换为网络字节序。
    rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);// 将RTP头部的同步源标识符(SSRC)从主机字节序转换为网络字节序。

    ret = sendto(socket, (void*)rtpPacket, dataSize+RTP_HEADER_SIZE, 0,
                    (struct sockaddr*)&addr, sizeof(addr));//使用sendto函数通过UDP套接字发送数据包

    //将 16 位和 32 位的网络字节序转换为主机字节序
    rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);//将RTP头部的序列号从网络字节序转换回主机字节序。
    rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);//将RTP头部的时间戳从网络字节序转换回主机字节序。
    rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);//将RTP头部的同步源标识符(SSRC)从网络字节序转换回主机字节序。

    return ret;
}

6.6 更新时间戳

更新RTP头部中的时间戳,以反映视频帧的播放时间。

rtpPacket->rtpHeader.timestamp += 90000/25;

假设视频帧率为25帧每秒,因此每帧的时间戳增量为90000/25(RTP时间戳单位为90kHz)。