0x00 前言

想看这个很久了,这两天把堆刷完了,正好有时间看一下

https://github.com/EZLippi/Tinyhttpd

0x01 网络编程

  1. 网络编程就是程序让两台联网的计算机相互交换数据,我们可以使用socket来进行数据的接收与发送。
  2. linux和unix中的一切都是文件,为了表示和区分已经打开的文件,unix和linux会给每个文件分配一个id,这个id被称为文件描述符。我们通常用0表示标准输入文件(stdin),对应的硬件设备就是键盘;用1表示标准输出文件(stdout),对应的硬件设备就是显示器。
  3. 网络连接也是一个文件,也有文件描述符,我们可以通过socket函数来创建一个网络连接,例如打开一个网站,socket的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件描述符来传输数据了,比如使用read读取数据,用write写入数据。
1
int socket(int af, int type, int protocol);
  • af(Address Family),也就是ip地址类型。常用的有AF_INET(PF_INET)和AF_INET6(PF_INET6),分别表示IPv4和IPv6。

  • type表示数据传输方式,常用的有SOCK_STREAM和SOCK_DGRAM。

    SOCK_STREAM:使用TCP协议,只要不断网,就能保证数据不丢失,如果中途数据损坏或者丢失会重新发送,数据是按照顺序传输的,数据的发送和接收是不同步的(不管数据发送多少,接收端只会按照自己的需求读取数据)。

    SOCK_DGRAM:使用UDP协议,传输速度快,但是可能会产生数据丢失,如果数据丢失了,无法重传。无顺序传输,且发送和接收同步。qq视频聊天和语音聊天就是使用了该类型,因为要保证通信的流程,即使丢失一小部分数据,也可以正常解析。

  • protocol表示传输协议,常用的有IPPROTO_TCP和IPPTOTO_UDP,表示TCP和UDP传输协议。

return value:非负数成功,-1出错

管道的概念:

管道是一种最基本的IPC机制(进程间通信),作用于有血缘关系的进程之间,进行数据传递。管道会把两个进程的标准输入和标准输出连接起来,从而提供一种让多个进程间通信的方法。

pipe系统函数可以创建一个管道。

0x02 http

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议),基于TCP/IP通信协议来传递数据。

  1. HTTP是无连接的:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户端的请求,收到客户端的应答之后会断开连接。
  2. HTTP是媒体独立的:只要客户端和服务器知道如何处理数据内容,任何类型数据都可以通过http发送。
  3. HTTP是无状态的:无状态是指协议对于事务处理没有记忆能力,所以如果后续需要处理前面的信息,就必须重新传输。

客户端请求消息

1
2
3
4
5
6
method url version <CRLF>
header-name: header-value <CRLF>
header-name: header-value <CRLF>
...
<CRLF>
请求数据
1
2
3
4
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

服务器响应消息

1
2
3
4
5
6
status line <CRLF>
header-name: header-value <CRLF>
header-name: header-value <CRLF>
...
<CRLF>
响应数据
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

请求方法

  • GET:请求指定的页面信息,并返回具体内容
  • HEAD:类似GET请求,只不过不会返回具体内容,只会返回报头
  • POST:向指定资源提交数据,数据被包含在请求体中。POST请求可能会导致新的资源建立或者修改已有资源
  • PUT:从客户端向服务器传送的数据取代制定文档的内容
  • DELETE:请求服务器删除页面
  • CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
  • OPTIONS:允许客户端查看服务器的性能
  • TRACE:回显服务器收到的请求,用于测试或者诊断
  • PATCH:对PUT方法对补充,用来对已知资源进行局部更新

状态码

