uhttpd是openwrt上默认的Web服务器,支持CGI,lua脚本,以及静态文件的服务。它是一个精简的服务器,一般适合作为路由器这样的嵌入式设备使用,或者Web服务器的入门学习。
uhttpd的源码可以用svn到这里下载。
概述
uhttpd.png
首先,在uhttpd启动的时候,它会先读取参数,进行服务器的配置。参数可以由命令行输入,其中port参数必须制定,其他都有默认值。
配置完参数之后,服务器会进入uh_mainloop,等待请求。
在主循环中,uhttpd采用select进行轮询,而不是采用fork进行并发,一定程度上降低了并发能力,但是很适合这样的小型服务器。
当select检测到有客户端请求,uhttpd就会先接受请求,再进行解析,之后再调用uh_dispatch_request去分发请求。其中,lua请求比较特殊,不由uh_dispatch_request分发。
在分发过程(不包括lua请求)当中,会根据path的前缀来判断是CGI请求还是静态文件请求,默认的CGI前缀是/cgi-bin。CGI请求进入uh_cgi_request,文件请求进入uh_file_request,lua请求则会进入lua_request。
在三种handler中,就会进行请求的处理了。lua_request会调用lua解释器进行处理,file_request直接读取文件并且返回,CGI请求比较复杂,之后会详细说明。
在三种request处理之后,都会返回给客户端。一次循环到此结束。
启动
服务器配置
启动入口的main函数位于uhttpd.c,它接受命令行参数,进行服务器配置,并且启动服务器。让我们先来看看它有哪些配置。
help
其中port必须指定,别的都有默认值。一般情况下我们可以用这样的参数来启动服务器:
1 |
./uhttpd -p 8080 |
服务器默认是运行在后台的,可以使用“ps -A | grep uhttp”看到它的运行情况,用nmap扫描一下本地端口也可以看到它已经在监听8080端口了。
nmap
uhttpd的配置用“struct config”存储,成员也很丰富:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
struct config { char docroot[PATH_MAX]; char *realm; char *file; char *index_file; char *error_handler; int no_symlinks; int no_dirlists; int network_timeout; int rfc1918_filter; int tcp_keepalive; #ifdef HAVE_CGI char *cgi_prefix; #endif #ifdef HAVE_LUA char *lua_prefix; char *lua_handler; lua_State *lua_state; lua_State * (*lua_init) (const char *handler); void (*lua_close) (lua_State *L); void (*lua_request) (struct client *cl, struct http_request *req, lua_State *L); #endif #if defined(HAVE_CGI) || defined(HAVE_LUA) int script_timeout; #endif #ifdef HAVE_TLS char *cert; char *key; SSL_CTX *tls; SSL_CTX * (*tls_init) (void); int (*tls_cert) (SSL_CTX *c, const char *file); int (*tls_key) (SSL_CTX *c, const char *file); void (*tls_free) (struct listener *l); int (*tls_accept) (struct client *c); void (*tls_close) (struct client *c); int (*tls_recv) (struct client *c, void *buf, int len); int (*tls_send) (struct client *c, void *buf, int len); #endif }; |
端口绑定
服务器的端口绑定没有写在config里面,而是直接用uh_socket_bind进行了端口的绑定。
1 2 3 4 5 |
/* bind sockets */ bound += uh_socket_bind( &serv_fds, &max_fd, bind[0] ? bind : NULL, port, &hints, (opt == ‘s‘), &conf ); |
uh_socket_bind:
1 2 3 4 |
static int uh_socket_bind( fd_set *serv_fds, int *max_fd, const char *host, const char *port, struct addrinfo *hints, int do_tls, struct config *conf ) |
此函数进行端口绑定并且把listener加到了一个全局的链表中,于是我们可以绑定多个端口。
1 2 3 4 5 6 |
/* add listener to global list */ if( ! (l = uh_listener_add(sock, conf)) ) { fprintf(stderr, "uh_listener_add(): Failed to allocate memory\n"); goto error; } |
比较有意思的是,uhttpd把一些信息存在了链表里,用add函数在表头插入。C语言没有现成的集合框架,但是自己写一个链表也是很轻松的。这些工具函数都在uhttpd-utils.c里。
1 2 3 4 |
static struct client *uh_clients = NULL; struct client * uh_client_add(int sock, struct listener *serv); struct client * uh_client_lookup(int sock); void uh_client_remove(int sock); |
配置文件
光用命令行的话肯定太麻烦,uhttpd也可以用配置文件来进行配置。
1 2 |
/* config file */ uh_config_parse(&conf); |
但是配置文件的选项好像不是很多,最好的方式还是写一个启动脚本。
正式启动
在一系列的配置之后,uhttpd终于要正式启动了。它默认是后台启动,fork一个子进程,父进程退出,子进程带着配置文件和服务器的FD进入了mainloop。
1 2 |
/* server main loop */ uh_mainloop(&conf, serv_fds, max_fd); |
等待请求
uh_mainloop函数也在uhttp.c里,最外层是一个大的循环。
1 2 3 4 5 6 7 |
while (run) { if( select(max_fd + 1, &read_fds, NULL, NULL, NULL) == -1 ) { ... } ... } |
select
select函数起到了阻塞请求的作用,并且和accept不用的是,它使用轮询机制,而不是fork,更加适合嵌入式设备。
1 2 3 4 5 |
if( select(max_fd + 1, &read_fds, NULL, NULL, NULL) == -1 ) { perror("select()"); exit(1); } |
最后一个参数是设置超时时间,如果设置成NULL,则无限超时,直到FD有变动。
获得请求
之后会进入一个嵌套复杂的“if-else”语句,数了一下最深有六层if嵌套。主要的功能就是遍历所有的FD,分别找到服务端和客户端的FD,在服务端,accept并且把client加入链表。在客户端的FD中,处理请求。
用uh_http_header_recv获得请求之后,用uh_http_header_parse解析,得到一个http_request的结构体。
1 2 3 4 5 6 7 8 |
struct http_request { int method; float version; int redirect_status; char *url; char *headers[UH_LIMIT_HEADERS]; struct auth_realm *realm; }; |
请求分发
得到http_request之后,就可以根据URL来进行请求的分发了。带有lua前缀的给lua_request,否则交给uh_dispatch_request。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* Lua request? */ if( conf->lua_state && uh_path_match(conf->lua_prefix, req->url) ) { conf->lua_request(cl, req, conf->lua_state); } else /* dispatch request */ if( (pin = uh_path_lookup(cl, req->url)) != NULL ) { /* auth ok? */ if( !pin->redirected && uh_auth_check(cl, req, pin) ) uh_dispatch_request(cl, req, pin); } |
dispatch也只是做了一个简单的判断然后就交给下一级了。
1 2 3 4 5 6 7 8 9 |
if( uh_path_match(cl->server->conf->cgi_prefix, pin->name) || (ipr = uh_interpreter_lookup(pin->phys)) ) { uh_cgi_request(cl, req, pin, ipr); } else { uh_file_request(cl, req, pin); } |
处理请求
lua请求暂时不说了,这里只说文件和CGI请求。
静态文件
file_request在uhttpd-file.c中。成员如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
uhttpd-file.c macro _XOPEN_SOURCE _BSD_SOURCE function uh_file_mime_lookup uh_file_mktag uh_file_date2unix uh_file_unix2date uh_file_header_lookup uh_file_response_ok_hdrs uh_file_response_200 uh_file_response_304 uh_file_response_412 uh_file_if_match uh_file_if_modified_since uh_file_if_none_match uh_file_if_range uh_file_if_unmodified_since uh_file_scandir_filter_dir uh_file_dirlist uh_file_request |
既然是静态文件请求,自然是先看看本地有没有这个文件,有的话就读取内容发给客户端,没有就404。
这里有一个有趣的函数,
1 2 3 4 5 6 |
/* test preconditions */ if(ok) ensure_out(uh_file_if_modified_since(cl, req, &pi->stat, &ok)); if(ok) ensure_out(uh_file_if_match(cl, req, &pi->stat, &ok)); if(ok) ensure_out(uh_file_if_range(cl, req, &pi->stat, &ok)); if(ok) ensure_out(uh_file_if_unmodified_since(cl, req, &pi->stat, &ok)); if(ok) ensure_out(uh_file_if_none_match(cl, req, &pi->stat, &ok)); |
处理请求的过程中大量使用了ensure_out,它应该是保证关闭FD的。如果网络发生异常或者文件读写异常,需要保证FD被正确关闭。实现很简单,一个类函数宏就搞定了。
1 2 3 4 5 6 |
#define ensure_out(x) \ do { if((x) < 0) goto out; } while(0) out: if( fd > -1 ) close(fd); |
CGI请求
处理CGI请求稍微复杂一点。
uh_cgi_request函数位于uhttpd-cgi.c。成员如下。
1 2 3 4 5 6 |
uhttpd-cgi.c function uh_cgi_header_parse uh_cgi_header_lookup uh_cgi_error_500 uh_cgi_request |
虽然成员很少,但是总体还是有600多行。
CGI的处理过程,基本上就是调用CGI程序,获得它的处理结果,然后返回给客户端。但CGI程序和主调函数,肯定是两个进程,它们之间如何通信,如何传递数据,这才是关键。
uhttpd采用了管道和CGI程序进行通信,有两个管道,实现双向通信。一个管道负责从父进程写数据到CGI程序,主要是客户端的POST数据。另一个就是读取CGI程序的处理结果。同时,按照CGI的标准,HTTP请求头都是通过环境变量的方式传给CGI程序的,CGI程序是fork和exec的,所以会继承环境变量,达到传递数据的目的。
在子进程中,则用dup2进行了一个重定向,把输入输出流都定向到了管道。
1 2 3 |
/* patch stdout and stdin to pipes */ dup2(rfd[1], 1); dup2(wfd[0], 0); |
之后就用了大段的代码设置环境变量。
1 2 3 4 5 6 |
setenv("SERVER_NAME", sa_straddr(&cl->servaddr), 1); setenv("SERVER_ADDR", sa_straddr(&cl->servaddr), 1); setenv("SERVER_PORT", sa_strport(&cl->servaddr), 1); setenv("REMOTE_HOST", sa_straddr(&cl->peeraddr), 1); setenv("REMOTE_ADDR", sa_straddr(&cl->peeraddr), 1); setenv("REMOTE_PORT", sa_strport(&cl->peeraddr), 1); |
之后才真正地调用CGI程序。
1 2 3 4 |
if( ip != NULL ) execl(ip->path, ip->path, pi->phys, NULL); else execl(pi->phys, pi->phys, NULL); |
与此同时,父进程则焦急地等待着管道另一头的回音。它等来等去等的不耐烦了,于是它又机制地给自己设置了一个timeout,过了这个时间它就离开了。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ensure_out(rv = select_intr(fd_max, &reader, (content_length > -1) ? &writer : NULL, NULL, &timeout)); ...... /* read it from socket ... */ ensure_out(buflen = uh_tcp_recv(cl, buf, min(content_length, sizeof(buf)))); ..... /* ... and write it to child‘s stdin */ if( write(wfd[1], buf, buflen) < 0 ) perror("write()"); ...... /* read data from child ... */ if( (buflen = read(rfd[0], buf, sizeof(buf))) > 0 ) |
从CGI程序读完了数据之后,它还是不放心,又解析了一下响应头,确认正确之后,才发给了客户端。
到这里,整个处理过程才算结束。
总结
第一次看服务器的源码,所以找了一个比较简单的服务器。大致能够理解它的原理,但是很多细节还是不明白,可能只有自己亲自去实现才能对它有一个深刻的理解。uhttd的代码并不多,其中很多的代码都用来处理错误,可见处理异常情况也是很重要的。有机会的话,希望自己能亲自实现一个服务器。