前段时间写完了RTMP的直播方案,因为是基于librtmp的库来实现的,所以比较简单。之后花了一个月吧,参照海思的rtsp推流框架,慢慢的写了一个基于RealTek为底层的网络摄像头Rtsp直播功能的demo。这个不带任何库,纯C写的推流功能,学到了蛮多东西的,都写下来以后忘了还能回来看看,同时也希望给刚刚起步做rtsp直播的小伙伴一点参考。
一时间也不知道从什么地方讲起,我还是顺着我的代码一步一步讲吧。首先要确定一个事情就是,在网络摄像头RTSP直播的方案中,摄像头是作为服务器端的,连接摄像头请求码流数据的都是客户端。
int main(int argc, char* argv[])
{
int s32MainFd;
pthread_t framesource_id;
struct timespec ts = { 0, 200000000 };
ringmalloc(720*576);//64个大小为720*576的环形缓冲区
printf("RTSP server START\n");
PrefsInit();//设置服务器信息全局变量 端口等
printf("listen for client connecting...\n");
signal(SIGINT, IntHandl);//捕捉信号用来终止程序
s32MainFd = tcp_listen(SERVER_RTSP_PORT_DEFAULT);//554
if (ScheduleInit() == ERR_FATAL)//推送视频数据流线程入口
{
fprintf(stderr,"Fatal: Can't start scheduler %s, %i \nServer is aborting.\n", __FILE__, __LINE__);
return 0;
}
RTP_port_pool_init(RTP_DEFAULT_PORT);//初始化端口集合,用以给多个RTP,RTCP分配端口
pthread_create(&framesource_id,NULL,FRAME_SOURCE_THREAD,NULL);//底层数据存取入口
while (!g_s32Quit)
{
nanosleep(&ts, NULL);
EventLoop(s32MainFd);//RTSP连接处理函数入口
}
sleep(2);
Camera_free();
ringfree();//缓冲区释放
printf("The Server quit!\n");
return 0;
}
这个是我的主函数,无关紧要的东西不说了,说一些重要的部分,第一个引出要说的就是Socket。
socket据我的了解,其实就是一个接口,它屏蔽了复杂的TCP/IP协议族,合理使用这些接口就可以完成基于TCP/UDP的数据传输。
上图是TCP通讯的步骤,编程按照这个模型来就行了。下面放一下各个函数的简单介绍。
1. socket()函数创建套接字
int socket(int af, int type, int protocol);
2.bind()和connect()函数
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;而客户端要用 connect() 函数建立连接。
bind() 函数的原型为:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。下面的代码与本机IP,指定端口进行绑定
/*创建套接字*/
if((f = socket(AF_INET, SOCK_STREAM, 0))<0)//TCP
{
fprintf(stderr, "socket() error in tcp_listen.\n");
return -1;
}
/*设置socket的可选参数*/
setsockopt(f, SOL_SOCKET, SO_REUSEADDR, (char *) &v, sizeof(int));
s.sin_family = AF_INET;//使用IPv4地址
s.sin_addr.s_addr = BigLittleSwap32(INADDR_ANY);//具体的IP地址(本机所有IP)
s.sin_port = BigLittleSwap16(port);//端口
/*绑定socket*/
if(bind(f, (struct sockaddr *)&s, sizeof(s)))
{
fprintf(stderr, "bind() error in tcp_listen");
return -1;
}
connect() 函数
connect() 函数用来建立连接,它的原型为:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
3.listen()和accept()函数
对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。
listen() 函数
通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:
int listen(int sock, int backlog);
sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。
如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。
当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。
accept() 函数
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
4. Socket 服务端与客户端数据交互方式
Socket 的服务端和客户端的数据交互是通过读写缓冲区来完成的。
如图可以看到通过write()send()函数写缓冲区,read()recv()函数读缓冲区。而数据何时发送是不受程序员控制的,这取决于当时的网络情况、当前线程是否空闲等诸多因素。我们只要把要写的数据写入缓冲区,从缓冲区里读取我们想要的数据就行了。
注意这里有两种情况,一种是阻塞模式(默认)
在向缓冲区写数据时,如果缓冲区剩余空间长度小于你要写入的数据长度,那么写入操作就会被阻塞(暂停执行),直到缓冲区数据发送出去了有足够的空间后会唤醒写入操作。
TCP在进行网络传输数据时,输入缓冲区也会被锁定无法写入数据,直到发送完成解锁缓冲区。
在接收方,会检测缓冲区,如果有数据,就会取数据,如果没有,程序会被阻塞,直到有数据之后才会往下跑。
如果要取的数据长度小于缓冲区中有效数据的长度,也会被阻塞,直到缓冲区中数据积累达到要取的数据长度后才会读取返回。
非阻塞模式
非阻塞模式要配合select()函数一起使用。
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
参数maxfd是需要监视的最大的文件描述符值+1;rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。struct timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
对于fd_set类型通过下面四个宏来操作:
FD_ZERO(fd_set *fdset) 将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
FD_SET(fd_set *fdset) 用于在文件描述符集合中增加一个新的文件描述符。
FD_CLR(fd_set *fdset) 用于在文件描述符集合中删除一个文件描述符。
FD_ISSET(int fd,fd_set *fdset) 用于测试指定的文件描述符是否在该集合中。
理解select的用法主要是要理解fd_set,这是一个套接字集合(socket创建的那个套接字),为了说明方便,假设取fd_set长度为一字节就是8位,这个8位的套接字集合fd_set每一位都可以表示一个套接字,这个套接字集合就可以对应8个套接字了。
一般的实现过程是:
fd_set rset;
struct timeval t;
FD_ZERO(&rset);
t.tv_sec=0; /*select 时间间隔*/
t.tv_usec=100000;
FD_SET(rtsp->fd,&rset);
/*调用select等待对应描述符变化*/
if (select(g_s32Maxfd+1,&rset,0,0,&t)<0)
{
fprintf(stderr,"select error %s %d\n", __FILE__, __LINE__);
send_reply(500, NULL, rtsp);
return ERR_GENERIC; //errore interno al server
}
/*有可供读进的rtsp包*/
if (FD_ISSET(rtsp->fd,&rset))
{
recv(s,...);
}
OK,Socket这部分讲完,先告一段落,下一部分讲下RTSP的交互过程,因为RTSP是TCP/IP协议体系中的一个应用层协议,所以我先讲讲socket通讯,打个基础。