Skip to content

Latest commit

 

History

History
362 lines (228 loc) · 19.5 KB

File metadata and controls

362 lines (228 loc) · 19.5 KB

HW 3: HTTP(指南中文译本)

原文: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

Getting started

开始前,登录你的 development environment 并获取 starter code。

cd ~/code/personal/
git pull staff main
cd hw-http

Background

来源:https://cs162.org/static/hw/hw-http/docs/background/

以下是关于你将要实现的 HTTP server 的一些基本信息。

Structure of an HTTP request

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.edu127.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 挺有意思)。

Structure of an HTTP response

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 lines

典型的 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

Header lines 提供关于该 response 的信息。以下是一些 HTTP response header 类型:

  • Content-Type:附在 response 上的数据的 MIME type,例如 text/htmltext/plain
  • Content-Length:response body 的字节数

Server outline

从网络角度看,你的基本 HTTP web server 应实现以下步骤。

  1. 创建 listening socket 并将其 bind 到某个 port
  2. 等待 client 连接到该 port
  3. Accept client 并获得新的 connection socket
  4. 读入并解析 HTTP request
  5. 做以下两件事之一(由 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 之一;不会同时做这两件事。

  1. 将适当的 HTTP response header 以及附带的 file/document 发回 client(或错误消息)

骨架代码已经实现了步骤 2–4。

Usage

以下说明如何从 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 个可执行文件:httpserverforkserverthreadserverpoolserver

How to test your server

要测试你的 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。

Accessing your server

要运行你的 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 访问。

Troubleshooting

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)。


Tasks

来源:https://cs162.org/static/hw/hw-http/docs/tasks/

本 homework 你需要实现以下任务。


Socket

来源:https://cs162.org/static/hw/hw-http/docs/tasks/socket/

serve_forever 函数中完成 server socket 的设置。

  • bind syscall 将 socket bind 到命令行指定的 IPv4 address 与 port(即 server_port)。
  • 然后,用 listen syscall 开始监听 incoming clients。在此阶段,listenbacklog 参数取 1024 即可。在 Performance 做 load testing 时,你可以尝试调整该值,并评论它如何影响 server performance。

完成此部分后,curl 应输出 ”Empty reply from server”。


GET request

来源:https://cs162.org/static/hw/hw-http/docs/tasks/get_request/

Files

实现 handle_files_request 以处理针对 files 的 HTTP GET requests。你需要相应地调用 serve_file。你还应能处理对 files directory 子目录中文件的请求(例如 GET /images/hero.jpg)。

  • 若 path 所指的 file 存在,对其调用 serve_file。读取该 file 的内容并写入 client socket。
    • 确保设置正确的 Content-Length HTTP header。该 header 的值应为 HTTP response body 的大小,以字节计。例如 Content-Length: 7810。你可以用 snprintf 把整数转换成字符串。
    • 本作业必须使用 readwrite syscalls。任何使用 freadfwrite 的实现都不会得分。这纯粹是出于教学原因;我们希望你熟悉 low-level I/O 可能不会一次性完成所请求的全部字节这一事实。
  • 否则,向 client 返回 404 Not Found response(HTTP body 可选)。HTTP request 过程中可能出错的地方很多,但我们只要求你为不存在的 file 支持 404 Not Found 错误消息。完成此部分后,对 index.html 做 curl 应输出文件 index.html 的内容。

Directories

实现 handle_files_request,使其同时处理针对 files 与 directories 的 HTTP GET requests。

  • 你现在需要判断 handle_files_request 中的 path 指向的是 file 还是 directory。stat syscall 以及 S_ISDIRS_ISREG macros 对此会很有用。判明 path 是 file 还是 directory 后,你需要相应地调用 serve_fileserve_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 内容,好用的函数是 opendirreaddir
  • 若该 directory 不存在,向 client 返回 404 Not Found response。

  • 你不必担心链接中多余的斜杠(例如 //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 上通过。


Proxy

来源:https://cs162.org/static/hw/hw-http/docs/tasks/proxy/

实现 handle_proxy_request,将 HTTP requests proxy 到另一台 HTTP server。连接建立相关代码我们已经为你处理好了。你应阅读并理解它,但不必修改。

以下是已经实现的部分。

  • 我们使用 --proxy command line argument 的值,其中包含 upstream HTTP server 的 address 与 port 号。这两个值分别存在全局变量 server_proxy_hostnameserver_proxy_port 中。
  • 我们对 server proxy hostname 做 DNS lookup,以查找该 hostname 的 IP address(参见 gethostbyname)。
  • 我们创建 network socket,并将其 connect 到从 DNS 得到的 IP address。参见 socketconnect
  • 使用 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。
    • 不要使用 selectfcntl 之类。以前学期我们曾推荐过这种方法,但我们发现它太容易令人困惑。
  • 若任一 socket 关闭,通信无法继续,因此你应关闭两个 sockets 以终止该 connection。

完成此部分后,所有 Proxy 测试应在 autograder 上通过。


Servers

来源:https://cs162.org/static/hw/hw-http/docs/tasks/servers/

在本节中,你将实现几种不同的 servers。借助条件编译预处理指令(即 #ifdef directives),我们只需改变在这些不同 servers 中如何调用 request handler。

Fork server

实现 forkserver。你不需要写太多新代码。

  • child process 应使用 client socket fd 调用 request_handler。发送完 response 后,child process 将终止。
  • parent process 将继续监听并 accept incoming connections。它不会 wait child。
  • 记住在 parent 与 child process 中都要适当地关闭 sockets。

Thread server

实现 threadserver

  • 创建新的 pthread 向 client 发送正确的 response。
  • 原来的 thread 继续监听并 accept incoming connections。它不会 join 那个新 thread。

Pool server

实现 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 descriptors wq_push 到声明在 httpserver.c 顶部、定义在 wq.h 中的 wq_t work_queue
    • 然后,thread pool 中的 threads 应使用 wq_pop 获取下一个要处理的 client socket file descriptor。
  • 你需要让 server spawn --num-threads 个新 threads,它们在 loop 中循环执行以下操作:
    • 对下一个 client socket file descriptor 做阻塞的 wq_pop 调用。
    • 成功 pop 出一个待服务的 client socket fd 后,调用适当的 request handler 处理该 client request。

Performance

来源:https://cs162.org/static/hw/hw-http/docs/tasks/performance/

测试、测量并评论 httpserverforkserverthreadserverpoolserver 的 performance。我们将使用 Apache HTTP server benchmarking tool(简称 ab)对每种 server 类型做 load test。你可以用 sudo apt-get install -y apache2-utils 安装 ab

  1. 运行 ./httpserver --files www/
  2. 在另一个 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 时它如何变化。

  1. abforkserverthreadserverpoolserver 做 load-test。也可以调整 nc 变量,以及 poolserver 中 thread pool 的大小。

你不再需要在 Gradescope 上回答这些问题,但请测试并思考以下问题!

  1. httpserver 运行 ab。当 nc 变大时会发生什么?
  2. forkserver 运行 ab。当 nc 变大时会发生什么?将这些结果与上一题的答案比较。
  3. threadserver 运行 ab。当 nc 变大时会发生什么?将这些结果与前面问题的答案比较。
  4. poolserver 运行 ab。当 nc 变大时会发生什么?将这些结果与前面问题的答案比较。

Submission

来源: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。