mjpg-streamer详解3

之前已经分析了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的分析完毕!



你可能感兴趣的:(项目经验积累)