TinyHTTPd
TinyHTTPd是一个超轻量级的http服务器, 使用C语言开发, 代码只有500多行, 不用于实际生产, 只是为了学习使用. 通过阅读代码可以理解初步web服务器的本质.
主页地址 : http://tinyhttpd.sourceforge.net/
注释后的源码 : https://github.com/tw1996/TinyHTTPd
HTTP协议
在阅读源码之间, 我们先要初步了解HTTP协议. 简单地说HTTP协议就是规定了客户端和服务器的通信格式, 它建立在TCP协议的基础上, 默认使用80端口. 但是并不涉及数据包的传输, 只规定了通信的规范. HTTP本身是无连接的, 也就是说建立TCP连接后就可以直接发送数据, 不必再建立HTTP连接, 对于数据包丢失重传由TCP实现, 下面简单介绍HTTP几个版本.
HTTP/0.9
TCP连接建立后, 客户端只能使用GET方式请求
GET /index.html
服务器只能回应html格式的字符串
<html> <body>Hello World</body> </html>
发送完毕后马上断开TCP连接.
HTTP/1.0
与HTTP/0.9相比, 增加了许多新的功能, 支持任何格式传输, 包括文本, 二进制数据, 文件, 音频等. 支持GET, POST, HEAD命令.
改变了数据通信的格式, 增加了头信息; 其他的新增功能还包括状态码(status code),多字符集支持,多部分发送(multi-part type),权限(authorization),缓存(cache),内容编码(content encoding)等, 所以HTTP协议一共可分为3部分 , 开始行, 首部行, 实体主体. 其中在首部行和实体主体之间以空格分开, 开始行和首部行都是以 结尾 举个例子 :
请求信息
GET / HTTP/1.0 //请求行 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) //请求头 Accept: */* //请求头
响应信息
HTTP/1.0 200 OK //响应行 Content-Type: text/plain //响应头 Content-Length: 137582 Expires: Thu, 05 Dec 1997 16:00:00 GMT Last-Modified: Wed, 5 August 1996 15:55:28 GMT Server: Apache 0.84 <html> //响应主体 <body>Hello World</body> </html>
HTTP/1.0规定, 头信息必须是ASCII码, 后面的数据可以是任何格式, Content-Type 用于规定格式. 下面是一些常见的 Content-Type 字段取值.
text/plain text/html text/css image/jpeg image/png image/svg+xml audio/mp4 video/mp4 application/javascript application/pdf application/zip application/atom+xml
有的浏览器为了提高通信效率, 使用了一个非标准的字段 Connection:keep-alive. 即维持一个TCP连接不断开, 多次发送HTTP数据, 直到客户端或服务器主动断开.
HTTP/1.1
现在最流行的HTTP协议, 默认复用TCP连接, 即不需要手动设置Connection:keep-alive, 客户端在最后一个请求时发送 Connection:close 断开连接.
增加了许多方法 : PUT, PUTCH, HEAD, OPTIONS, DELETE.
引入管道机制, 以前是先发送一个请求, 等待回应继续发送下一个请求. 现在可以连续发送多个请求, 不用等待, 但是服务器仍然会按顺序回应. 使用 Content-Lenth字段区分数据包属于哪一个回应.
为了避免队头堵塞, 只有两种办法 : 少发送数据, 同时开多个持久连接.
HTTP/2
这里就不多做介绍了
CGI与FASTCGI
参考这篇文章 : http://www.php-internals.com/book/?p=chapt02/02-02-03-fastcgi
工作流程
- 服务器启动, 如果没有指定端口则随机选取端口建立套接字监听客户端连接
- accept()会一直阻塞等待客户端连接, 如果客户端连接上, 则创建一个新线程处理该客户端连接.
- 在accetp_request() 主要处理客户端连接, 首先解析HTTP请求报文. 只支持GET/POST请求, 否则返回HTTP501错误. 如果有请求参数的话, 记录在query_string中. 将请求的路径记录在path中, 如果请求的是目录, 则访问该目录下的index.html文件.
- 最后判断请求类型, 如果是静态请求, 直接读取文件发送给客户端; 如果是动态请求, 则fork()一个子进程, 在子进程中调用exec()函数簇执行cgi脚本. 然后父进程读取子进程执行结果 父子进程之间通过管道通信实现.
- 父进程等待子进程结束后, 关闭连接, 完成一次HTTP请求.
源码分析
首先看程序入口, 这里建立套接字, 然后与sockaddr_in结构体进行绑定, 然后用listen监听该套接字上的连接请求, 这几步都在startup()中实现.
然后服务器在通过accept接受客户端请求, 如没有请求accept()会阻塞, 如果有请求就会创建一个新线程去处理客户端请求.
int main(void) { /* 定义socket相关信息 */ int server_sock = -1; u_short port = 4000; int client_sock = -1; struct sockaddr_in client_name; socklen_t client_name_len = sizeof(client_name); pthread_t newthread; server_sock = startup(&port); printf("httpd running on port %d ", port); while (1) { /* 通过accept接受客户端请求, 阻塞方式 */ client_sock = accept(server_sock, (struct sockaddr *)&client_name, &client_name_len); if (client_sock == -1) error_die("accept"); /* accept_request(&client_sock); */ /* 开启线程处理客户端请求 */ if (pthread_create(&newthread , NULL, accept_request, (void *)&client_sock) != 0) perror("pthread_create"); } close(server_sock); return(0); }
accept_request()主要处理客户端请求, 做出了基本的错误处理. 主要功能判断是静态请求还是动态请求, 静态请求直接读取文件发送给客户端即可, 动态请求则调用execute_cgi()处理.

/**********************************************************************/ /* A request has caused a call to accept() on the server port to * return. Process the request appropriately. * Parameters: the socket connected to the client * 处理每个客户端连接 * */ /**********************************************************************/ void *accept_request(void *arg) { int client = *(int*)arg; char buf[1024]; size_t numchars; char method[255]; char url[255]; char path[512]; size_t i, j; struct stat st; int cgi = 0; /* becomes true if server decides this is a CGI * program */ char *query_string = NULL; /* 获取请求行, 返回字节数 eg: GET /index.html HTTP/1.1 */ numchars = get_line(client, buf, sizeof(buf)); /* debug */ //printf("%s", buf); /* 获取请求方式, 保存在method中 GET或POST */ i = 0; j = 0; while (!ISspace(buf[i]) && (i < sizeof(method) - 1)) { method[i] = buf[i]; i++; } j=i; method[i] = '