一、项目创立初衷:
由于之前学过计算机网络的相关知识,了解了计算机网络的基本工作原理,对于主流的协议有一定的了解。但对于应用层的协议还知之甚少,因此我去了解了下目前主要的应用层传输协议,发现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