tinyhttpd 是一个简易的 HTTP 服务器,支持 CGI。代码量少,非常容易阅读,十分适合网络编程初学者学习的项目。麻雀虽小,五脏俱全。在 tinyhttpd 中可以学到 linux 上进程的创建、管道的使用。Linux 下 socket 编程基本方法和 HTTP 协议的最基本结构。

函数描述

1
2
3
4
5
6
7
8
9
10
11
12
void error_die(const char *);       // 异常信息写到 perror 并退出
int get_line(int, char *, int); // 读取 request headers 的一行,把回车换行等情况都统一为换行符结束
void cat(int, FILE *); // 读取 server 上的一个文件,写到 client socket
void serve_file(int, const char *); // 调用响应头并调用 cat 函数,将 server 上的文件发给 client
void headers(int, const char *); // 构造 200 OK 的响应头部,写到套接字
void bad_request(int); // 返回给客户端一个 400 BAD REQUEST 响应报文
void not_found(int); // 返回给客户端一个 404 NOT FOUND 响应报文
void cannot_execute(int); // 返回给客户端一个 500 Internal Server Error 响应报文,主要处理发生在执行 CGI 程序时出现的错误
void unimplemented(int); // 返回给客户端一个 501 Method Not Implemented 响应报文,表明 httpd 服务器不支持该方法
int startup(u_short *); // 建立 HTTP 服务,包括创建套接字、绑定端口、监听客户端等
void *accept_request(void *); // 处理 server 监听到的 client socket 的一个 HTTP 请求,在这里体现了服务器处理请求的流程
void execute_cgi(int, const char *, const char *, const char *); // 执行 CGI 脚本程序,核心执行函数

不熟悉 HTTP 协议的可以看 这篇博客

建议源码阅读顺序

main -> startup -> accept_request -> execute_cgi,通晓主要工作流程后,再仔细把每个函数的源码看一看。

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./
├── client # makefile 编译产生
├── htdocs
│   ├── check.cgi # 744 <- Linux 文件权限
│   ├── color.c # 644
│   ├── color.cgi # 744
│   ├── color_cgi # 755, makefile 编译产生
│   ├── index.html # 600
│   └── README
├── httpd # makefile 编译产生
├── httpd.c
├── LICENSE
├── Makefile
├── README.md
└── simpleclient.c

整体执行流程

整体执行流程图如下:

httpd 执行流程图

