之前已经分析了main函数,了解了整个程序的主体,然后分析了input_init函数和input_run函数,理清了输入渠道
本篇讲解剩下的output_init函数和output_run函数(即输出渠道)
1、output_init(...)函数
发现只是做了解析参数和给变量赋值的工作(解析参数同之前分析一样),直接贴代码:
int output_init(output_parameter *param) {
char *argv[MAX_ARGUMENTS]={NULL};
int argc=1, i;
int port;
char *credentials, *www_folder;
char nocommands;
DBG("output #%02d\n", param->id);
port = htons(8080);
credentials = NULL;
www_folder = NULL;
nocommands = 0;
argv[0] = OUTPUT_PLUGIN_NAME;
if ( param->parameter_string != NULL && strlen(param->parameter_string) != 0 ) {
char *arg=NULL, *saveptr=NULL, *token=NULL;
arg=(char *)strdup(param->parameter_string);
if ( strchr(arg, ' ') != NULL ) {
token=strtok_r(arg, " ", &saveptr);
if ( token != NULL ) {
argv[argc] = strdup(token);
argc++;
while ( (token=strtok_r(NULL, " ", &saveptr)) != NULL ) {
argv[argc] = strdup(token);
argc++;
if (argc >= MAX_ARGUMENTS) {
OPRINT("ERROR: too many arguments to output plugin\n");
return 1;
}
}
}
}
}
for (i=0; iid].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;
OPRINT("www-folder-path...: %s\n", (www_folder==NULL)?"disabled":www_folder);
OPRINT("HTTP TCP port.....: %d\n", ntohs(port));
OPRINT("username:password.: %s\n", (credentials==NULL)?"disabled":credentials);
OPRINT("commands..........: %s\n", (nocommands)?"disabled":"enabled");
return 0;
}
2、output_run(...)函数
跟input_run(...)差不多,都是在里面创建一个线程
int output_run(int id)
{
DBG("launching server thread #%02d\n", id);//打印一个调试信息
pthread_create(&(servers[id].threadID), NULL, server_thread, &(servers[id]));//创建一个线程,注:最后一个参数是传递给server_thread函数用的
//server_thread函数细节见下方同色代码部分
pthread_detach(servers[id].threadID);//等待线程结束,以便回收它的资源
return 0;
}
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;//取出global这个结构体变量
pthread_cleanup_push(server_cleanup, pcontext);//当线程结束的时候会调用server_cleanup函数来做些清理工作
//开始socket相关的初始化工作了(博客里正好有一篇《socket网络编程基础篇》)
pcontext->sd = socket(PF_INET, SOCK_STREAM, 0);
if ( pcontext->sd < 0 ) {
fprintf(stderr, "socket failed\n");
exit(EXIT_FAILURE);
}
on = 1;
//此函数的做用是:如果有其它服务器已经使用了此(ip+端口号),那么还可以继续使用此(ip+端口号){因为给此函数传入了标志SO_REUSEADDR}
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; //AF_INET决定了要使用ipv4地址(32位)+端口号(16位)的组合
addr.sin_port = pcontext->conf.port; //端口号
addr.sin_addr.s_addr = htonl(INADDR_ANY); //ip地址,这里的INADDR_ANY表示监听本地所有的ip
//将套接字和(ip+端口)进行绑定
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);
}
//开始监听,参数2表示最多可以同时连接10个客户端
if ( listen(pcontext->sd, 10) != 0 ) {
fprintf(stderr, "listen failed\n");
exit(EXIT_FAILURE);
}
while ( !pglobal->stop )
{
cfd *pcfd = malloc(sizeof(cfd));//分配一个cfd结构体,里面有两成员 : 1、socket套接字标记fd 2、context结构体(内部有线程ID等等信息)
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");
//下面的accept函数会使得在此一直等待,等待客户端来连接自己。注:连接上后会将客户端的地址传过来放在此函数的参数2中(以后会用到它)
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 ) {//client_thread函数详见下面同色代码
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;
}
void *client_thread( void *arg ) //这个arg参数是创建线程时候传来的(即就是pthread_create函数的最后一个参数)
{
int cnt;
char buffer[BUFFER_SIZE]={0}, *pb=buffer;
iobuffer iobuf;
request req;
cfd lcfd; /* local-connected-file-descriptor */
if (arg != NULL) //如果传入的参数arg不为空,则将参数的内容拷贝到lcfd中去
{
memcpy(&lcfd, arg, sizeof(cfd));
free(arg);
}
else
return NULL;//否则就直接返回
//初始化iobuf,将里面两个成员变量清为0
init_iobuffer(&iobuf);//iobuf会在下面的_readline(...)函数中被使用:它只相当于一个临时缓存。iobuf里有两成员:
1、int level; //buffer中的数据量
2、char buffer[IO_BUFFER];//用于存放数据
//初始化req,注:http协议首先需要客户端给服务端发一个请求(即此处的req)
init_request(&req);
//从客户端接受一些数据(即客户端发来的请求),才知道要给客户端发什么数据
memset(buffer, 0, sizeof(buffer));
if ( (cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer)-1, 5)) == -1 ) //readline函数:从客户端读取一行数据,以换行符"\n"结束
{ //readline内部最终会使用到socket的读函数read()
close(lcfd.fd);
return NULL;
}
//因为上面收到客户端发来的请求了,所以下面解析一下请求,并决定给客户端发什么数据
if ( strstr(buffer, "GET /?action=snapshot") != NULL ) {
req.type = A_SNAPSHOT;//只发一张图片(拍照)
}
else if ( strstr(buffer, "GET /?action=stream") != NULL ) {
req.type = A_STREAM;//我走的是此分支,即获得一个标记,表示要给客户端发送视频流的数据
}
else if ( strstr(buffer, "GET /?action=command") != NULL ) //如果是command请求就走此分支内部
{
int len;
req.type = A_COMMAND;
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");
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//如果那三种请求都不是就走这个分支
{
//将请求后面的参数拷贝到req.parameter中
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);
}
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;
}
//开始解析刚才收到的客户端的数据buffer
if ( strstr(buffer, "User-Agent: ") != NULL ) {
req.client = strdup(buffer+strlen("User-Agent: "));//如果客户端发来的字符串中有用户名,就将其拷贝到req.client中
}
else if ( strstr(buffer, "Authorization: Basic ") != NULL ) {
req.credentials = strdup(buffer+strlen("Authorization: Basic "));//如果发来的字符串中有密码,就将其拷贝到req.credentials中
decodeBase64(req.credentials);//对密码进行解码
DBG("username:password: %s\n", req.credentials);
}
} while( cnt > 2 && !(buffer[0] == '\r' && buffer[1] == '\n') );//接受的字节数小于2字节就退出此循环(可以不用此功能)
//如果支持密码功能,则要检查用户名和密码是否能匹配
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);//发送视频流(走的此分支)//send_stream函数的内部实现见下面同色代码部分
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;
}
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");
//使得buffer = ".....",这是一串报文
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");
//将报文发送给客户端
if ( write(fd, buffer, strlen(buffer)) < 0 ) {
free(frame);
return;
}
DBG("Headers send, sending stream now\n");
while ( !pglobal->stop ) //只要不按Ctrl+C 就会一直在这个while循环中连续发送数据给客户端了
{
//等待输入渠道来发送一个数据更新的信号(之前在分析输入渠道时候讲过的,数据更新了就会调用pthread_cond_broadcast(&pglobal->dbupdate)函数来发更新信号)
//这边收到更新信号后才会继续往下执行,否则就等待
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;
}
//将中介global中的buf拷贝到frame中去
memcpy(frame, pglobal->buf, frame_size);
DBG("got frame (size: %d kB)\n", frame_size/1024);
pthread_mutex_unlock( &pglobal->db );//解锁
//使得buffer = "Content-Type:........"(即报文):表明了我们接下来要发送的这一帧图片有多大
sprintf(buffer, "Content-Type: image/jpeg\r\n" \
"Content-Length: %d\r\n" \
"\r\n", frame_size);
DBG("sending intemdiate header\n");
//将buffer发送给客户端
if ( write(fd, buffer, strlen(buffer)) < 0 ) break;
DBG("sending frame\n");
//将一帧图片发送出去
if( write(fd, frame, frame_size) < 0 ) break;
DBG("sending boundary\n");
//让buffer = "boundarydonotcross"这个字符串
sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
//将这个字符串发送给客户端,即告诉客户端一帧数据接收完了(通过标志boundarydonotcross):即相当于两帧图片之间的边界!
if ( write(fd, buffer, strlen(buffer)) < 0 ) break;
}
free(frame);
}
答:猜测可能是防止产生粘包吧!
而像上面这样人为地(有点复杂)给它做一个边界就不会出乱子了,也算是socket编程的高级用法吧,比较稳定!
到此为止mipg-streamer的分析完毕!