状态码 状态码英文名称 中文描述
100 Continue 继续。客户端应继续其请求
101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
200 OK 请求成功。一般用于GET与POST请求
201 Created 已创建。成功请求并创建了新的资源
202 Accepted 已接受。已经接受请求,但未处理完成
203 Non-Authoritative Information 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本
204 No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
205 Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
206 Partial Content 部分内容。服务器成功处理了部分GET请求
300 Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301 Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302 Found 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303 See Other 查看其它地址。与301类似。使用GET和POST请求查看
304 Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305 Use Proxy 使用代理。所请求的资源必须通过代理访问
306 Unused 已经被废弃的HTTP状态码
307 Temporary Redirect 临时重定向。与302类似。使用GET请求重定向
400 Bad Request 客户端请求的语法错误,服务器无法理解
401 Unauthorized 请求要求用户的身份认证
402 Payment Required 保留,将来使用
403 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求
404 Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面
405 Method Not Allowed 客户端请求中的方法被禁止
406 Not Acceptable 服务器无法根据客户端请求的内容特性完成请求
407 Proxy Authentication Required 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408 Request Time-out 服务器等待客户端发送的请求时间过长,超时
409 Conflict 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突
410 Gone 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
411 Length Required 服务器无法处理客户端发送的不带Content-Length的请求信息
412 Precondition Failed 客户端请求信息的先决条件错误
413 Request Entity Too Large 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息
414 Request-URI Too Large 请求的URI过长(URI通常为网址),服务器无法处理
415 Unsupported Media Type 服务器无法处理请求附带的媒体格式
416 Requested range not satisfiable 客户端请求的范围无效
417 Expectation Failed 服务器无法满足Expect的请求头信息
500 Internal Server Error 服务器内部错误,无法完成请求
501 Not Implemented 服务器不支持请求的功能,无法完成请求
502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
503 Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
504 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求
505 HTTP Version not supported 服务器不支持请求的HTTP协议的版本,无法完成处理
应答头 说明
Allow 服务器支持哪些请求方法(如GET、POST等)。
Content-Encoding 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader(“Accept-Encoding”))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。
Content-Length 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStream,完成后查看其大小,然后把该值放入Content-Length头,最后通过byteArrayStream.writeTo(response.getOutputStream()发送内容。
Content-Type 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。
Date 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。
Expires 应该在什么时候认为文档已经过期,从而不再缓存它?
Last-Modified 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。
Location 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。
Refresh 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader(“Refresh”, “5; URL=http://host/path")让浏览器读取指定的页面。 注意这种功能通常是通过设置HTML页面HEAD区的<META HTTP-EQUIV=”Refresh” CONTENT=”5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。 注意Refresh的意义是”N秒之后刷新本页面或访问指定页面”,而不是”每隔N秒刷新本页面或访问指定页面”。因此,连续刷新要求每次都发送一个Refresh头,而发送204状态代码则可以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV=”Refresh” …>。 注意Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它。
Server 服务器名字。Servlet一般不设置这个值,而是由Web服务器自己设置。
Set-Cookie 设置和页面关联的Cookie。Servlet不应使用response.setHeader(“Set-Cookie”, …),而是应使用HttpServletResponse提供的专用方法addCookie。参见下文有关Cookie设置的讨论。
WWW-Authenticate 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的应答中这个头是必需的。例如,response.setHeader(“WWW-Authenticate”, “BASIC realm=\”executives\””)。 注意Servlet一般不进行这方面的处理,而是让Web服务器的专门机制来控制受密码保护页面的访问(例如.htaccess)。

0x03 main

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
int main(void)
{
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); // 初始化httpd服务
printf("httpd running on port %d\n", port);

while (1)
{
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, (void *)accept_request, (void *)(intptr_t)client_sock) != 0) // 创建一个新线程处理客户端请求
perror("pthread_create");
}

close(server_sock); // 关闭httpd服务

return(0);
}
1
2
3
4
5
6
7
8
9
10
struct sockaddr_in {
__uint8_t sin_len; // 为了兼容性,现在大部分版本不用了
sa_family_t sin_family; // 地址类型
in_port_t sin_port; // 16位的端口号
struct in_addr sin_addr; // 32位的ip地址
char sin_zero[8]; // 不使用,一般为0
};
struct in_addr {
in_addr_t s_addr; // 由于历史原因,导致是个结构
};
1
2
3
4
5
6
int accept(int s, struct sockaddr * addr, socklen_t * addrlen);
// s 表示socket标识符
// addr 用于存放客户端的地址
// addrlen addr指向区域的长度
// 如果客户端有连接请求,用该函数接受客户端的请求
// 返回一个接收到的socket的描述符
1
2
3
4
5
6
7
pthread_create(
pthread_t *restrict tidp, // 指向线程标识符的指针
const pthread_attr_t *restrict attr, // 线程属性,默认为NULL
void *(*start_rtn)(void *), // 线程运行函数的起始地址
void *restrict arg // 默认为NULL。若上述函数需要参数,把参数放入结构将地址作为arg传入
);
// 该函数用于创建线程