工作流程

  1. 【setup 函数】服务器启动,在指定端口或随机选取端口绑定 httpd 服务。

  2. 【main 函数死循环】收到一个 HTTP 请求报文时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。

  3. 【accept_request 函数】取出 HTTP 请求报文的请求行中的 method (GET 或 POST) 和 url。

    • 对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。
    • 格式化 url 到 path 数组,path 表示浏览器请求的服务器文件路径(在 tinyhttpd 中服务器文件位于 htdocs 文件夹下)。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页。
    • 如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到流程 10。其他情况(带参数 GET、POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。
  4. 【excute_cgi 函数】读取整个 HTTP 请求头并丢弃,如果是 POST 则找出 Content-Length 的值并放到环境变量中(后续 POST 请求使用)。然后,把 HTTP 200 状态码写到套接字。建立两个管道,cgi_input 和 cgi_output,并 fork 一个子进程。

    • 在子进程中,把 STDOUT 重定向到 cgi_output 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端。然后,设置 request_method 环境变量,GET 的话设置 query_string 环境变量,POST 的话设置 content_length 环境变量,这些环境变量都是为了给 cgi 脚本调用,接着调用 execl 运行 cgi 程序。
    • 在父进程中,关闭 cgi_input 的读取端和 cgi_output 的写入端。如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 管道的输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。这一部分比较乱,见“httpd 执行流程图”的管道流向状态图。
  5. 关闭与浏览器的连接,完成了一次 HTTP 请求与响应,因为 HTTP 是无连接的。

尽管 HTTP 在 TCP 之上运行,但 HTTP 被称为“无连接”是因为它每次请求 - 响应周期都是短暂的,没有保持持久连接状态。这意味着每个请求都需要在客户端和服务器之间建立一个新的 TCP 连接,并在请求完成后立即关闭连接。

项目编译

Makefile 文件:

1
2
3
4
5
6
7
8
9
10
11
12
all: httpd client ./htdocs/color_cgi
LIBS = -lpthread #-lsocket
httpd: httpd.c
gcc -g -W -Wall $(LIBS) -o $@ $<

client: simpleclient.c
gcc -W -Wall -o $@ $<

./htdocs/color_cgi: ./htdocs/color.c
gcc -W -Wall -o $@ $<
clean:
rm httpd client ./htdocs/color_cgi

验证 httpd 服务器

GET 与 POST 请求

CGI 程序

这里使用 C 语言写一个 CGI 简单程序:通过获取服务器 execute_cgi 函数设置的 CONTENT_LENGTH 环境变量,来拿到请求正文的长度、解析出 color 表单数据,并使用 color 值生成相应的简单页面。

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
// gcc -o color_cgi color.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LENGTH (1024)

// 函数用于从标准输入读取 POST 数据
char *get_post_data()
{
char *post_data = NULL;
char *content_length_str = getenv("CONTENT_LENGTH"); // 服务器 execute_cgi 函数设置的

if (content_length_str != NULL)
{
int content_length = atoi(content_length_str);
if (content_length > 0)
{
post_data = (char *)malloc((content_length + 1) * sizeof(char));
fgets(post_data, content_length + 1, stdin); // 从标准输入读取 n 个字节到数组
}
}

return post_data;
}

int main()
{
// 获取 POST 数据
char *post_data = get_post_data();

char color[MAX_LENGTH] = "yellow"; // 默认颜色为黄色

if (post_data != NULL)
{
// 包含 HTTP POST 请求数据的字符串,格式类似于 key1=value1&key2=value2&key3=value3
char *token = strtok(post_data, "&"); // 返回第一个子字符串(键值对)的指针
while (token != NULL)
{
char key[MAX_LENGTH], value[MAX_LENGTH];
sscanf(token, "%[^=]=%s", key, value);

if (strcmp(key, "color") == 0)
{
strncpy(color, value, MAX_LENGTH - 1);
color[MAX_LENGTH - 1] = '\0'; // 确保字符串结尾
}

token = strtok(NULL, "&"); // 获取下一个子字符串(键值对)的指针
}
}

// 输出 HTML
printf("Content-type: text/html\r\n\r\n");
printf("<!DOCTYPE html>\n");
printf("<html>\n");
printf("<head>\n");
printf(" <title>%s</title>\n", color);
printf(" <style>\n");
printf(" body {\n");
printf(" background-color: %s;\n", color);
printf(" }\n");
printf(" </style>\n");
printf("</head>\n");
printf("<body>\n");
printf(" <h1>This is %s</h1>\n", color);
printf("</body>\n");
printf("</html>\n");

// 释放内存
if (post_data != NULL)
{
free(post_data);
}

return 0;
}

GET 请求

HTTP 请求报文的一般格式

1
2
3
4
<request-line>
<request-headers>
<blank line>
<request-body>

我们通过 Chrome 进行以下请求:

Rqeuest Headers:

1
2
3
4
5
6
7
8
GET /index.html HTTP/1.1  # 请求行
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: 127.0.0.1:4000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36

上述请求的响应报文:

Response Headers:

1
2
3
HTTP/1.0 200 OK
Server: jdbhttpd/0.1.0
Content-Type: text/html

Response Body:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<p>Welcome to J. David's webserver.</p>
<h1>CGI demo</h1>
<form action="color_cgi" method="post">
Enter a color: <input type="text" name="color">
<input type="submit">
</form>
</body>
</html>

POST 请求

HTTP 响应报文的一般格式

1
2
3
4
<status-line>
<response-headers>
<blank line>
<response-body>

我们通过 Chrome 进行以下请求:

Rqeuest Headers: 通过页面输入的 Form Data 为 color=gray

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /color_cgi HTTP/1.1  # 请求行
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 10 # 请求正文长度,在这里即 `color=gray`
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:4000
Origin: http://127.0.0.1:4000
Referer: http://127.0.0.1:4000/
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36

上述请求的响应报文:

Response Headers:

1
2
HTTP/1.0 200 OK  # 状态行
Content-type: text/html

Response Body:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<title>gray</title>
<style>
body {
background-color: gray;
}
</style>
</head>
<body>
<h1>This is gray</h1>
</body>
</html>

完整源码

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>

#define ISspace(x) isspace((int)(x)) // 检查字符是否为空白字符(如空格、换行符等)

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"

#define MAX_CONNECTS (5) // 监听(连接)的最大客户端数量

void error_die(const char *); // 异常信息写到 perror 并退出
int get_line(int, char *, int); // 读取 request headers 的一行,把回车换行等情况都统一为换行符结束
void cat(int, FILE *); // 读取 server 上的一个文件,写到 client socket
void serve_file(int, const char *); // 调用响应头并调用 cat 函数,将 server 上的文件发给 client
void headers(int, const char *); // 构造 200 OK 的响应头部,写到套接字
void bad_request(int); // 返回给客户端一个 400 BAD REQUEST 响应报文
void not_found(int); // 返回给客户端一个 404 NOT FOUND 响应报文
void cannot_execute(int); // 返回给客户端一个 500 Internal Server Error 响应报文,主要处理发生在执行 CGI 程序时出现的错误
void unimplemented(int); // 返回给客户端一个 501 Method Not Implemented 响应报文,表明 httpd 服务器不支持该方法
int startup(u_short *); // 建立 HTTP 服务,包括创建套接字、绑定端口、监听客户端等
void *accept_request(void *); // 处理 server 监听到的 client socket 的一个 HTTP 请求,在这里体现了服务器处理请求的流程

void execute_cgi(int, const char *, const char *, const char *); // 执行 CGI 脚本程序,核心执行函数

#define SEND_BUF_MSG(sock_fd, buf, flags, msg, ...) \
do \
{ \
sprintf(buf, msg, ##__VA_ARGS__); \
send(sock_fd, buf, strlen(buf), flags); \
} while (0)

#define ERROR_REPORT_WITH_EXIT(code, msg) \
do \
{ \
if ((code) < 0) \
{ \
error_die(msg); \
} \
} while (0)

#define CHECK_POINTER_NON_NULL(ptr) \
do \
{ \
if (NULL == (ptr)) \
{ \
error_die("null pointer"); \
} \
} while (0)

/**********************************************************************/
/* 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 c_sock_fd = (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;

/*
* 读取 HTTP 请求报文的第一行数据,假如访问 https://www.google.com/search?q=socket
* 那么,HTTP 请求报文的第一行数据是:GET /search?q=socket HTTP/1.1
* HTTP 请求报文组成:
* 请求方法 请求 URL HTTP 协议及版本
* 请求报文头
* \r\n
* 请求报文体
*/
numchars = get_line(c_sock_fd, buf, sizeof(buf));

/* 将请求方法存到 method 缓冲区 */
i = 0, j = 0;
while (!ISspace(buf[i]) && (i < sizeof(method) - 1)) // 遍历到空白字符或缓存满为止
{
method[i] = buf[i];
i++;
}
j = i; // 指向第一行数据的首个空白字符或缓存满的位置
method[i] = '\0';

/* 若请求的方法既不是 GET 或 POST,则发送 HTTP 响应报文,通知客户端请求的 Web 方法尚未实现 */
if ((0 != strcasecmp(method, "GET")) && (0 != strcasecmp(method, "POST")))
{
unimplemented(c_sock_fd);
return NULL;
}

i = 0;
while (ISspace(buf[j]) && (j < numchars)) // 跳过请求方法后面的所有空白字符(空格)
{
j++;
}

/* 将 HTTP 请求报文的请求 URL 存到 url 缓冲区 */
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
url[i++] = buf[j++];
}
url[i] = '\0';

/* 若请求的方法是 POST,则服务器认为是 CGI 程序 */
if (0 == strcasecmp(method, "POST"))
{
cgi = 1; // 使能 CGI 标志
}
/*
* 若请求的方法是 GET,且在 url 中检查到 ? 字符,
* 则服务器认为是 CGI 程序,并从 ? 位置分隔 url 缓存区为两份字符串
*/
else if (0 == strcasecmp(method, "GET"))
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0')) // 遍历到 ? 字符或 \0 结束符为止
{
query_string++;
}
if (*query_string == '?') // 检查是 ? 字符还是 url 的 \0 导致的跳出循环
{
cgi = 1; // 使能 CGI 标志
*query_string = '\0'; // 从 ? 位置分隔 url 缓存区为两份字符串
query_string++; // 指向后一份字符串的起始位置(? 字符的下一个字符位置)
}
}

