0.环境配置

  1. 安装docker
1
2
sudo apt update
sudo apt install docker.io
  1. 启动docker服务
1
sudo systemctl start docker
  1. 验证安装成功
1
2
3
sudo docker pull docker.m.doacloud.io/hello-world
sudo docker run hello-world
sudo docker images

输出分别如下:

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
┌──(qlc㉿kali)-[~]
└─$ sudo docker pull docker.m.daocloud.io/hello-world
Using default tag: latest
latest: Pulling from hello-world
Digest: sha256:452a468a4bf985040037cb6d5392410206e47db9bf5b7278d281f94d1c2d0931
Status: Image is up to date for docker.m.daocloud.io/hello-world:latest
docker.m.daocloud.io/hello-world:latest
┌──(qlc㉿kali)-[~]
└─$ sudo docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/
┌──(qlc㉿kali)-[~]
└─$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest e2ac70e7319a 6 days ago 10.1kB
docker.m.daocloud.io/hello-world latest e2ac70e7319a 6 days ago 10.1kB
  1. 换源加速
1
2
cd /etc/docker 
sudo vim daemon.json

在.json文件中添加内容

1
2
3
4
5
6
7
8
9
{
"registry-mirrors": [
"https://proxy.1panel.live",
"https://docker.1panel.top",
"https://docker.m.daocloud.io",
"https://docker.1ms.run",
"https://docker.ketches.cn"
]
}
1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

拉取并运行hello-world验证

1
2
sudo docker pull hello-world
sudo docker run docker.m.daocloud.io/hello-world
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
┌──(qlc㉿kali)-[~]
└─$ sudo docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
Digest: sha256:452a468a4bf985040037cb6d5392410206e47db9bf5b7278d281f94d1c2d0931
Status: Image is up to date for hello-world:latest
docker.io/library/hello-world:latest
┌──(qlc㉿kali)-[~]
└─$ sudo docker run docker.m.daocloud.io/hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/
  1. 创建镜像
    将课程提供的webServerStartCodes压缩包传到虚拟机中并解压
1
2
3
unzip webServerStartCodes-2026.zip
cd webServerStartCodes-2025
ls

将用户添加到docker组

1
2
3
sudo usermod -aG docker $USER
newgrp docker
reboot

重启来生效
构建镜像并启动容器(主机路径替换成自己的)

1
2
3
4
5
docker build -t 15-441/641-project-1:latest -f ./Dockerfile .
docker run -it -p 9999:9999 -v /home/qlc/webServerStartCodes-2025/:/home/project-1/ --name Liso 15-441/641-project-1 /bin/bash

# 下面是提示符变化:
root@cc65f5fc4dd9:/home#

1. 阶段1(简单echowebserver的设计)

  1. 编译项目并运行服务器
1
2
3
4
cd project-l
ll
make
./echo_server 9999

新开一个终端测试

