原文:https://cs162.org/static/hw/hw-http/
归档参考(Wayback):https://web.archive.org/web/20251215163710/https://cs162.org/static/
Hypertext Transport Protocol(HTTP)是当今 Internet 上最常用的应用层协议。与许多网络协议一样,HTTP 采用 client-server 模型。HTTP client 打开到 HTTP server 的网络连接并发送 HTTP request message。随后,server 回复 HTTP response message,其中通常包含 client 所请求的某种资源(例如 file、text、binary data)。
在本作业中,你将实现一个处理 HTTP GET request 的 HTTP server。你需要通过 HTTP response header 提供功能、支持 HTTP error code、用 HTML 生成 directory listing,并实现一个 HTTP proxy。request 与 response header 必须符合 HTTP 1.0 协议,协议说明见 here。
开始前,登录你的 development environment 并获取 starter code。
cd ~/code/personal/
git pull staff main
cd hw-http来源:https://cs162.org/static/hw/hw-http/docs/background/
以下是关于你将要实现的 HTTP server 的一些基本信息。
HTTP request message 的格式为:
- 一行 HTTP request line,包含 method、request URI,以及 HTTP protocol version
- 零行或多行 HTTP header lines
- 一个空行(即单独一个 CRLF)
HTTP 使用的行结束符是 CRLF,在 C 中表示为 \r\n。
下面是 Google Chrome 浏览器向运行在 localhost(127.0.0.1)端口 8000 上的 HTTP web server 发送的示例 HTTP request message(其中的 CRLF 用转义序列写出):
GET /hello.html HTTP/1.0\r\n
Host: 127.0.0.1:8000\r\n
Connection: keep-alive\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
User-Agent: Chrome/45.0.2454.93\r\n
Accept-Encoding: gzip,deflate,sdch\r\n
Accept-Language: en-US,en;q=0.8\r\n
\r\n
Header lines 提供关于该 request 的信息。若要更深入理解,请 open your web browser’s developer tools,然后点击 “Network” 标签,查看你请求任意网页时发送的 headers。以下是一些 HTTP request header 类型:
- Host:包含 HTTP request 的 URL 中的 hostname 部分(例如
inst.eecs.berkeley.edu或127.0.0.1:8000) - User-Agent:标识 HTTP client 程序,形式为
Program-name/x.xx,其中x.xx是程序版本。在上面的例子中,Google Chrome 浏览器将 User-Agent 设为Chrome/45.0.2454.93(至少在 Web 早期是这个想法。如今 User-Agent 通常是一团乱麻。如果你好奇原因,the history behind it 挺有意思)。
HTTP response message 的格式为:
- 一行 HTTP response status line,包含 HTTP protocol version、status code,以及对 status code 的可读描述
- 零行或多行 HTTP header lines
- 一个空行(即单独一个 CRLF)
- body(即 HTTP request 所请求的 content)
下面是一个 status code 为 200、body 为 HTML 文件的示例 HTTP response(其中的 CRLF 用转义序列写出):
HTTP/1.0 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 84\r\n
\r\n
<html>\n
<body>\n
<h1>Hello World</h1>\n
<p>\n
Let's see if this works\n
</p>\n
</body>\n
</html>\n
典型的 status line 可能是 HTTP/1.0 200 OK(如上例)、HTTP/1.0 404 Not Found 等。
status code 是一个三位数整数,第一位标识 response 的大致类别。(若感兴趣,更多信息见 here):
- 1xx 表示仅 informational message
- 2xx 表示 success
- 3xx 将 client 重定向到另一个 URL
- 4xx 表示 client 侧错误
- 5xx 表示 server 侧错误
Header lines 提供关于该 response 的信息。以下是一些 HTTP response header 类型:
- Content-Type:附在 response 上的数据的 MIME type,例如
text/html或text/plain - Content-Length:response body 的字节数
从网络角度看,你的基本 HTTP web server 应实现以下步骤。
- 创建 listening socket 并将其 bind 到某个 port
- 等待 client 连接到该 port
- Accept client 并获得新的 connection socket
- 读入并解析 HTTP request
- 做以下两件事之一(由 command line arguments 决定):
- 从本地 file system 提供一个 file,或返回 404 Not Found
- 将 request proxy 到另一台 HTTP server。使用 proxy 时,HTTP server 通过把 request 流式转发到远程 HTTP server(proxy)来提供服务。来自 proxy 的 response 再发回给 clients。
httpserver 会处于 file mode 或 proxy mode 之一;不会同时做这两件事。
- 将适当的 HTTP response header 以及附带的 file/document 发回 client(或错误消息)
骨架代码已经实现了步骤 2–4。
以下说明如何从 shell 调用 http_server。参数解析步骤已经为你实现好了:
./httpserver --help
Usage: ./httpserver --files any_directory_with_files/ [--port 8000 --num-threads 5]
./httpserver --proxy inst.eecs.berkeley.edu:80 [--port 8000 --num-threads 5]
可用选项如下:
--files
选择用于提供 files 的目录。你应从
hw-http/文件夹提供 files(例如,若你当前在hw-http/文件夹中,应直接使用--files www/。
--proxy
选择要 proxy 的 “upstream” HTTP server。参数可以在冒号后带 port 号(例如
inst.eecs.berkeley.edu:80)。若未指定 port 号,默认是 port 80。
--port
选择 HTTP server 监听 incoming connections 的 port。在 files mode 与 proxy mode 中都使用。若未指定 port 号,默认是 port 8000。若要使用 0 到 1023 之间的 port 号,需要以 root 运行你的 HTTP server。这些 port 是 “reserved” ports,只能由 root user bind。你可以这样运行:
sudo http_server_rs --port PORT --files www/。
--num-threads
表示你的 thread pool 中能够并发服务 client requests 的 thread 数量。该参数最初未被使用,需要你正确使用它。
运行 make 会得到 4 个可执行文件:httpserver、forkserver、threadserver 和 poolserver。
要测试你的 server,至少需要两个不同的 terminal,且都已 ssh 到 Workspace。使用 tmux 会更方便,但不是必须的。
在其中一个 terminal 中启动你的 server,使其开始监听 connections。可以运行 ./httpserver --files www。你可以用上面的 flags 修改该命令,也可以修改 --files flag 的参数。此外,一旦实现了其他类型的 server,你需要根据要测试的 server 使用相应的可执行文件。
注意:你可以使用上文提到的 --port flag 指定 server 用于监听 connections 的 port。若上面的基本命令有问题,可以尝试用 --port flag 更换 server 使用的 port。若问题仍然存在,请参见下方 Troubleshooting 一节。
在另一个 terminal 中,你应能使用下方可见的某个 curl 命令。这会向你在另一 terminal 中启动的 server 发送 request。
要运行你的 server,应 ssh 进入 Workspace,然后按所示命令运行。
你可以用 curl 程序发送 HTTP requests。使用 curl 的示例如下:
curl -v http://0.0.0.0:8000/
curl -v http://0.0.0.0:8000/index.html
curl -v http://0.0.0.0:8000/path/to/file
你也可以用 netcat(nc)通过 network socket 直接打开到 HTTP server 的连接,并手动输入 HTTP request(或从文件 pipe 进去)。
> nc -v 0.0.0.0 8000
Connection to 0.0.0.0 8000 port [tcp/*] succeeded!
> (Now, type out your HTTP request here.)
完成 GET request (directories) 任务后,你可以通过 web browser 访问你的 HTTP server。你需要使用 port forwarding 配置本地机器,以便在你 ssh 进入 Workspace 时把连接隧道到 Workspace。典型的 port forwarding 命令示例如下。
ssh -L local_port:destination_server_ip:remote_port ssh_server_hostname
在这种情况下,你应从本地机器执行命令,命令应类似于:
ssh -L 8000:127.0.0.1:8000 workspace
注意:若你的机器上已在使用 port 8000,则需另选一个 port 来隧道连接。任何高于 1024 的 port 都应可用。
配置好 tunnel 后,你应能在 browser 中通过 localhost 连接以及你为 tunnel 选择的 port 访问。
Failed to bind on socket: Address already in use
这意味着你有一个
httpserver在后台运行。若代码泄漏了仍持有 sockets 的 processes,或你断开了与 Workspace 的连接却从未关闭httpserver,就可能发生这种情况。可通过运行pkill -9 httpserver修复。若仍不行,可通过--port指定另一个 port。
Failed to bind on socket: Permission denied
若使用小于 1024 的 port 号,可能收到此错误。只有 root user 才能使用 “well-known” ports(编号 1 到 1023),因此你应选择更高的 port 号(1024 到 65535)。
来源:https://cs162.org/static/hw/hw-http/docs/tasks/
本 homework 你需要实现以下任务。
来源:https://cs162.org/static/hw/hw-http/docs/tasks/socket/
在 serve_forever 函数中完成 server socket 的设置。
- 用
bindsyscall 将 socket bind 到命令行指定的 IPv4 address 与 port(即server_port)。 - 然后,用
listensyscall 开始监听 incoming clients。在此阶段,listen的backlog参数取 1024 即可。在 Performance 做 load testing 时,你可以尝试调整该值,并评论它如何影响 server performance。
完成此部分后,curl 应输出 ”Empty reply from server”。
来源:https://cs162.org/static/hw/hw-http/docs/tasks/get_request/
实现 handle_files_request 以处理针对 files 的 HTTP GET requests。你需要相应地调用 serve_file。你还应能处理对 files directory 子目录中文件的请求(例如 GET /images/hero.jpg)。
- 若 path 所指的 file 存在,对其调用
serve_file。读取该 file 的内容并写入 client socket。- 确保设置正确的
Content-LengthHTTP header。该 header 的值应为 HTTP response body 的大小,以字节计。例如Content-Length: 7810。你可以用snprintf把整数转换成字符串。 - 本作业必须使用
read与writesyscalls。任何使用fread或fwrite的实现都不会得分。这纯粹是出于教学原因;我们希望你熟悉 low-level I/O 可能不会一次性完成所请求的全部字节这一事实。
- 确保设置正确的
- 否则,向 client 返回
404 Not Foundresponse(HTTP body 可选)。HTTP request 过程中可能出错的地方很多,但我们只要求你为不存在的 file 支持404 Not Found错误消息。完成此部分后,对index.html做 curl 应输出文件index.html的内容。
实现 handle_files_request,使其同时处理针对 files 与 directories 的 HTTP GET requests。
-
你现在需要判断
handle_files_request中的path指向的是 file 还是 directory。statsyscall 以及S_ISDIR或S_ISREGmacros 对此会很有用。判明path是 file 还是 directory 后,你需要相应地调用serve_file或serve_directory。 -
若该 directory 包含
index.html文件,则以200 OK响应,并返回index.html文件的全部内容。你不能假设 directory requests 的 query string 一定带有尾部斜杠。libhttp.c中的http_format_index函数可能有用。
-
若该 directory 不包含
index.html文件,则响应一个 HTML 页面,其中包含指向该 directory 所有直接子项的链接(类似ls -1),以及指向 parent directory 的链接。libhttp.c中的http_format_href函数可能有用。- 要列出 directory 内容,好用的函数是
opendir与readdir。
-
若该 directory 不存在,向 client 返回
404 Not Foundresponse。 -
你不必担心链接中多余的斜杠(例如
//files///a.jpg完全没问题)。file system 与 web browser 对此都能容忍。 -
你不需要处理 files 与 directories 以外的 file system 对象(即不必处理 symbolic links、pipes 或 special files)。
-
记住在从
handle_files_request函数返回前关闭 client socket。 -
尽可能编写 helper functions 以复用相似代码。这会让代码更易调试!
完成此部分后,对根目录 / 做 curl 应输出文件 index.html 的内容。Basic Server 的所有测试应在 autograder 上通过。
来源:https://cs162.org/static/hw/hw-http/docs/tasks/proxy/
实现 handle_proxy_request,将 HTTP requests proxy 到另一台 HTTP server。连接建立相关代码我们已经为你处理好了。你应阅读并理解它,但不必修改。
以下是已经实现的部分。
- 我们使用
--proxycommand line argument 的值,其中包含 upstream HTTP server 的 address 与 port 号。这两个值分别存在全局变量server_proxy_hostname与server_proxy_port中。 - 我们对 server proxy hostname 做 DNS lookup,以查找该 hostname 的 IP address(参见
gethostbyname)。 - 我们创建 network socket,并将其 connect 到从 DNS 得到的 IP address。参见
socket与connect。 - 使用
htons设置 socket 的 port 号(x86 上内存中的整数是 little-endian,而网络相关内容期望 big-endian)。另请注意 HTTP 是SOCK_STREAM协议。
以下是你需要处理的部分。
- 等待两个 sockets(HTTP client fd,以及目标 HTTP server fd)上的新数据。当数据到达时,你应立即将其读入 buffer,再写入另一个 socket。本质上你是在 HTTP client 与目标 HTTP server 之间维护双向通信。你的 proxy 必须支持多个 requests/responses。
- 这比向 file 写入或从
stdin读取更棘手,因为你不知道双向 stream 的哪一侧会先写数据,也不知道它们在收到 response 后是否还会继续写更多数据。在 proxy mode 中,你会发现多个 HTTP requests/responses 会在同一条 connection 内发送,这与你的 HTTP server 只需支持每条 connection 一次 request/response 不同。 - 本任务应使用
pthreads。可考虑用两个 threads 分别促成双向通信,一个从 A 到 B,另一个从 B 到 A。 - 不要使用
select、fcntl之类。以前学期我们曾推荐过这种方法,但我们发现它太容易令人困惑。
- 这比向 file 写入或从
- 若任一 socket 关闭,通信无法继续,因此你应关闭两个 sockets 以终止该 connection。
完成此部分后,所有 Proxy 测试应在 autograder 上通过。
来源:https://cs162.org/static/hw/hw-http/docs/tasks/servers/
在本节中,你将实现几种不同的 servers。借助条件编译预处理指令(即 #ifdef directives),我们只需改变在这些不同 servers 中如何调用 request handler。
实现 forkserver。你不需要写太多新代码。
- child process 应使用 client socket fd 调用
request_handler。发送完 response 后,child process 将终止。 - parent process 将继续监听并 accept incoming connections。它不会 wait child。
- 记住在 parent 与 child process 中都要适当地关闭 sockets。
实现 threadserver。
- 创建新的
pthread向 client 发送正确的 response。 - 原来的 thread 继续监听并 accept incoming connections。它不会 join 那个新 thread。
实现 poolserver。
- 你的 thread pool 应恰好能并发服务
--num-threads个 clients,不能更多。注意我们的程序中通常使用--num-threads + 1个 threads。原来的 thread 负责在 while loop 中 accept client connections,并将相关 requests 分派给 thread pool 中的 threads 处理。 - 先从查看
wq.h中的函数开始。- 原来的 thread(即你用来启动
httpserver程序的那个 thread)应将从accept得到的 client socket file descriptorswq_push到声明在httpserver.c顶部、定义在wq.h中的wq_t work_queue。 - 然后,thread pool 中的 threads 应使用
wq_pop获取下一个要处理的 client socket file descriptor。
- 原来的 thread(即你用来启动
- 你需要让 server spawn
--num-threads个新 threads,它们在 loop 中循环执行以下操作:- 对下一个 client socket file descriptor 做阻塞的
wq_pop调用。 - 成功 pop 出一个待服务的 client socket fd 后,调用适当的 request handler 处理该 client request。
- 对下一个 client socket file descriptor 做阻塞的
来源:https://cs162.org/static/hw/hw-http/docs/tasks/performance/
测试、测量并评论 httpserver、forkserver、threadserver 与 poolserver 的 performance。我们将使用 Apache HTTP server benchmarking tool(简称 ab)对每种 server 类型做 load test。你可以用 sudo apt-get install -y apache2-utils 安装 ab。
- 运行
./httpserver --files www/。 - 在另一个 terminal 窗口中,运行
ab -n 500 -c 10 http://192.168.162.162:8000/
该命令以并发度 10(即一次派发 10 个 requests)发出 500 个 requests。阅读 man ab 以了解该工具。你可以在 terminal 或你喜欢的搜索引擎中输入 “man ab”。不过请注意,在 Google 中输入 “man ab” 也会给你一些轮廓分明的男性腹肌图片;请自行决定是否这样做。
注意 ab 会输出 mean time per request。记下该值,并评论当我们改变 server 如何 处理 requests 时它如何变化。
- 用
ab对forkserver、threadserver与poolserver做 load-test。也可以调整n与c变量,以及poolserver中 thread pool 的大小。
你不再需要在 Gradescope 上回答这些问题,但请测试并思考以下问题!
- 对
httpserver运行ab。当n与c变大时会发生什么? - 对
forkserver运行ab。当n与c变大时会发生什么?将这些结果与上一题的答案比较。 - 对
threadserver运行ab。当n与c变大时会发生什么?将这些结果与前面问题的答案比较。 - 对
poolserver运行ab。当n与c变大时会发生什么?将这些结果与前面问题的答案比较。
来源:https://cs162.org/static/hw/hw-http/docs/submission/
要提交并推送到 autograder,请把你的更改 push 到你的 repo。这应会触发 autograder;除非你在使用 slip days,那种情况下需要手动运行 autograder。你应在几分钟内收到来自 autograder 的 email。若半小时内仍未收到 autograder 的 email,请通过 Ed 上的 private post 通知 staff。你的代码不应包含多余的或 debugging 用的 print statements,因为这会干扰(原文:interefere)autograder。