如果使用如下指令启动的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};从此也可看出-o可以接受什么参数,一般要指定-p 8080(默认),-w /www
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;
}
}
***********************************************************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可以看出线程函数(对应一个-o的)server_thread里面是 为每个addrinfo(最多50个)创建一个套接字 ,然后去监听。每当一个套接字上有客户端链接请求,就会再创建一个线 程去传输数据。这里的套接字地址与beginning linux programming上讲的不太一样,在ipv6新加的吧。。。
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;
}
./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可以看到此函数会按照浏览器地址指定的文件在www目录寻找这个文件,然后发送出去。所以可以按照项目要求自己加一些网页进去就可以扩增功能啦
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);
}
比如在板子上
[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详细部分见下文。