1
curl -v http://localhost:9999/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(qlc㉿kali)-[~]
└─$ curl -v http://localhost:9999/
* Host localhost:9999 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:9999...
* Connected to localhost (::1) port 9999
* using HTTP/1.x
> GET / HTTP/1.1
> Host: localhost:9999
> User-Agent: curl/8.12.1
> Accept: */*
>
* Request completely sent off
* Received HTTP/0.9 when not allowed
* closing connection #0
curl: (1) Received HTTP/0.9 when not allowed
  1. 更改server.c(ctrl+c暂停服务器)
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
cd src
cp src/echo_server.c src/echo_server.c.bak
cat > src/echo_server.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <ctype.h>

#define DEFAULT_PORT 9999
#define BUF_SIZE 8192
#define REQ_SIZE 8192

int sock = -1, client_sock = -1;

// 解析请求行
int parse_request_line(char *request, char *method, char *uri, char *version) {
char *line = strtok(request, "\r\n");
if (!line) return -1;

char *token = strtok(line, " ");
if (!token) return -1;
strcpy(method, token);

token = strtok(NULL, " ");
if (!token) return -1;
strcpy(uri, token);

token = strtok(NULL, " ");
if (!token) return -1;
strcpy(version, token);

if (strncmp(version, "HTTP/", 5) != 0) return -1;
return 0;
}

// 检查方法是否支持
int is_supported_method(char *method) {
return (strcmp(method, "GET") == 0 ||
strcmp(method, "HEAD") == 0 ||
strcmp(method, "POST") == 0);
}

// 验证请求格式
int validate_request_format(char *request, int len) {
// 空请求
if (len == 0) return -1;

// 不以HTTP方法开头
char *methods[] = {"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"};
int valid = 0;
for (int i = 0; i < 7; i++) {
if (strncmp(request, methods[i], strlen(methods[i])) == 0) {
valid = 1;
break;
}
}
if (!valid) return -1;

// 缺少第一个空格
char *sp1 = strchr(request, ' ');
if (!sp1) return -1;

// 缺少第二个空格
char *sp2 = strchr(sp1 + 1, ' ');
if (!sp2) return -1;

// HTTP版本格式错误
if (strncmp(sp2 + 1, "HTTP/", 5) != 0) return -1;

// 包含非法字符
for (int i = 0; i < len && i < 200; i++) {
unsigned char c = request[i];
if (c == '\r' || c == '\n' || c == '\0') continue;
if (c < 32 || c > 126) return -1;
}

// 请求行过长
char *crlf = strstr(request, "\r\n");
if (crlf && (crlf - request) > 1024) return -1;

return 0;
}

void handle_client(int client_fd, struct sockaddr_in cli_addr) {
char buffer[BUF_SIZE];
char req_copy[REQ_SIZE];
char method[32], uri[512], version[32];

memset(buffer, 0, BUF_SIZE);
int n = recv(client_fd, buffer, BUF_SIZE - 1, 0);

if (n < 0) {
perror("recv");
return;
}

// 处理空请求:recv 返回 0 表示对方关闭连接
if (n == 0) {
char *resp = "HTTP/1.1 400 Bad Request\r\n\r\n";
send(client_fd, resp, strlen(resp), 0);
printf(">>> Connection closed without data, sent 400 Bad Request\n");
return;
}

printf("\n----- Received %d bytes ------\n", n);
fwrite(buffer, 1, n, stdout);
printf("\n------------------------------\n");

// 检查是否只有空白字符
int only_whitespace = 1;
for (int i = 0; i < n; i++) {
if (buffer[i] != '\r' && buffer[i] != '\n' && buffer[i] != ' ' && buffer[i] != '\t') {
only_whitespace = 0;
break;
}
}
if (only_whitespace) {
char *resp = "HTTP/1.1 400 Bad Request\r\n\r\n";
send(client_fd, resp, strlen(resp), 0);
printf("--- Blank request, 400 Bad Request\n");
return;
}

strncpy(req_copy, buffer, REQ_SIZE - 1);
req_copy[REQ_SIZE - 1] = '\0';

// 格式验证:400
if (validate_request_format(req_copy, n) != 0) {
char *resp = "HTTP/1.1 400 Bad Request\r\n\r\n";
send(client_fd, resp, strlen(resp), 0);
printf("--- 400 Bad Request\n");
return;
}

// 解析请求行
char req_line[REQ_SIZE];
strcpy(req_line, req_copy);
if (parse_request_line(req_line, method, uri, version) != 0) {
char *resp = "HTTP/1.1 400 Bad Request\r\n\r\n";
send(client_fd, resp, strlen(resp), 0);
printf("--- 400 Bad Request (parse)\n");
return;
}

printf("--- Method: %s, URI: %s, Version: %s\n", method, uri, version);

// 方法不支持:501
if (!is_supported_method(method)) {
char *resp = "HTTP/1.1 501 Not Implemented\r\n\r\n";
send(client_fd, resp, strlen(resp), 0);
printf(">>> 501 Not Implemented\n");
return;
}

// Echo 返回原始请求
char response[REQ_SIZE + 512];
int rlen = snprintf(response, sizeof(response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
n, buffer);

send(client_fd, response, rlen, 0);
printf(">>> 200 OK (echo, %d bytes)\n", rlen);
}

int main(int argc, char *argv[]) {
int port = (argc > 1) ? atoi(argv[1]) : DEFAULT_PORT;
struct sockaddr_in addr, cli_addr;
socklen_t cli_size;

signal(SIGPIPE, SIG_IGN);

sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket");
return EXIT_FAILURE;
}

int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;

if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
return EXIT_FAILURE;
}

if (listen(sock, 10) == -1) {
perror("listen");
return EXIT_FAILURE;
}

printf("\n-----------------------\n");
printf("Echo Web Server - Phase 1\n");
printf("Listening on port %d\n", port);
printf("Supported: GET, HEAD, POST\n");
printf("501: other methods, 400: bad format\n");
printf("------------------------\n\n");

while (1) {
cli_size = sizeof(cli_addr);
printf("Waiting for connection...\n");

client_sock = accept(sock, (struct sockaddr*)&cli_addr, &cli_size);
if (client_sock == -1) {
perror("accept");
continue;
}

printf("Connection from %s:%d\n",
inet_ntoa(cli_addr.sin_addr),
ntohs(cli_addr.sin_port));

handle_client(client_sock, cli_addr);
close(client_sock);
printf("Connection closed\n");
}

close(sock);
return EXIT_SUCCESS;
}
EOF
cd ..
make clean
make
./echo_server 9999

再新开终端进行测试

1
2
3
4
5
6
7
8
9
10
curl -v http://localhost:9999/
curl -v -X POST -d "username=test&password=123" http://localhost:9999/
curl -v -X HEAD http://localhost:9999/
curl -v -X PUT http://localhost:9999/
curl -v -X DELETE http://localhost:9999/
echo -n "" | nc localhost 9999
echo -e "GET/ HTTP/1.1\r\n\r\n" | nc localhost 9999
echo -e "INVALID / HTTP/1.1\r\n\r\n" | nc localhost 9999
echo -e "GET HTTP/1.1\r\n\r\n" | nc localhost 9999
echo -e "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nGET /test HTTP/1.1\r\nHost: localhost\r\n\r\n" | nc localhost 9999

阶段2(基本HEAD、GET、POST方法的设计)

  1. 启动容器
1
docker start -i Liso 
  1. 更改echo_server.c
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
// 新增头文件
#include <sys/stat.h>
#include <time.h>
#include <errno.h>

// 新定义常量
#define STATIC_DIR "./static_site"

// 删除RESP_200_HEADER
// 新增:
char *RESP_404 = "HTTP/1.1 404 Not Found\r\n\r\n";
char *RESP_505 = "HTTP/1.1 505 HTTP Version not supported\r\n\r\n";

// 新增8个函数
// 日志函数
void log_access(const char *ip, const char *method, const char *uri, int status, int bytes) {
FILE *fp = fopen("logs/access.log", "a");
if (!fp) return;

time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char time_str[64];
strftime(time_str, sizeof(time_str), "[%d/%b/%Y:%H:%M:%S %z]", tm_info);

fprintf(fp, "%s - - %s \"%s %s HTTP/1.1\" %d %d\n",
ip, time_str, method, uri, status, bytes);
fclose(fp);
}

void log_error(const char *msg) {
FILE *fp = fopen("logs/error.log", "a");
if (!fp) return;

time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char time_str[64];
strftime(time_str, sizeof(time_str), "[%d/%b/%Y:%H:%M:%S %z]", tm_info);

fprintf(fp, "%s [error] %s\n", time_str, msg);
fclose(fp);
}

// 获取MIME类型
const char* get_mime_type(const char *filepath) {
const char *dot = strrchr(filepath, '.');
if (!dot) return "text/plain";

if (strcmp(dot, ".html") == 0) return "text/html";
if (strcmp(dot, ".css") == 0) return "text/css";
if (strcmp(dot, ".js") == 0) return "application/javascript";
if (strcmp(dot, ".png") == 0) return "image/png";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0) return "image/jpeg";
if (strcmp(dot, ".gif") == 0) return "image/gif";
if (strcmp(dot, ".txt") == 0) return "text/plain";

return "application/octet-stream";
}

// 发送成功响应头
void send_response_header(int fd, const char *content_type, int content_len, int keep_alive) {
char header[512];
int len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %d\r\n"
"Connection: %s\r\n"
"\r\n",
content_type ? content_type : "text/html",
content_len,
keep_alive ? "keep-alive" : "close");
send(fd, header, len, 0);
}

// 发送错误响应(按照指导书格式)
void send_error_response(int fd, int error_code) {
const char *response;
switch (error_code) {
case 400: response = RESP_400; break;
case 404: response = RESP_404; break;
case 501: response = RESP_501; break;
case 505: response = RESP_505; break;
default: response = RESP_400; break;
}
send(fd, response, strlen(response), 0);
}

// 处理GET请求
void handle_get(int fd, const char *uri, int keep_alive, const char *ip) {
char filepath[512];
struct stat st;

// 防止目录遍历攻击
if (strstr(uri, "..")) {
send_error_response(fd, 400);
log_access(ip, "GET", uri, 400, 0);
return;
}

// 构建文件路径
if (strcmp(uri, "/") == 0) {
snprintf(filepath, sizeof(filepath), "%s/index.html", STATIC_DIR);
} else {
snprintf(filepath, sizeof(filepath), "%s%s", STATIC_DIR, uri);
}

// 检查文件是否存在
if (stat(filepath, &st) != 0) {
send_error_response(fd, 404);
log_access(ip, "GET", uri, 404, 0);
log_error("File not found");
return;
}

// 检查是否为目录
if (S_ISDIR(st.st_mode)) {
send_error_response(fd, 404);
log_access(ip, "GET", uri, 404, 0);
return;
}

// 打开并读取文件
FILE *fp = fopen(filepath, "rb");
if (!fp) {
send_error_response(fd, 404);
log_access(ip, "GET", uri, 404, 0);
return;
}

char *content = malloc(st.st_size);
if (!content) {
fclose(fp);
send_error_response(fd, 500);
return;
}

size_t read_size = fread(content, 1, st.st_size, fp);
fclose(fp);

// 发送响应
const char *mime = get_mime_type(filepath);
send_response_header(fd, mime, read_size, keep_alive);
send(fd, content, read_size, 0);
log_access(ip, "GET", uri, 200, read_size);

free(content);
}

// 处理HEAD请求
void handle_head(int fd, const char *uri, int keep_alive, const char *ip) {
char filepath[512];
struct stat st;

if (strstr(uri, "..")) {
send_error_response(fd, 400);
log_access(ip, "HEAD", uri, 400, 0);
return;
}

if (strcmp(uri, "/") == 0) {
snprintf(filepath, sizeof(filepath), "%s/index.html", STATIC_DIR);
} else {
snprintf(filepath, sizeof(filepath), "%s%s", STATIC_DIR, uri);
}

if (stat(filepath, &st) != 0) {
send_error_response(fd, 404);
log_access(ip, "HEAD", uri, 404, 0);
return;
}

if (S_ISDIR(st.st_mode)) {
send_error_response(fd, 404);
log_access(ip, "HEAD", uri, 404, 0);
return;
}

// HEAD只发送响应头
const char *mime = get_mime_type(filepath);
send_response_header(fd, mime, st.st_size, keep_alive);
log_access(ip, "HEAD", uri, 200, st.st_size);
}

// 处理POST请求 - 按照正确答案:返回整个原始请求
void handle_post(int fd, int keep_alive, const char *ip, const char *uri, char *buffer, int buf_len) {
// 返回整个原始请求内容
char response[8192];
int len = snprintf(response, sizeof(response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: %d\r\n"
"Connection: %s\r\n"
"\r\n"
"%.*s",
buf_len,
keep_alive ? "keep-alive" : "close",
buf_len, buffer);

send(fd, response, len, 0);
log_access(ip, "POST", uri, 200, buf_len);
}

// 修改:
int is_bad_request(char *request, int len) {
if (len <= 0) return 1;

// 检查头部是否超过8192字节(新增)
if (len > 8192) return 1;

char *sp1 = strchr(request, ' ');
if (!sp1) return 1;

char *sp2 = strchr(sp1 + 1, ' ');
if (!sp2) return 1;

if (strncmp(sp2 + 1, "HTTP/", 5) != 0) return 1;

// 检查方法是否全大写
int method_len = sp1 - request;
for (int i = 0; i < method_len; i++) {
if (!isupper(request[i])) return 1;
}

return 0;
}

// 处理客户端请求
void handle_client(int client_fd, struct sockaddr_in cli_addr) {
char buffer[BUF_SIZE];
char method[32] = {0}, uri[512] = {0}, version[32] = {0};
char client_ip[INET_ADDRSTRLEN];

inet_ntop(AF_INET, &cli_addr.sin_addr, client_ip, sizeof(client_ip));

while (1) {
memset(buffer, 0, BUF_SIZE);
int n = recv(client_fd, buffer, BUF_SIZE - 1, 0);

if (n <= 0) {
break;
}

// 检查请求格式
if (is_bad_request(buffer, n)) {
send_error_response(client_fd, 400);
log_access(client_ip, "UNKNOWN", "?", 400, 0);
break;
}

// 解析请求行
char req_line[REQ_SIZE];
strcpy(req_line, buffer);

if (parse_request_line(req_line, method, uri, version) != 0) {
send_error_response(client_fd, 400);
log_access(client_ip, "UNKNOWN", "?", 400, 0);
break;
}

// 检查HTTP版本
if (strcmp(version, "HTTP/1.1") != 0) {
send_error_response(client_fd, 505);
log_access(client_ip, method, uri, 505, 0);
break;
}

// 检查是否支持该方法
if (!is_supported_method(method)) {
send_error_response(client_fd, 501);
log_access(client_ip, method, uri, 501, 0);
break;
}

// 检查Connection头部决定是否保持连接
int keep_alive = 1;
char *conn = strstr(buffer, "Connection:");
if (conn) {
if (strstr(conn, "close")) {
keep_alive = 0;
}
}

// 根据方法处理
if (strcmp(method, "GET") == 0) {
handle_get(client_fd, uri, keep_alive, client_ip);
} else if (strcmp(method, "HEAD") == 0) {
handle_head(client_fd, uri, keep_alive, client_ip);
} else if (strcmp(method, "POST") == 0) {
handle_post(client_fd, keep_alive, client_ip, uri, buffer, n);
}

// 如果不保持连接,退出循环
if (!keep_alive) {
break;
}
}

close(client_fd);
}

// main() 新增日志目录创建和详细输出
// 新增
system("mkdir -p logs"); // 创建日志目录

// 修改输出信息
printf("========================================\n");
printf("HTTP/1.1 Web Server (Phase 2)\n");
printf("Port: %d\n", port);
printf("Static files directory: %s\n", STATIC_DIR);
printf("Logs directory: logs/\n");
printf("========================================\n");
fflush(stdout); // 新增,确保输出立即刷新
  1. 更改makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 更改all规则
all : example echo_server echo_client
@echo "========================================="
@echo "Starting HTTP/1.1 Web Server on port 9999"
@echo "========================================="
@nohup ./echo_server 9999 > server.log 2>&1 &
@sleep 2
@echo "Server started (PID: $$(pgrep echo_server))"
# 更改clean规则
clean:
$(RM) $(OBJ) $(BIN) $(SRC_DIR)/lex.yy.c $(SRC_DIR)/y.tab.*
$(RM) -r $(OBJ_DIR)
@pkill echo_server 2>/dev/null || true
# 修改.PHONY
.PHONY: all clean stop restart
  1. 重新编译运行
1
2
make clean
make
  1. 打包
1
2
cd ~/webServerStartCodes-2025
sudo tar -czvf ../webServerStartCodes.tar *
  1. 相关测试
1
2
3
4
5
6
7
curl -v http://localhost:9999/
curl -I http://localhost:9999/
curl -X POST -d "Hello World" http://localhost:9999/
curl -v http://localhost:9999/notexist.html
curl -X PUT http://localhost:9999/
curl --http1.0 http://localhost:9999/
curl -v http://localhost:9999/ http://localhost:9999/

阶段三(实现 HTTP 流水线请求)

  1. 修改缓冲区大小来支持多个请求
1
#define BUF_SIZE 16384
  1. 新增辅助函数用来检查请求是否完整
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int is_complete_request(char *buffer, int len) {
char *end = strstr(buffer, "\r\n\r\n");
if (!end) return 0;

// 检查是否需要读取Content-Length指定的body
char *cl = strstr(buffer, "Content-Length:");
if (cl && cl < end) {
int content_length = atoi(cl + 15);
int body_start = (end + 4) - buffer;
return len >= body_start + content_length;
}

return 1; // 无body的请求,有\r\n\r\n就是完整的
}
  1. 修改handle_client函数
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
void handle_client(int client_fd, struct sockaddr_in cli_addr) {
char buffer[BUF_SIZE];
char method[32] = {0}, uri[512] = {0}, version[32] = {0};
char client_ip[INET_ADDRSTRLEN];
int total_received = 0;
int keep_connection = 1;

inet_ntop(AF_INET, &cli_addr.sin_addr, client_ip, sizeof(client_ip));

while (keep_connection) {
// 接收数据
memset(buffer + total_received, 0, BUF_SIZE - total_received);
int n = recv(client_fd, buffer + total_received, BUF_SIZE - 1 - total_received, 0);

if (n <= 0) {
break;
}

total_received += n;

// 循环处理缓冲区中的所有完整请求
int offset = 0;
while (offset < total_received) {
char *request_start = buffer + offset;
char *request_end = strstr(request_start, "\r\n\r\n");

if (!request_end) {
break; // 请求不完整,继续接收更多数据
}

// 计算当前请求的长度
int request_len = (request_end + 4) - request_start;

// 检查是否需要读取body
char *cl = strstr(request_start, "Content-Length:");
int content_length = 0;
if (cl && cl < request_end) {
content_length = atoi(cl + 15);
int total_needed = request_len + content_length;
if (offset + total_needed > total_received) {
break; // body不完整,继续接收
}
request_len += content_length;
}

// 复制当前请求
char current_request[REQ_SIZE];
strncpy(current_request, request_start, request_len);
current_request[request_len] = '\0';

// 复制请求内容用于解析
char req_copy[REQ_SIZE];
strncpy(req_copy, current_request, REQ_SIZE - 1);
req_copy[REQ_SIZE - 1] = '\0';

// 解析请求行
char req_line[REQ_SIZE];
strcpy(req_line, req_copy);

// 检查请求格式
if (is_bad_request(current_request, request_len)) {
send_error_response(client_fd, 400);
log_access(client_ip, "UNKNOWN", "?", 400, 0);
// 继续处理后续请求,不中断连接
offset += request_len;
continue;
}

if (parse_request_line(req_line, method, uri, version) != 0) {
send_error_response(client_fd, 400);
log_access(client_ip, "UNKNOWN", "?", 400, 0);
offset += request_len;
continue;
}

// 检查HTTP版本
if (strcmp(version, "HTTP/1.1") != 0) {
send_error_response(client_fd, 505);
log_access(client_ip, method, uri, 505, 0);
offset += request_len;
continue;
}

// 检查是否支持该方法
if (!is_supported_method(method)) {
send_error_response(client_fd, 501);
log_access(client_ip, method, uri, 501, 0);
offset += request_len;
continue;
}

// 检查Connection头部决定是否保持连接
int keep_alive = 1;
char *conn = strstr(current_request, "Connection:");
if (conn && strstr(conn, "close")) {
keep_alive = 0;
keep_connection = 0; // 最后一个请求,处理完关闭
}

// 根据方法处理
if (strcmp(method, "GET") == 0) {
handle_get(client_fd, uri, keep_alive, client_ip);
} else if (strcmp(method, "HEAD") == 0) {
handle_head(client_fd, uri, keep_alive, client_ip);
} else if (strcmp(method, "POST") == 0) {
handle_post(client_fd, keep_alive, client_ip, uri, current_request, request_len);
}

// 移动到下一个请求
offset += request_len;
}

// 移动剩余未处理的数据到缓冲区开头
if (offset > 0 && offset < total_received) {
memmove(buffer, buffer + offset, total_received - offset);
total_received -= offset;
} else if (offset >= total_received) {
total_received = 0;
}

// 如果连接不需要保持,退出循环
if (!keep_connection) {
break;
}
}

close(client_fd);
}f
  1. 在终端进行测试:预计输出3个200 Ok状态
1
2
3
4
printf 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n' > req.txt
printf 'GET /style.css HTTP/1.1\r\nHost: localhost\r\n\r\n' >> req.txt
printf 'POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\n\r\ntest' >> req.txt
cat req.txt | nc localhost 9999