编写简单RTSP协议-处理客户端请求
在RTSP(实时流协议)中,处理客户端请求步骤如下:
- 
发送请求:客户端发送RTSP请求命令,常见的请求类型包括: - OPTIONS:请求服务器支持的方法,了解服务器可用的方法。
- DESCRIBE:请求服务器发送媒体描述信息(通常是SDP格式),以了解可用的媒体流及其属性。
- SETUP:请求建立会话连接,准备接收音视频数据。此请求会指定传输参数,如RTP/RTCP端口、传输协议(TCP或UDP)、单播或组播等。
- PLAY:请求开始播放媒体流。
- PAUSE:请求暂停播放。
- TEARDOWN:请求终止会话并释放资源。
 
- 
响应请求:服务器处理客户端的请求并返回响应。例如,对于SETUP请求,服务器会返回确认的传输参数和会话ID。 
- 
传输数据:一旦会话建立,媒体数据通过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_PORT和SERVER_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)。