0x04 startup

初始化httpd服务,包括建立socket,绑定端口进行监听等。(服务端)

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
int startup(u_short *port)
{
int httpd = 0;
int on = 1;
struct sockaddr_in name;

httpd = socket(PF_INET, SOCK_STREAM, 0); // 使用tcp,创建socket
if (httpd == -1)
error_die("socket");
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
// htonl将主机的无符号长整形数转换成网络字节顺序。

if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
{
error_die("setsockopt failed");
}
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
if (*port == 0) /* if dynamically allocating a port */
{
socklen_t namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
if (listen(httpd, 5) < 0) // 监听socket,成功返回0,错误返回-1
error_die("listen");
return(httpd);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int setsockopt(int s, int level, int optname, const void* optval, sockien_t optlen);
// s 表示socket标识符
// level 表示网络层,一般设置为SOL_SOCKET以访问socket或者SOL_TCP来访问TCP
// optname 表示要校验的选项,有下面一些
/*
SO_DEBUG 打开或关闭排错模式
SO_REUSEADDR 允许在bind ()过程中本地地址可重复使用
SO_TYPE 返回socket 形态.
SO_ERROR 返回socket 已发生的错误原因
SO_DONTROUTE 送出的数据包不要利用路由设备来传输.
SO_BROADCAST 使用广播方式传送
SO_SNDBUF 设置送出的暂存区大小
SO_RCVBUF 设置接收的暂存区大小
SO_KEEPALIVE 定期确定连线是否已终止.
SO_OOBINLINE 当接收到OOB 数据时会马上送至标准输入设备
SO_LINGER 确保数据安全且可靠的传送出去.
*/
// optval 表示接收选项值的指针
// optlen 表示optval的长度
// 该函数用于设置socket的选项
1
2
3
4
5
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
// sock 表示socket标识符
// addr 表示sockaddr结构体变量的指针
// addrlen为addr变量的大小
// 该函数用于把socket和特定的ip和端口绑定起来
1
2
3
4
5
int getsockname(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
// sockfd 表示socket标识符
// addr 存放socket名称的buffer
// addrlen addr指向空间的大小
// 该函数可以获得一个socket的地址

0x05 accept_request

处理从socket上监听到的一个 HTTP 请求(当服务器listen的端口accpet的时候,新建一个线程用该函数进行处理)

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
void accept_request(void *arg)
{
int client = (intptr_t)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;

numchars = get_line(client, buf, sizeof(buf)); // 读取一行socket,这里读取的是请求方法
i = 0; j = 0;
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
// ISspace检查读取的buf是不是空,是的话返回0
{
method[i] = buf[i]; // 获取请求方法
i++;
}
j=i;
method[i] = '\0';

if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
// 判断是GET请求还是POST请求
{
unimplemented(client); // 返回给浏览器表示收到的http的method不被支持,报错501
return;
}

if (strcasecmp(method, "POST") == 0)
cgi = 1;

i = 0;
while (ISspace(buf[j]) && (j < numchars))
j++;
// 跳过空格
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
// 获取url
{
url[i] = buf[j];
i++; j++;
}
url[i] = '\0';

if (strcasecmp(method, "GET") == 0) // 如果是GET请求的话
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?')
{
cgi = 1;
*query_string = '\0';
query_string++;
}
}

sprintf(path, "htdocs%s", url);
if (path[strlen(path) - 1] == '/') // 如果url的路径是/,自动加上index.html
strcat(path, "index.html");
if (stat(path, &st) == -1) {
// stat获取文件信息到st,成功返回0,失败返回-1
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
not_found(client); // 404
}
else
{
if ((st.st_mode & S_IFMT) == S_IFDIR) // 判断st是不是一个目录
strcat(path, "/index.html"); // 是目录的话添加index.html
if ((st.st_mode & S_IXUSR) || // 判断st是不是一个文件
(st.st_mode & S_IXGRP) || // 判断用户组是否可执行
(st.st_mode & S_IXOTH) ) // 判断其他用户是否可执行
cgi = 1;
if (!cgi) // 如果是cgi就读取后执行
serve_file(client, path);
else
execute_cgi(client, path, method, query_string);
}

close(client);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int stat(const char *path, struct stat *buf);
struct stat
{
dev_t st_dev; /* ID of device containing file */文件使用的设备号
ino_t st_ino; /* inode number */ 索引节点号
mode_t st_mode; /* protection */ 文件对应的模式,文件,目录等
nlink_t st_nlink; /* number of hard links */ 文件的硬连接数
uid_t st_uid; /* user ID of owner */ 所有者用户识别号
gid_t st_gid; /* group ID of owner */ 组识别号
dev_t st_rdev; /* device ID (if special file) */ 设备文件的设备号
off_t st_size; /* total size, in bytes */ 以字节为单位的文件容量
blksize_t st_blksize; /* blocksize for file system I/O */ 包含该文件的磁盘块的大小
blkcnt_t st_blocks; /* number of 512B blocks allocated */ 该文件所占的磁盘块
time_t st_atime; /* time of last access */ 最后一次访问该文件的时间
time_t st_mtime; /* time of last modification */ /最后一次修改该文件的时间
time_t st_ctime; /* time of last status change */ 最后一次改变该文件状态的时间
};

0x06 serve_file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void serve_file(int client, const char *filename)
{
FILE *resource = NULL;
int numchars = 1;
char buf[1024];

buf[0] = 'A'; buf[1] = '\0';
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));

resource = fopen(filename, "r");
if (resource == NULL) // 打开文件,没找到就404
not_found(client);
else
{
headers(client, filename); // 打开成功,响应码200
cat(client, resource); // 读取socket写入文件
}
fclose(resource);
}

0x07 execute_cgi

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
void execute_cgi(int client, const char *path,
const char *method, const char *query_string)
{
char buf[1024];
int cgi_output[2];
int cgi_input[2];
pid_t pid;
int status;
int i;
char c;
int numchars = 1;
int content_length = -1;

buf[0] = 'A'; buf[1] = '\0';
if (strcasecmp(method, "GET") == 0)
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf)); // 读取url,后面的信息没必要分析
else if (strcasecmp(method, "POST") == 0) /*POST*/
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
buf[15] = '\0';
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16])); // 读取content_length
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) {
bad_request(client);
return;
}
}
else/*HEAD or other*/
{
}


