MJPG-streamer可以创建多个输出,简单的说,根据主函数中输入的参数解析的结果,确定输出通道的个数,至少保证有一个输出通道在程序运行时存在。从参数解析结果确定每个输出通道的参数,并以这些参数为每个通道创建发送线程。在每个发送线程上,不断侦听是否有连接请求。每当有连接请求,在未达到最高连接数目时,为每个连接请求创建连接线程。在连接线程中,根据参数,确实发送方式是stream?snapshot?或者是其他方式。在连接请求不关闭时,连接线程一直存在,连接请求退出时,线程随之退出,并有系统自动释放其所分配的资源。当程序运行终止信号stop不为1时,输出通道线程不断侦听连接和发送数据,当终止信号被置1时,退出输出线程,同时释放其所分配的资源。
输出通道的初始化部分仅仅是将输出通道的参数分别解析和分配到各自通道指定的变量中,为后续每个通道的run函数提供参数。其余的,都是run函数所执行部分,包括其执行的子函数和操作等。
需要说明的是,由于有多个输出通道,故而需要对每个通道都进行初始化和执行输出,这个过程通过一个循环体完成,循环体的控制变量就是输出通道的个数。
本文根据上述思路,把关键部分源码进行解释,具体的还得参考源码,如有错误,欢迎指出以便改正。
===============================================初始化部分====================================================
1、定义和初始化部分参数,这些参数用于在对输出通道参数解析过程中的临时变量;
char *argv[MAX_ARGUMENTS]={NULL}; int argc=1, i; int port; char *credentials, *www_folder; char nocommands; port = htons(8080); credentials = NULL; www_folder = NULL; nocommands = 0; argv[0] = OUTPUT_PLUGIN_NAME;
2、【解析参数】
将传入的单串参数param->parameter_string转换为字符串数组,存放在argv数组中,调用c = getopt_long_only(argc, argv, "", long_options, &option_index),当c==-1时,说明解析完毕,退出解析过程,否则根据option_index参数设置port,credentials,www_folder,nocommands等参数
这部分的代码与前面主程序和输入通道程序的解析过程类似,不再详述,都是把对应参数填写到指定的变量上。
3、根据param->id配置每个服务器线程参数(servers全局)
程序为每个输出通道都定义了一个对应的参数,用于配置和存储每个输出通道的信息。其结构体如下所示:
/* context of each server thread */ typedef struct { int sd; int id; globals *pglobal; pthread_t threadID; config conf; } context;
经过上述的参数解析过程之后,用户输入的参数都被识别,之后再被存放到每个输出通道服务器线程参数变量上。即servers[param->id]。
servers[param->id].id = param->id; servers[param->id].pglobal = param->global; servers[param->id].conf.port = port; servers[param->id].conf.credentials = credentials; servers[param->id].conf.www_folder = www_folder; servers[param->id].conf.nocommands = nocommands;
至此,输出通道的初始化完毕,等待run函数的执行。
===============================================输出通道执行部分====================================================
在每个输出通道上,先为每个通道创建服务线程,调用线程处理函数进行连接请求的侦听和处理
int output_run(int id) { DBG("launching server thread #%02d\n", id); //server_thread位于httpd.c头文件中 pthread_create(&(servers[id].threadID), NULL, server_thread, &(servers[id])); //将状态改为unjoinable状态,确保资源的释放,无须调用join函数释放资源 pthread_detach(servers[id].threadID); return 0; }
线程处理函数创建打开一个socket并等待连接请求,在未达到最大连接请求时,为每个请求创建相对应的客户端服务线程client_thread(),并使用通道线程相同的处理方式,将该子线程detach处理。子线程根据不同的请求方式,进行数据的分发。
void *server_thread( void *arg ) { //定义线程参数:服务端和客户端地址变量,客户端服务子线程 struct sockaddr_in addr, client_addr; int on; pthread_t client; socklen_t addr_len = sizeof(struct sockaddr_in); //每个服务器线程的参数 context *pcontext = arg; pglobal = pcontext->pglobal; /* set cleanup handler to cleanup ressources */ pthread_cleanup_push(server_cleanup, pcontext); /* 打开通道服务端线程 */ pcontext->sd = socket(PF_INET, SOCK_STREAM, 0); if ( pcontext->sd < 0 ) { fprintf(stderr, "socket failed\n"); exit(EXIT_FAILURE); } //默认情况下,server重启,调用socket,bind,然后listen,会失败.因为该端口正在被使用. //一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。 on = 1; if (setsockopt(pcontext->sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) { perror("setsockopt(SO_REUSEADDR) failed"); exit(EXIT_FAILURE); } /* 配置服务器参数,用于监听 */ memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = pcontext->conf.port; /* is already in right byteorder */ //宏INADDR_ANY代替本机的IP,无需手动选择,自动替换,当存在多个网卡情况下,自动选择 addr.sin_addr.s_addr = htonl(INADDR_ANY); if ( bind(pcontext->sd, (struct sockaddr*)&addr, sizeof(addr)) != 0 ) { perror("bind"); OPRINT("%s(): bind(%d) failed", __FUNCTION__, htons(pcontext->conf.port)); closelog(); exit(EXIT_FAILURE); } //socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。 if ( listen(pcontext->sd, 10) != 0 ) { fprintf(stderr, "listen failed\n"); exit(EXIT_FAILURE); } //等待客户端连接,有连接则创建新的线程进行处理 while ( !pglobal->stop ) { //cfd结构体定义在httpd.d cfd *pcfd = malloc(sizeof(cfd)); if (pcfd == NULL) { fprintf(stderr, "failed to allocate (a very small amount of) memory\n"); exit(EXIT_FAILURE); } DBG("waiting for clients to connect\n"); pcfd->fd = accept(pcontext->sd, (struct sockaddr *)&client_addr, &addr_len); pcfd->pc = pcontext; /* 创建客户端子线程处理连接请求 */ DBG("create thread to handle client that just established a connection\n"); syslog(LOG_INFO, "serving client: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); if( pthread_create(&client, NULL, &client_thread, pcfd) != 0 ) { DBG("could not launch another client thread\n"); close(pcfd->fd); free(pcfd); continue; } pthread_detach(client); } DBG("leaving server thread, calling cleanup function now\n"); pthread_cleanup_pop(1); return NULL; }
其中,用于保存客户端子线程参数的结构体定义如下:
typedef struct { context *pc; //保留父线程的变量 int fd; //保存监听accept返回的套接字描述符 } cfd;
每个连接请求的处理都是在void *client_thread( void *arg )中进行。函数首先对连接请求的参数进行解析配置,根据req.type确定发送数据的方式:
/* thread for clients that connected to this server */ void *client_thread( void *arg ) { int cnt; char buffer[BUFFER_SIZE]={0}, *pb=buffer; iobuffer iobuf; request req; //本地连接请求文件描述变量 cfd lcfd; //如果传入参数不为空,则将参数的内容拷贝到 lcfd 中(参数为 pcfd ,不为空) if (arg != NULL) { memcpy(&lcfd, arg, sizeof(cfd)); free(arg); } else return NULL; /* 初始化结构体 */ // 把iobuf清为0,iobuf变量在_readline函数中被使用,起一个临时缓存的作用 // iobuf的level成员表示buffer中还剩多少字节的空间,而buffer成员用于存放数据 init_iobuffer(&iobuf); //http协议,需要客服端给服务器发送一个请求,而request就是这个请求 init_request(&req); //从客服端接收数据,表示客服端发来的请求,确定给客服端发什么数据 memset(buffer, 0, sizeof(buffer)); //_readline()函数:从客服端中读取一行的数据,以换行符结束 if ( (cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer)-1, 5)) == -1 ) { close(lcfd.fd); return NULL; } /* 解析参数 */ if ( strstr(buffer, "GET /?action=snapshot") != NULL ) { //请求字符串中含有"GET /?action=snapshot",则请求类型为 A_SNAPSHOT(拍照类型) req.type = A_SNAPSHOT; } else if ( strstr(buffer, "GET /?action=stream") != NULL ) { //如果请求字符串中含有"GET /?action=stream",则请求类型为 A_STREAM(发送视频流类型) req.type = A_STREAM; } else if ( strstr(buffer, "GET /?action=command") != NULL ) { //命令请求,将请求后面的参数保存到 req.parameter int len; req.type = A_COMMAND; /* advance by the length of known string */ if ( (pb = strstr(buffer, "GET /?action=command")) == NULL ) { DBG("HTTP request seems to be malformed\n"); send_error(lcfd.fd, 400, "Malformed HTTP request"); close(lcfd.fd); return NULL; } pb += strlen("GET /?action=command"); /* only accept certain characters */ len = MIN(MAX(strspn(pb, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-=&1234567890"), 0), 100); req.parameter = malloc(len+1); if ( req.parameter == NULL ) { exit(EXIT_FAILURE); } memset(req.parameter, 0, len+1); strncpy(req.parameter, pb, len); DBG("command parameter (len: %d): \"%s\"\n", len, req.parameter); } else { int len; DBG("try to serve a file\n"); req.type = A_FILE; if ( (pb = strstr(buffer, "GET /")) == NULL ) { DBG("HTTP request seems to be malformed\n"); send_error(lcfd.fd, 400, "Malformed HTTP request"); close(lcfd.fd); return NULL; } pb += strlen("GET /"); len = MIN(MAX(strspn(pb, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-1234567890"), 0), 100); req.parameter = malloc(len+1); if ( req.parameter == NULL ) { exit(EXIT_FAILURE); } memset(req.parameter, 0, len+1); strncpy(req.parameter, pb, len); DBG("parameter (len: %d): \"%s\"\n", len, req.parameter); } /* * parse the rest of the HTTP-request * the end of the request-header is marked by a single, empty line with "\r\n" */ do { memset(buffer, 0, sizeof(buffer)); //从客户端再次读取一次字符串 if ( (cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer)-1, 5)) == -1 ) { free_request(&req); close(lcfd.fd); return NULL; } if ( strstr(buffer, "User-Agent: ") != NULL ) { //如果buffer(客服端)中存有(发送了)用户名,则将用户名保存到 req.client 中 req.client = strdup(buffer+strlen("User-Agent: ")); } //如果buffer(客服端)中存有(发送了)密码,则将密码保存到 req.credentials 中 else if ( strstr(buffer, "Authorization: Basic ") != NULL ) { req.credentials = strdup(buffer+strlen("Authorization: Basic ")); //对密码进行解码 decodeBase64(req.credentials); DBG("username:password: %s\n", req.credentials); } } while( cnt > 2 && !(buffer[0] == '\r' && buffer[1] == '\n') ); //如果支持密码功能,则要检查用户名和密码是否匹配 if ( lcfd.pc->conf.credentials != NULL ) { if ( req.credentials == NULL || strcmp(lcfd.pc->conf.credentials, req.credentials) != 0 ) { DBG("access denied\n"); send_error(lcfd.fd, 401, "username and password do not match to configuration"); close(lcfd.fd); if ( req.parameter != NULL ) free(req.parameter); if ( req.client != NULL ) free(req.client); if ( req.credentials != NULL ) free(req.credentials); return NULL; } DBG("access granted\n"); } //根据请求的类型,采取相应的行动 switch ( req.type ) { case A_SNAPSHOT: DBG("Request for snapshot\n"); send_snapshot(lcfd.fd); break; case A_STREAM: DBG("Request for stream\n"); send_stream(lcfd.fd); break; case A_COMMAND: if ( lcfd.pc->conf.nocommands ) { send_error(lcfd.fd, 501, "this server is configured to not accept commands"); break; } command(lcfd.pc->id, lcfd.fd, req.parameter); break; case A_FILE: if ( lcfd.pc->conf.www_folder == NULL ) send_error(lcfd.fd, 501, "no www-folder configured"); else send_file(lcfd.pc->id, lcfd.fd, req.parameter); break; default: DBG("unknown request\n"); } close(lcfd.fd); free_request(&req); DBG("leaving HTTP client thread\n"); return NULL; }
其中_readline()函数通过调用_read()函数从客服端中读取一行的数据,将其逐个防止在buffer上,并将其buffer返回,用于参数解析;
/* read just a single line or timeout */ int _readline(int fd, iobuffer *iobuf, void *buffer, size_t len, int timeout) { char c='\0', *out=buffer; int i; memset(buffer, 0, len); //从iobuf.buf[]中逐个字节的将数据取出存放到buffer中,直到遇见换行符'\n'或者长度达到了 for ( i=0; i<len && c != '\n'; i++ ) { if ( _read(fd, iobuf, &c, 1, timeout) <= 0 ) { /* timeout or error occured */ return -1; } *out++ = c; } return i; } int _read(int fd, iobuffer *iobuf, void *buffer, size_t len, int timeout) { int copied=0, rc, i; fd_set fds; struct timeval tv; memset(buffer, 0, len); while ( (copied < len) ) { //第一次,i=0(iobuf->level初始化为0,len-copied,其中len为1,copied为0),以后,i=1 //i=0相当于下面的不拷贝 //iobuf->level表示iobuf->buffer中的字节数,初始时为0 i = MIN(iobuf->level, len-copied); memcpy(buffer+copied, iobuf->buffer+IO_BUFFER-iobuf->level, i); iobuf->level -= i; copied += i; if ( copied >= len ) return copied; //当客服端发有数据或者超时的时候,select函数就返回,目的防止while循环永不退出 tv.tv_sec = timeout; tv.tv_usec = 0; FD_ZERO(&fds); FD_SET(fd, &fds); if ( (rc = select(fd+1, &fds, NULL, NULL, &tv)) <= 0 ) { if ( rc < 0) exit(EXIT_FAILURE); /* this must be a timeout */ return copied; } init_iobuffer(iobuf); /* * there should be at least one byte, because select signalled it. * But: It may happen (very seldomly), that the socket gets closed remotly between * the select() and the following read. That is the reason for not relying * on reading at least one byte. */ //调用read函数,从客服端读取最多 256 字节的数据,存放到iobuf->buffer if ( (iobuf->level = read(fd, &iobuf->buffer, IO_BUFFER)) <= 0 ) { /* an error occured */ return -1; } //拷贝iobuf->buffer中的数据到地址iobuf->buffer+(IO_BUFFER-iobuf->level) memmove(iobuf->buffer+(IO_BUFFER-iobuf->level), iobuf->buffer, iobuf->level); } return 0; }
经过对http请求的参数解析,确定服务器发送数据的类型,常用的是snapshot和stream模式,简单的说,就是发送单帧图像或者实时视频(其实是通过一帧帧图像实现的)。
/****************************************************************************** Description.: Send a complete HTTP response and a single JPG-frame. Input Value.: fildescriptor fd to send the answer to Return Value: - ******************************************************************************/ void send_snapshot(int fd) { unsigned char *frame=NULL; int frame_size=0; char buffer[BUFFER_SIZE] = {0}; //等待输入通道input_uvc.c里发送数据更新请求 */ //输入通道:摄像头源源不断采集数据,每采集完一帧数据就会往仓库里存放数据, //存放好后,通过条件变量发出一个数据更新信号(通过phread_cond_broadcast函数) //得到数据更新信号后锁定互斥锁 pthread_cond_wait(&pglobal->db_update, &pglobal->db); //获得一帧图像的大小,经过输入通道采集并复制到全局缓冲区后,该全局变量会随之更新 frame_size = pglobal->size; //根据一帧数据的大小,分配一个本地 frame 缓冲区,如分配内存出错,释放内存并解锁互斥锁 if ( (frame = malloc(frame_size+1)) == NULL ) { free(frame); pthread_mutex_unlock( &pglobal->db ); send_error(fd, 500, "not enough memory"); return; } //从仓库(pglobal->buf)中取出一帧数据,并将其放置在frame中 memcpy(frame, pglobal->buf, frame_size); DBG("got frame (size: %d kB)\n", frame_size/1024); pthread_mutex_unlock( &pglobal->db ); /* write the response */ //buffer的字符串为HTTP/1.0 200 OK\r\n" STD_HEADER \"Content-type: image/jpeg //HTTP/1.0 表明http协议所用版本1.0 sprintf(buffer, "HTTP/1.0 200 OK\r\n" \ STD_HEADER \ "Content-type: image/jpeg\r\n" \ "\r\n"); //将buffer中的字符串发送给客服端 */ //对于mjpeg-streamer,输出通道是通过socket编程来模拟http协议, //而对于我们的http协议来说,它需要先让客户端发送一个请求, //当服务器收到这个请求以后,接下来会发送应答,首先会发送一个头部信息, //http应答中的头部信息保存在buffer(报文),会报告http协议所用的版本 if( write(fd, buffer, strlen(buffer)) < 0 ) { free(frame); return; } //发送一帧图像 write(fd, frame, frame_size); free(frame); }
/****************************************************************************** Description.: Send a complete HTTP response and a stream of JPG-frames. Input Value.: fildescriptor fd to send the answer to Return Value: - ******************************************************************************/ void send_stream(int fd) { unsigned char *frame=NULL, *tmp=NULL; int frame_size=0, max_frame_size=0; char buffer[BUFFER_SIZE] = {0}; DBG("preparing header\n"); sprintf(buffer, "HTTP/1.0 200 OK\r\n" \ STD_HEADER \ "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \ "\r\n" \ "--" BOUNDARY "\r\n"); // 将 buffer 中的字符串发送出去(报文) if ( write(fd, buffer, strlen(buffer)) < 0 ) { free(frame); return; } DBG("Headers send, sending stream now\n"); //循环发送图片形成视频流 //进入循环,pglobal->stop为1时终止,按ctrl+c时pglobal->stop为1 while ( !pglobal->stop ) { /* 等待输入通道发出数据更新的信号 */ pthread_cond_wait(&pglobal->db_update, &pglobal->db); /* 读取一帧图像的大小 */ frame_size = pglobal->size; /* 检查我们之前分配的缓存是否够大,如果不够,则重新分配 */ if ( frame_size > max_frame_size ) { DBG("increasing buffer size to %d\n", frame_size); max_frame_size = frame_size+TEN_K; if ( (tmp = realloc(frame, max_frame_size)) == NULL ) { free(frame); pthread_mutex_unlock( &pglobal->db ); send_error(fd, 500, "not enough memory"); return; } frame = tmp; } //从仓库中取出一帧数据放在 frame memcpy(frame, pglobal->buf, frame_size); DBG("got frame (size: %d kB)\n", frame_size/1024); pthread_mutex_unlock( &pglobal->db ); //让 buffer = ""报文,告诉客服端即将发送的图片的大小 sprintf(buffer, "Content-Type: image/jpeg\r\n" \ "Content-Length: %d\r\n" \ "\r\n", frame_size); DBG("sending intemdiate header\n"); //发送报文 if ( write(fd, buffer, strlen(buffer)) < 0 ) break; //发送一帧图像 DBG("sending frame\n"); if( write(fd, frame, frame_size) < 0 ) break; //发送这个字符串是因为对于客户端来说,接收一帧数据怎么知道接收一帧数据接收完。 //一种是根据frame_size来判断一帧数据是否接收完, //另一种是接收的数据是字符串boundarydonotcross(每两帧图片的边界值)的时候, //就表示一帧图片接收完了,即将接收的是第二帧图片 DBG("sending boundary\n"); sprintf(buffer, "\r\n--" BOUNDARY "\r\n"); if ( write(fd, buffer, strlen(buffer)) < 0 ) break; } free(frame); }