uhttpd源码分析

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的代码并不多,其中很多的代码都用来处理错误,可见处理异常情况也是很重要的。有机会的话,希望自己能亲自实现一个服务器。

时间: 2024-11-08 04:00:45

uhttpd源码分析的相关文章

TeamTalk源码分析之login_server

login_server是TeamTalk的登录服务器,负责分配一个负载较小的MsgServer给客户端使用,按照新版TeamTalk完整部署教程来配置的话,login_server的服务端口就是8080,客户端登录服务器地址配置如下(这里是win版本客户端): 1.login_server启动流程 login_server的启动是从login_server.cpp中的main函数开始的,login_server.cpp所在工程路径为server\src\login_server.下表是logi

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

1 背景 还记得前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制. PS:阅读本篇前建议先查看前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>,这一篇承接上一篇. 关于View与ViewGroup的区别在前一篇的A

HashMap与TreeMap源码分析

1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Java这么久,也写过一些小项目,也使用过TreeMap无数次,但到现在才明白它的实现原理).因此本着"不要重复造轮子"的思想,就用这篇博客来记录分析TreeMap源码的过程,也顺便瞅一瞅HashMap. 2. 继承结构 (1) 继承结构 下面是HashMap与TreeMap的继承结构: pu

Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7)【转】

原文地址:Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.chinaunix.net/uid-25909619-id-4938395.html 前面粗略分析start_kernel函数,此函数中基本上是对内存管理和各子系统的数据结构初始化.在内核初始化函数start_kernel执行到最后,就是调用rest_init函数,这个函数的主要使命就是创建并启动内核线

Spark的Master和Worker集群启动的源码分析

基于spark1.3.1的源码进行分析 spark master启动源码分析 1.在start-master.sh调用master的main方法,main方法调用 def main(argStrings: Array[String]) { SignalLogger.register(log) val conf = new SparkConf val args = new MasterArguments(argStrings, conf) val (actorSystem, _, _, _) =

Solr4.8.0源码分析(22)之 SolrCloud的Recovery策略(三)

Solr4.8.0源码分析(22)之 SolrCloud的Recovery策略(三) 本文是SolrCloud的Recovery策略系列的第三篇文章,前面两篇主要介绍了Recovery的总体流程,以及PeerSync策略.本文以及后续的文章将重点介绍Replication策略.Replication策略不但可以在SolrCloud中起到leader到replica的数据同步,也可以在用多个单独的Solr来实现主从同步.本文先介绍在SolrCloud的leader到replica的数据同步,下一篇

zg手册 之 python2.7.7源码分析(4)-- pyc字节码文件

什么是字节码 python解释器在执行python脚本文件时,对文件中的python源代码进行编译,编译的结果就是byte code(字节码) python虚拟机执行编译好的字节码,完成程序的运行 python会为导入的模块创建字节码文件 字节码文件的创建过程 当a.py依赖b.py时,如在a.py中import b python先检查是否有b.pyc文件(字节码文件),如果有,并且修改时间比b.py晚,就直接调用b.pyc 否则编译b.py生成b.pyc,然后加载新生成的字节码文件 字节码对象

LevelDB源码分析--Iterator

我们先来参考来至使用Iterator简化代码2-TwoLevelIterator的例子,略微修改希望能帮助更加容易立即,如果有不理解请各位看客阅读原文. 下面我们再来看一个例子,我们为一个书店写程序,书店里有许多书Book,每个书架(BookShelf)上有多本书. 类结构如下所示 class Book { private: string book_name_; }; class Shelf { private: vector<Book> books_; }; 如何遍历书架上所有的书呢?一种实

【Heritrix源码分析】Heritrix基本内容介绍

1.版本说明 (1)最新版本:3.3.0 (2)最新release版本:3.2.0 (3)重要历史版本:1.14.4 3.1.0及之前的版本:http://sourceforge.net/projects/archive-crawler/files/ 3.2.0及之后的版本:http://archive.org/ 由于国情需要,后者无法访问,因此本blog研究的是1.14.4版本. 2.官方材料 source:http://sourceforge.net/projects/archive-cra