if (pipe(cgi_output) < 0) { // 创建管道
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) { // 创建管道
cannot_execute(client);
return;
}

if ( (pid = fork()) < 0 ) { // 创建子进程
cannot_execute(client);
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
if (pid == 0) /* child: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];

dup2(cgi_output[1], STDOUT); // 子进程输出重定向
dup2(cgi_input[0], STDIN); // 子进程输入重定向
close(cgi_output[0]); // 重定向完了就可以关闭了
close(cgi_input[1]);
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env); // 修改环境变量
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
execl(path, NULL);
exit(0);
} else { /* parent */
close(cgi_output[1]);
close(cgi_input[0]);
if (strcasecmp(method, "POST") == 0)
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);
write(cgi_input[1], &c, 1);
}
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);

close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);
}
}

0x08 工作流程

  1. 服务器启动,在指定端口或随机选取端口绑定 httpd 服务。
  2. 收到一个 HTTP 请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。
  3. 取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。
  4. 格式化 url 到 path 数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页。
  5. 如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到10。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。
  6. 读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200 状态码写到套接字。
  7. 建立两个管道,cgi_input 和 cgi_output, 并 fork 一个进程。
  8. 在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序。
  9. 在父进程中,关闭 cgi_input 的读取端 和 cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。这一部分比较乱,见下图说明:

  1. 关闭与浏览器的连接,完成了一次 HTTP 请求与回应,因为 HTTP 是无连接的。