// 将 url 字符串的第一份(可能未分隔,只有一份)拼接在 htdocs 后面,并存储在 path 缓冲区
sprintf(path, "htdocs%s", url);

// 如果 path 缓冲区以 / 结尾,则拼接 index.html,即 Web 首页的意思
if (path[strlen(path) - 1] == '/')
{
strcat(path, "index.html");
}

/* 在服务器端中查找 path 缓冲区中的文件的状态信息,并将状态信息存储到 struct stat 指向的变量 */
if (-1 == stat(path, &st))
{
/*
* 如果文件不存在,则按行读取 HTTP 请求的 headers,但都忽略掉;
* 并向客户端发送 404 NOT FOUND's response
*/
while ((numchars > 0) && (0 != strcmp("\n", buf))) /* read & discard headers */
{
numchars = get_line(c_sock_fd, buf, sizeof(buf));
}
not_found(c_sock_fd);
}
else
{
/* 若文件类型为目录,则对路径拼接 index.html 首页 Web 文件 */
if (S_ISDIR(st.st_mode))
{
strcat(path, "/index.html");
}
/* 判断文件是否具有执行权限(任意用户),若是则使能 CGI 标志(才有权限执行 CGI 程序)*/
if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
{
cgi = 1;
}
/* 使能或未使能 CGI 对应不同动作 */
if (cgi)
{
execute_cgi(c_sock_fd, path, method, query_string);
}
else
{
serve_file(c_sock_fd, path); // 将服务器上的文件发给客户端
}
}

