---WebCam网络摄像头10 socket
如果使用如下指令启动的mjpg_streamer
./mjpg_streamer -o "output_http.so -w ./www" -i "input_s3c2410.so -d /dev/camera"则在mjpg_streamer.c中的两条指令
for (i=0; i<global.outcnt; i++) {//只指定了一个-o,global.outcnt = 1 global.out[i].init(&global.out[i].param); global.out[i].run(global.out[i].param.id); }
分别是执行output_http.c中的
output_init(output_parameter *param)// param.parameter_string="-w ./www" output_run(int id) //id=0
搜索"见下面"取得线索。
***********************************************************init***************************************************************************
在output_http.c里,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; /* convert the single parameter-string to an array of strings */ 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; } } } } } /* show all parameters for DBG purposes */ for (i=0; i<argc; i++) { DBG("argv[%d]=%s\n", i, argv[i]); } reset_getopt(); while(1) { int option_index = 0, c=0; static struct option long_options[] = \ { {"h", no_argument, 0, 0}, {"help", no_argument, 0, 0}, {"p", required_argument, 0, 0}, {"port", required_argument, 0, 0}, {"c", required_argument, 0, 0}, {"credentials", required_argument, 0, 0}, {"w", required_argument, 0, 0}, {"www", required_argument, 0, 0}, {"n", no_argument, 0, 0}, {"nocommands", no_argument, 0, 0}, {0, 0, 0, 0} }; c = getopt_long_only(argc, argv, "", long_options, &option_index); /* no more options to parse */ if (c == -1) break; /* unrecognized option */ if (c == '?'){ help(); return 1; } switch (option_index) { /* h, help */ case 0: case 1: DBG("case 0,1\n"); help(); return 1; break; /* p, port */ case 2: case 3: DBG("case 2,3\n"); port = htons(atoi(optarg)); break; /* c, credentials */ case 4: case 5: DBG("case 4,5\n"); credentials = strdup(optarg); break; /* w, www */ case 6: case 7: DBG("case 6,7\n"); www_folder = malloc(strlen(optarg)+2); strcpy(www_folder, optarg); if ( optarg[strlen(optarg)-1] != '/' ) strcat(www_folder, "/"); break; /* n, nocommands */ case 8: case 9: DBG("case 8,9\n"); nocommands = 1; break; } }从此也可看出-o可以接受什么参数,一般要指定-p 8080(默认),-w /www
***********************************************************run***************************************************************************
在output_http.c里,output_run源码如下
int output_run(int id) { DBG("launching server thread #%02d\n", id); /* create thread and pass context to thread function */ pthread_create(&(servers[id].threadID), NULL, server_thread, &(servers[id]));//见下面 pthread_detach(servers[id].threadID); return 0; }由于在mjpg_streamer.c中是根据-o的数量使用for循环调用的output_run(),所以有几个-o就会创建几个服务线程,每个服务线程对应一个线程上下文servers[id],id是线程的序号(即-o的序号)
pthread_create的
参数1.servers[id].threadID.第id个线程对应的线程号
参数4.servers[id] 第id个线程的上下文参数,成员如下
/* context of each server thread */ typedef struct { int sd[MAX_SD_LEN]; int sd_len; int id; globals *pglobal; pthread_t threadID; config conf; } context;//httpd.h #define MAX_OUTPUT_PLUGINS 10//mjpg-streamer.h 可知最多支持10个 -o context servers[MAX_OUTPUT_PLUGINS];//output_http.c
然后进入线程函数
/****************************************************************************** Description.: Open a TCP socket and wait for clients to connect. If clients connect, start a new thread for each accepted connection. Input Value.: arg is a pointer to the globals struct Return Value: always NULL, will only return on exit ******************************************************************************/ void *server_thread( void *arg ) { int on; pthread_t client; struct addrinfo *aip, *aip2; struct addrinfo hints; struct sockaddr_storage client_addr; socklen_t addr_len = sizeof(struct sockaddr_storage); fd_set selectfds; int max_fds = 0; char name[NI_MAXHOST]; int err; int i; context *pcontext = arg; pglobal = pcontext->pglobal; /* set cleanup handler to cleanup ressources */ pthread_cleanup_push(server_cleanup, pcontext); bzero(&hints, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_flags = AI_PASSIVE; hints.ai_socktype = SOCK_STREAM;//tcp //为调用getaddrinfo()准备hints snprintf(name, sizeof(name), "%d", ntohs(pcontext->conf.port)); //端口号 8080 if((err = getaddrinfo(NULL, name, &hints, &aip)) != 0) { //取得指定类型的socket address(addrinfo),以便后面的函数使用 //参数1 主机名或ip //参数2 服务名或端口号 //参数3 指定需要返回的地址类型 //参数4 返回的第一个addrinfo结构体(通过遍历addrinfo结构体的链表得到所有符合条件的addrinfo) //hints.ai_flags = AI_PASSIVE;和主机名设为NULL,则此函数会返回本机所有ip的addrinfo,包括回环地址127.0.0.1和本地地址如192.168.1.230 //refer to man getaddrinfo , http://blog.csdn.net/lgtnt/article/details/3745194 perror(gai_strerror(err)); exit(EXIT_FAILURE); } for(i = 0; i < MAX_SD_LEN; i++) pcontext->sd[i] = -1; //httpd.c #define MAX_SD_LEN 50 //初始化所有的套接字描述符为-1 /* open sockets for server (1 socket / address family) */ i = 0; for(aip2 = aip; aip2 != NULL; aip2 = aip2->ai_next) { //遍历所有的套接字地址,为每一个地址创建一个套接字(服务器套接字)。最多可以建立MAX_SD_LEN个(50)--即最多支持本机的50个ip。但 //通过上面的getaddrinfo()返回的是两个socket地址(ip),一个是回环ip 127.0.0.1一个是本地ip比如192.168.1.230 //所以执行2次 if((pcontext->sd[i] = socket(aip2->ai_family, aip2->ai_socktype, 0)) < 0) { //创建socket,返回套接字描述符 pcontext->sd[i] //每个-o 会创建2个(也是所有了)socket,即会监视本机的所有ip的8080端口 continue; } /* ignore "socket already in use" errors */ on = 1; if(setsockopt(pcontext->sd[i], SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) { perror("setsockopt(SO_REUSEADDR) failed"); } /* IPv6 socket should listen to IPv6 only, otherwise we will get "socket already in use" */ on = 1; if(aip2->ai_family == AF_INET6 && setsockopt(pcontext->sd[i], IPPROTO_IPV6, IPV6_V6ONLY, (const void *)&on , sizeof(on)) < 0) { perror("setsockopt(IPV6_V6ONLY) failed"); } /* perhaps we will use this keep-alive feature oneday */ /* setsockopt(sd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); */ if(bind(pcontext->sd[i], aip2->ai_addr, aip2->ai_addrlen) < 0) { //为上面创建的socket绑定地址 perror("bind"); pcontext->sd[i] = -1; continue; } if(listen(pcontext->sd[i], 10) < 0) { //创建一个可以容纳2个请求者的监听队列 perror("listen"); pcontext->sd[i] = -1; } else { i++; if(i >= MAX_SD_LEN) { OPRINT("%s(): maximum number of server sockets exceeded", __FUNCTION__); i--; break; } } } pcontext->sd_len = i; if(pcontext->sd_len < 1) { OPRINT("%s(): bind(%d) failed", __FUNCTION__, htons(pcontext->conf.port)); closelog(); exit(EXIT_FAILURE); } /* create a child for every client that connects */ while ( !pglobal->stop ) { //int *pfd = (int *)malloc(sizeof(int)); 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"); do { FD_ZERO(&selectfds); for(i = 0; i < MAX_SD_LEN; i++) { if(pcontext->sd[i] != -1) { FD_SET(pcontext->sd[i], &selectfds); //将上面创建的socket加入selectfds描述符集合 if(pcontext->sd[i] > max_fds) max_fds = pcontext->sd[i]; } } err = select(max_fds + 1, &selectfds, NULL, NULL, NULL); //使用select监听文件文件描述符集合,没有动静就阻塞在这里。有动静继续执行。 if (err < 0 && errno != EINTR) { perror("select"); exit(EXIT_FAILURE); } } while(err <= 0); for(i = 0; i < max_fds + 1; i++) { //遍历所有的服务器套接字描述符,以便确认是哪个套接字上有链接请求 if(pcontext->sd[i] != -1 && FD_ISSET(pcontext->sd[i], &selectfds)) { pcfd->fd = accept(pcontext->sd[i], (struct sockaddr *)&client_addr, &addr_len); //accept函数会自动创建一个新的套接字于这个客户端套接字通信,并且返回新套接字的文件描述符。原有的套接字继续执行监听。 pcfd->pc = pcontext; /*httpd.c typedef struct { context *pc; int fd; } cfd; */ /* start new thread that will handle this TCP connected client */ DBG("create thread to handle client that just established a connection\n"); if(getnameinfo((struct sockaddr *)&client_addr, addr_len, name, sizeof(name), NULL, 0, NI_NUMERICHOST) == 0) { syslog(LOG_INFO, "serving client: %s\n", name); } if( pthread_create(&client, NULL, &client_thread, pcfd) != 0 ) {//见下面 //为客户端创建服务线程 //参数4 pcfd //pcfd->fd 套接字的文件描述符 //pcfd->pc 套接字上下文 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; }可以看出线程函数(对应一个-o的)server_thread里面是为每个addrinfo(最多50个)创建一个套接字,然后去监听。每当一个套接字上有客户端链接请求,就会再创建一个线程去传输数据。这里的套接字地址与beginning linux programming上讲的不太一样,在ipv6新加的吧。。。
./mjpg_streamer -i "input_s3c2410.so -d /dev/camera" -o "output_http.so -p 8080" -o "output_http.so -p 8081"
这样就会创建2个线程,
一个线程里面会创建2个套接字,一个在侦听127.0.0.1:8080,一个在侦听192.168.1.230:8080
另一个线程也会创建2个套接字,一个在侦听127.0.0.1:8081,一个在侦听192.168.1.230:8081
然后在客户端的浏览器中同时访问如下两个网址
http://192.168.1.230:8080/?action=stream
http://192.168.1.230:8081/?action=stream
则服务器上监听192.168.1.230:8080和监听192.168.1.230:8081的socket就会accept()---
函数会自动创建一个新的套接字(和一个线程)与这个客户端套接字通信,并且返回新套接字的文件描述符。原有的套接字继续执行监听。所以之后再开多个浏览器去访问比如http://192.168.1.230:8080/?action=stream也可以访问得到数据。
以上是个人理解仅供参考
线程函数如下
/****************************************************************************** Description.: Serve a connected TCP-client. This thread function is called for each connect of a HTTP client like a webbrowser. It determines if it is a valid HTTP request and dispatches between the different response options. Input Value.: arg is the filedescriptor and server-context of the connected TCP socket. It must have been allocated so it is freeable by this thread function. Return Value: always NULL ******************************************************************************/ /* 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; /* local-connected-file-descriptor */ /* we really need the fildescriptor and it must be freeable by us */ if (arg != NULL) { memcpy(&lcfd, arg, sizeof(cfd)); free(arg); } else return NULL; /* initializes the structures */ init_iobuffer(&iobuf); init_request(&req); /* What does the client want to receive? Read the request. */ memset(buffer, 0, sizeof(buffer)); if ( (cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer)-1, 5)) == -1 ) { //从描述符(客户端)读取一行数据到buffer close(lcfd.fd); return NULL; } /* determine what to deliver */ if ( strstr(buffer, "GET /?action=snapshot") != NULL ) { req.type = A_SNAPSHOT; } else if ( strstr(buffer, "GET /?action=stream") != NULL ) { req.type = A_STREAM; //比如浏览器中输入 http://192.168.1.230:8080/?action=stream } else if ( strstr(buffer, "GET /?action=command") != NULL ) { 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); if ( unescape(req.parameter) == -1 ) { free(req.parameter); send_error(lcfd.fd, 500, "could not properly unescape command parameter string"); LOG("could not properly unescape command parameter string\n"); close(lcfd.fd); return NULL; } 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 ) { req.client = strdup(buffer+strlen("User-Agent: ")); } 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') ); /* check for username and password if parameter -c was given */ 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"); } /* now it's time to answer */ 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; }
下面是服务器响应客户端的?action=stream请求所发送的全部数据-----一个web服务器发送数据的实现
比如 http://192.168.1.230:8081/?action=stream
从这个函数可以看出,在运行程序时即使不使能www 路径也可以观看图像。因为它发送了完整的http标记。
/****************************************************************************** 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"); if ( write(fd, buffer, strlen(buffer)) < 0 ) { //发送http头 free(frame); return; } DBG("Headers send, sending stream now\n"); while ( !pglobal->stop ) { //只要没停止就一直发送图像,所以在浏览器中看到的是 视频 /* wait for fresh frames */ pthread_cond_wait(&pglobal->db_update, &pglobal->db); /* read buffer */ frame_size = pglobal->size; /* check if framebuffer is large enough, increase it if necessary */ 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; } memcpy(frame, pglobal->buf, frame_size); DBG("got frame (size: %d kB)\n", frame_size/1024); pthread_mutex_unlock( &pglobal->db ); /* * print the individual mimetype and the length * sending the content-length fixes random stream disruption observed * with firefox */ 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; //发送内容--图像数据 DBG("sending boundary\n"); sprintf(buffer, "\r\n--" BOUNDARY "\r\n"); if ( write(fd, buffer, strlen(buffer)) < 0 ) break; //发送http尾 } free(frame); }
而如果客户端想要访问www下的文件,则在服务器端启动mjpg-streamer时需要指定www路径,发送文件的函数是send_file()
一个简单的web服务器的实现
/****************************************************************************** Description.: Send HTTP header and copy the content of a file. To keep things simple, just a single folder gets searched for the file. Just files with known extension and supported mimetype get served. If no parameter was given, the file "index.html" will be copied. Input Value.: * fd.......: filedescriptor to send data to * parameter: string that consists of the filename * id.......: specifies which server-context is the right one Return Value: - ******************************************************************************/ void send_file(int id, int fd, char *parameter) { char buffer[BUFFER_SIZE] = {0}; char *extension, *mimetype=NULL; int i, lfd; config conf = servers[id].conf; /* in case no parameter was given */ if ( parameter == NULL || strlen(parameter) == 0 ) parameter = "index.html"; /* find file-extension */ if ( (extension = strstr(parameter, ".")) == NULL ) { send_error(fd, 400, "No file extension found"); return; } /* determine mime-type */ for ( i=0; i < LENGTH_OF(mimetypes); i++ ) { if ( strcmp(mimetypes[i].dot_extension, extension) == 0 ) { mimetype = (char *)mimetypes[i].mimetype; break; } } /* in case of unknown mimetype or extension leave */ if ( mimetype == NULL ) { send_error(fd, 404, "MIME-TYPE not known"); return; } /* now filename, mimetype and extension are known */ DBG("trying to serve file \"%s\", extension: \"%s\" mime: \"%s\"\n", parameter, extension, mimetype); /* build the absolute path to the file */ strncat(buffer, conf.www_folder, sizeof(buffer)-1); strncat(buffer, parameter, sizeof(buffer)-strlen(buffer)-1); /* try to open that file */ if ( (lfd = open(buffer, O_RDONLY)) < 0 ) { DBG("file %s not accessible\n", buffer); send_error(fd, 404, "Could not open file"); return; } DBG("opened file: %s\n", buffer); /* prepare HTTP header */ sprintf(buffer, "HTTP/1.0 200 OK\r\n" \ "Content-type: %s\r\n" \ STD_HEADER \ "\r\n", mimetype); //发送的这些数据在浏览器中看不到的,是给浏览器一个提供的一个版本识别信息 //浏览器中可以观察到的后面真正的数据(比如index.html的内容) //上面send_stream()发送图像流也是一样,浏览器中只呈现出图像 i = strlen(buffer); /* first transmit HTTP-header, afterwards transmit content of file */ do { if ( write(fd, buffer, i) < 0 ) { close(lfd); return; } } while ( (i=read(lfd, buffer, sizeof(buffer))) > 0 ); /* close file, job done */ close(lfd); }可以看到此函数会按照浏览器地址指定的文件在www目录寻找这个文件,然后发送出去。所以可以按照项目要求自己加一些网页进去就可以扩增功能啦
比如在板子上
[root@FriendlyARM www]# touch a.html [root@FriendlyARM www]# echo hhheh > a.html然后客户端访问
http://192.168.1.230:8080/a.html
同样可以想到,如果在www目录下放一个cgi文件,是否也可以访问呢?
不支持。看上面line 36 --line39,有识别的。如果注释掉那个return,则浏览到的是乱码。
boa是支持的,可以参考一下boa的源码,修改一下send_file()估计就可以了。
http详细部分见下文。