close(c_sock_fd);

return NULL;
}

/**********************************************************************/
/* Inform the client that a request it has made has a problem.
* Parameters: client socket */
/**********************************************************************/
void bad_request(int c_sock_fd)
{
char buf[1024];
SEND_BUF_MSG(c_sock_fd, buf, 0, "HTTP/1.0 400 BAD REQUEST\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "Content-type: text/html\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "<P>Your browser sent a bad request, ");
SEND_BUF_MSG(c_sock_fd, buf, 0, "such as a POST without a Content-Length.\r\n");
}

/**********************************************************************/
/* Put the entire contents of a file out on a socket. This function
* is named after the UNIX "cat" command, because it might have been
* easier just to do something like pipe, fork, and exec("cat").
* Parameters: the client socket descriptor
* FILE pointer for the file to cat */
/**********************************************************************/
void cat(int c_sock_fd, FILE *resource)
{
char buf[1024];

/* 不断地从文件流中读取一行数据,然后发给客户端,直到 End of File 为止 */
fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(c_sock_fd, buf, strlen(buf), 0);
fgets(buf, sizeof(buf), resource);
}
}

/**********************************************************************/
/* Inform the client that a CGI script could not be executed.
* Parameter: the client socket descriptor. */
/**********************************************************************/
void cannot_execute(int c_sock_fd)
{
char buf[1024];
SEND_BUF_MSG(c_sock_fd, buf, 0, "HTTP/1.0 500 Internal Server Error\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "Content-type: text/html\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "<P>Error prohibited CGI execution.\r\n");
}

/**********************************************************************/
/* Print out an error message with perror() (for system errors; based
* on value of errno, which indicates system call errors) and exit the
* program indicating an error. */
/**********************************************************************/
inline void error_die(const char *sc)
{
perror(sc);
exit(EXIT_FAILURE);
}

/**********************************************************************/
/* Execute a CGI script. Will need to set environment variables as
* appropriate.
* Parameters: client socket descriptor
* path to the CGI script */
/**********************************************************************/
void execute_cgi(int c_sock_fd, const char *path,
const char *method, const char *query_string)
{
char buf[1024] = {'A', '\0'};
int cgi_output[2]; // 管道的读端与写端文件描述符
int cgi_input[2];
pid_t cpid;
int w_status;
int iter;
char c;
int numchars = 1; // 初始时 buf 的长度
int content_length = -1;

/*
* NOTE: 因为这个函数被 accpect 函数调用(内部处理了客户端请求报文的请求行),
* 故这里是从请求报头(第二行数据)开始接收处理的,但从请求行开始处理也没问题
*/

/* 按行读取 HTTP 请求 headers,但都忽略掉 */
if (0 == strcasecmp(method, "GET"))
{

while ((numchars > 0) && (0 != strcmp("\n", buf))) /* read & discard headers */
{
numchars = get_line(c_sock_fd, buf, sizeof(buf));
}
}
/* 按行读取 HTTP 请求的 headers 并截断,获取 POST 方法中的 `Content-Length:` 的值 */
else if (0 == strcasecmp(method, "POST"))
{
while ((numchars > 0) && (0 != strcmp("\n", buf)))
{
buf[15] = '\0';
if (0 == strcasecmp(buf, "Content-Length:"))
{
/* 请求格式:Content-Length: 28,从子串中获取 Body 内容长度 */
content_length = atoi(&(buf[16]));
/* break; */ // 把 headers 接收完,所有不提前退出
}
numchars = get_line(c_sock_fd, buf, sizeof(buf));
}
if (-1 == content_length)
{
bad_request(c_sock_fd);
return;
}
}
else /* HEAD or other */
{
}

/* 创建 CGI 脚本的输入、输出管道 */
if (pipe(cgi_output) < 0)
{
cannot_execute(c_sock_fd);
return;
}
if (pipe(cgi_input) < 0)
{
cannot_execute(c_sock_fd);
return;
}

cpid = fork();
if (cpid < 0)
{
cannot_execute(c_sock_fd);
return;
}

SEND_BUF_MSG(c_sock_fd, buf, 0, "HTTP/1.0 200 OK\r\n");

if (0 == cpid) /* child process: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];

/* 将标准输出(输入)重定向到 CGI 输出(输入)管道的写端(读端),并关闭不使用的一端 */
dup2(cgi_output[1], STDOUT_FILENO);
dup2(cgi_input[0], STDIN_FILENO);
close(cgi_output[0]);
close(cgi_input[1]);

sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (0 == strcasecmp(method, "GET"))
{
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else
{ /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}

close(cgi_output[1]); // 调用 execl 前关闭 fd
close(cgi_input[0]);

execl(path, "", NULL); // `""` 消除 warning
_exit(EXIT_FAILURE);
}
else
{ /* parent process */
close(cgi_output[1]);
close(cgi_input[0]);
if (0 == strcasecmp(method, "POST"))
{
for (iter = 0; iter < content_length; iter++)
{
recv(c_sock_fd, &c, 1, 0);
write(cgi_input[1], &c, 1);
}
}
while (read(cgi_output[0], &c, 1) > 0)
{
send(c_sock_fd, &c, 1, 0); // With a zero flags argument, send() is equivalent to write()
}

close(cgi_output[0]);
close(cgi_input[1]);
waitpid(cpid, &w_status, 0);
}
}

/**********************************************************************/
/* Get a line from a socket, whether the line ends in a newline,
* carriage return, or a CRLF combination. Terminates the string read
* with a null character. If no newline indicator is found before the
* end of the buffer, the string is terminated with a null. If any of
* the above three line terminators is read, the last character of the
* string will be a linefeed and the string will be terminated with a
* null character.
* Parameters: the socket descriptor
* the buffer to save the data in
* the size of the buffer
* Returns: the number of bytes stored (excluding null) */
/**********************************************************************/
int get_line(int sock_fd, char *buf, int size)
{
int i = 0;
char c = '\0';
int recv_bytes;

while ((i < size - 1) && (c != '\n'))
{
recv_bytes = recv(sock_fd, &c, 1, 0);
if (recv_bytes > 0)
{
if (c == '\r')
{
recv_bytes = recv(sock_fd, &c, 1, MSG_PEEK); // 预读一个字节,不从管道弹出
if ((recv_bytes > 0) && (c == '\n'))
{
recv(sock_fd, &c, 1, 0);
}
else
{
c = '\n';
}
}
buf[i] = c;
i++;
}
else
{
c = '\n';
}
}
buf[i] = '\0';

return i;
}

/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
* the name of the file */
/**********************************************************************/
void headers(int c_sock_fd, const char *filename)
{
char buf[1024];
(void)filename; /* could use filename to determine file type */

SEND_BUF_MSG(c_sock_fd, buf, 0, "HTTP/1.0 200 OK\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, SERVER_STRING);
SEND_BUF_MSG(c_sock_fd, buf, 0, "Content-Type: text/html\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "\r\n");
}

/**********************************************************************/
/* Give a client a 404 not found status message. */
/**********************************************************************/
void not_found(int c_sock_fd)
{
char buf[1024];
SEND_BUF_MSG(c_sock_fd, buf, 0, "HTTP/1.0 404 NOT FOUND\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, SERVER_STRING);
SEND_BUF_MSG(c_sock_fd, buf, 0, "Content-Type: text/html\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "<HTML><TITLE>Not Found</TITLE>\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "<BODY><P>The server could not fulfill\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "your request because the resource specified\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "is unavailable or nonexistent.\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "</BODY></HTML>\r\n");
}

/**********************************************************************/
/* Send a regular file to the client. Use headers, and report
* errors to client if they occur.
* Parameters: a pointer to a file structure produced from the socket
* file descriptor
* the name of the file to serve */
/**********************************************************************/
void serve_file(int c_sock_fd, const char *filename)
{
FILE *resource = NULL;
char buf[1024] = {'A', '\0'};
int numchars = 1; // 初始时 buf 的长度

/* 按行读取 HTTP 请求的 headers,但都忽略掉 */
while ((numchars > 0) && (0 != strcmp("\n", buf))) /* read & discard headers */
{
numchars = get_line(c_sock_fd, buf, sizeof(buf));
}

/*
* 打开服务器上的文件,若打开失败则发送 404 NOT FOUND 给客户端;
* 否则,构造 response headers 和 response body,将文件发送给客户端
*/
resource = fopen(filename, "r");
if (NULL == resource)
{
not_found(c_sock_fd);
}
else
{
headers(c_sock_fd, filename);
cat(c_sock_fd, resource);
}
fclose(resource);
}

/**********************************************************************/
/* This function starts the process of listening for web connections
* on a specified port. If the port is 0, then dynamically allocate a
* port and modify the original port variable to reflect the actual
* port.
* Parameters: pointer to variable containing the port to connect on
* Returns: the socket */
/**********************************************************************/
int startup(u_short *port)
{
int httpd_fd;
int on = 1;
struct sockaddr_in s_addr;
int ret_code;

CHECK_POINTER_NON_NULL(port);

/* 创建一个 socket 描述符,但此时 socket 未与特定地址绑定 */
httpd_fd = socket(AF_INET, // IPv4 地址族
SOCK_STREAM, // 提供有序的,可靠的,双向的,基于字节流的通讯
0); // system picks protocol (as TCP)
ERROR_REPORT_WITH_EXIT(httpd_fd, "socket");

/* 初始化服务器的本地地址,用于与指定的文件描述符绑定(bind)在一起 */
memset(&s_addr, 0, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(*port);
s_addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY = 0.0.0.0

/*
* SO_REUSEADDR 选项允许在套接字关闭后,可以立即重新绑定相同的端口,
* 而不必等待一段时间(不必等待 TCP 的 TIME-WAIT 状态的持续时间)
*/
ret_code = setsockopt(httpd_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
ERROR_REPORT_WITH_EXIT(ret_code, "setsockopt failed");

/*
* 将套接字描述符与指定的服务器本地地址相绑定,
* 若 sockaddr 结构中的端口号为 0,操作系统会自动为套接字描述符分配一个可用的临时端口号
*/
ret_code = bind(httpd_fd, (struct sockaddr *)&s_addr, sizeof(s_addr));
ERROR_REPORT_WITH_EXIT(ret_code, "bind");

/* 检查 bind 调用是否动态分配了端口 */
if (*port == 0)
{
/* 获取 httpd_fd 绑定到的本地地址,位于 s_addr 指向的缓冲区中 */
socklen_t s_addr_len = (socklen_t)sizeof(s_addr);
ret_code = getsockname(httpd_fd, (struct sockaddr *)&s_addr, &s_addr_len);
ERROR_REPORT_WITH_EXIT(ret_code, "getsockname");

*port = ntohs(s_addr.sin_port); // 获取动态分配的端口号,以便在函数外正确读取
}

ret_code = listen(httpd_fd, MAX_CONNECTS);
ERROR_REPORT_WITH_EXIT(ret_code, "listen");

return httpd_fd;
}

/**********************************************************************/
/* Inform the client that the requested web method has not been
* implemented.
* Parameter: the client socket */
/**********************************************************************/
void unimplemented(int c_sock_fd)
{
char buf[1024];
SEND_BUF_MSG(c_sock_fd, buf, 0, "HTTP/1.0 501 Method Not Implemented\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, SERVER_STRING);
SEND_BUF_MSG(c_sock_fd, buf, 0, "Content-Type: text/html\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "</TITLE></HEAD>\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "<BODY><P>HTTP request method not supported.\r\n");
SEND_BUF_MSG(c_sock_fd, buf, 0, "</BODY></HTML>\r\n");
}

/**********************************************************************/

int main(void)
{
int s_sock_fd, c_sock_fd; // 服务端与客户端的文件描述符
u_short port = 4000;
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
pthread_t newthread;
int ret_code;

s_sock_fd = startup(&port);
printf("httpd running on port %d\n", port);

while (1)
{
c_sock_fd = accept(s_sock_fd, (struct sockaddr *)&client_addr, &client_addr_len);
ERROR_REPORT_WITH_EXIT(c_sock_fd, "accept");
printf("httpd accepts a client connect, fd=%d\n", c_sock_fd);

// accept_request((void *)(intptr_t)c_sock_fd);
ret_code = pthread_create(&newthread, NULL, (void *)accept_request, (void *)(intptr_t)c_sock_fd);
if (ret_code != 0)
{
perror("pthread_create");
}
}

close(s_sock_fd);

return 0;
}

参考资料:

  1. https://hanfeng.ink/post/tinyhttpd
  2. https://jacktang816.github.io/post/tinyhttpdread
  3. https://blog.csdn.net/chen1415886044/article/details/103748926
  4. https://blog.csdn.net/weixin_42621338/article/details/84574977
  5. 本文对应的源码:https://github.com/Pursue26/Tinyhttpd