概述

在本作业中,您将开发一个并发 Web 服务器。 为了简化这个项目,我们为您提供了一个非并发(但工作)Web 服务器的代码。 这个基本的 Web 服务器仅使用一个线程运行; 您的工作是使 Web 服务器成为多线程的,以便它可以同时处理多个请求。
这个项目的目标是:

  • 学习一个简单的网络服务器的基本架构
  • 了解如何向非并发系统添加并发
  • 学习如何有效地阅读和修改现有的代码库

    HTTP背景 HTTP Background

    在描述您将在本项目中实现的内容之前,我们将简要概述经典 Web 服务器的工作原理,以及用于与其通信的 HTTP 协议(1.0 版); 尽管 Web 浏览器和服务器这些年来已经有了很大的发展,但旧版本仍然有效,并为您了解事物的工作原理提供了一个良好的开端。 我们为您提供一个基本的 Web 服务器的目标是,您可以免于学习网络连接的所有细节和完成项目所需的 HTTP 协议; 然而,网络代码已经大大简化,如果你选择学习它是相当容易理解的。
    经典 Web 浏览器和 Web 服务器使用称为 HTTP(超文本传输协议(Hypertext Transfer Protocol))的基于文本的协议进行交互。 Web 浏览器打开与 Web 服务器的连接并使用 HTTP 请求一些内容。 Web 服务器响应请求的内容并关闭连接。 浏览器读取内容并将其显示在屏幕上。
    HTTP 建立在操作系统提供的 TCP/IP 协议套件之上。 TCP 和 IP 共同确保消息路由到正确的目的地,在遇到故障时从源可靠地到达目的地,并且不会因为一次发送太多消息而导致网络过度拥塞,以及其他特性。 要了解有关网络的更多信息,请参加网络课程(或多门课程!),或阅读这本免费书籍
    Web 服务器上的每条内容都与服务器文件系统中的一个文件相关联。 最简单的是静态内容,其中客户端发送请求只是为了从服务器读取特定文件。 稍微复杂一点的是动态内容,其中客户端请求在 Web 服务器上运行可执行文件并将其输出返回给客户端。 每个文件都有一个唯一的名称,称为 URL(统一资源定位器(Universal Resource Locator))。
    作为一个简单的例子,假设客户端浏览器想要从运行在某台机器上的 Web 服务器获取静态内容(即,只是一些文件)。 然后客户端可能会在浏览器中输入以下 URL:http://www.cs.wisc.edu/index.html。 此 URL 标识将使用 HTTP 协议,并且应获取主机 www.cs.wisc.edu 上名为 index.html 的 Web 服务器根目录 (/) 中的 HTML 文件。
    Web 服务器不仅由其运行的机器唯一标识,还由它侦听连接的端口(port)标识。 端口是一种通信抽象,允许在一台机器上同时发生多个(可能是独立的)网络通信; 例如,Web 服务器可能在端口 80 上接收 HTTP 请求,而邮件服务器使用端口 25 发送电子邮件。默认情况下,Web 服务器应该在端口 80(众所周知的 HTTP 端口号)上运行,但是 有时(如在本项目中),将使用不同的端口号。 要从运行在不同端口号(例如 8000)上的 Web 服务器获取文件,请直接在 URL 中指定端口号,例如 http://www.cs.wisc.edu:8000/index.html
    可执行文件(即动态内容)的 URL 可以在文件名后包含程序参数。 例如,要仅运行不带任何参数的程序 (test.cgi),客户端可能会使用 URL http://www.cs.wisc.edu/test.cgi。 要指定更多参数,? 和 & 字符被使用,带有 ? 字符将文件名与参数分开,& 字符将每个参数与其他参数分开。 例如,http://www.cs.wisc.edu/test.cgi?x=10&y=20` 可用于向程序 test.cgi 发送多个参数 x 和 y 以及它们各自的值。 正在运行的程序称为 CGI 程序(Common Gateway Interface 的缩写;是的,这是一个可怕的名字); 参数作为 QUERY_STRING 环境变量的一部分传递到程序中,然后程序可以对其进行解析以访问这些参数。

    HTTP请求 The HTTP Request

    当客户端(例如浏览器)想要从机器上获取文件时,该过程首先向机器发送消息。 但该消息的正文究竟是什么? 这些请求内容,以及后续的回复内容,都是由 HTTP 协议精确指定的。
    让我们从从 Web 浏览器发送到服务器的请求内容开始。 此 HTTP 请求包含一个请求行,后跟零个或多个请求标头,最后是一个空文本行。 请求行具有以下形式:method uri version。 方法(method)通常是GET,它告诉Web服务器客户端只是想读取指定的文件; 但是,存在其他方法(例如,POST)。 uri 是文件名,也可能是可选参数(在动态内容的情况下)。 最后,版本(version)指示 Web 客户端使用的 HTTP 协议的版本(例如,HTTP/1.0)。
    HTTP 响应(从服务器到浏览器)类似;它由一个响应行、零个或多个响应标头、一个空文本行,最后是有趣的部分,响应正文组成。响应行包含表单版本状态消息(status message)。status 是一个三位数的正整数,表示请求的状态;一些常见的状态是 200 表示正常,403 表示禁止(即客户端无法访问该文件),以及 404 表示未找到文件(著名的错误)。标头(header)中的两行重要的行是 Content-Type,它告诉客户端响应正文中的内容类型(例如,HTML 或 gif 或其他)和 Content-Length,它指示文件大小(以字节为单位)。
    对于这个项目,除非你想了解我们给你的代码的细节,否则你真的不需要知道这些关于 HTTP 的信息。您无需修改 Web 服务器中处理 HTTP 协议或网络连接的任何过程。然而,多学点总是好的,不是吗?

    一个基础的Web服务器 A Basic Web Server

    Web 服务器的代码在此存储库(repository)中可用。您可以通过简单地输入 make 来编译这里的文件。在对它进行任何更改之前编译并运行这个基础的 Web 服务器!make clean 删除 .o 文件和可执行文件,并让您进行干净的构建。
    当您运行这个基础的 Web 服务器时,您需要指定它将侦听的端口号;低于 1024 的端口是保留的(请参阅此处的列表),因此您应该指定大于 1023 的端口号以避免此保留范围;最大值为 65535。请注意:如果在共享机器上运行,您可能会与其他机器发生冲突,从而导致您的服务器无法绑定到所需端口。如果发生这种情况,请尝试不同的端口号!
    当您随后将 Web 浏览器连接到此服务器时,请确保您指定了相同的端口。例如,假设您在 bumble21.cs.wisc.edu 上运行并使用端口号 8003;将您的favorite HTML 文件复制到您启动 Web 服务器的目录。然后,要从 Web 浏览器(在相同或不同的机器上运行)查看此文件,请使用 url bumble21.cs.wisc.edu:8003/favorite.html。如果您在同一台机器上运行客户端和 Web 服务器,您可以使用主机名 localhost 为方便起见,例如 localhost:8003/favorite.html。
    为了使项目更容易一些,我们为您提供了一个最小的 Web 服务器,它只包含几百行 C 代码。因此,服务器的功能受到限制;除了 GET 之外,它不处理任何 HTTP 请求,仅理解少数内容类型,并且仅支持 CGI 程序的 QUERY_STRING 环境变量。这个 Web 服务器也不是很健壮;例如,如果 Web 客户端关闭与服务器的连接,它可能会触发服务器中的断言,导致其退出。我们不希望您解决这些问题(尽管您可以,如果您愿意,您知道,为了好玩)。
    提供了帮助函数来简化错误检查。包装器调用所需的函数并在发生错误时立即终止。包装器位于文件 io-helper.h);更多关于这在下面。应该总是检查错误代码,即使你所做的只是退出;默默漏失错误是糟糕的 C 编程(BAD C PROGRAMMING),应该不惜一切代价避免。

    最后:一些新功能 Finally: Some New Functionality!

    在此项目中,您将向基础 Web 服务器添加两个关键功能。首先,您使 Web 服务器成为多线程的。其次,您将实施不同的调度策略,以便以不同的顺序处理请求。您还将修改 Web 服务器的调用方式,以便它可以处理新的输入参数(例如,要创建的线程数)。

    第1部分:多线程 Part 1: Multi-threaded

    我们提供的基础 Web 服务器具有单个控制线程。单线程 Web 服务器存在一个基本的性能问题,因为一次只能处理一个 HTTP 请求。因此,访问此 Web 服务器的所有其他客户端都必须等到当前 http 请求完成;如果当前 HTTP 请求是长时间运行的 CGI 程序或仅驻留在磁盘上(即不在内存中),则这尤其成问题。因此,您将添加的最重要的扩展是使基础 Web 服务器成为多线程的。
    构建多线程服务器的最简单方法是为每个新的 http 请求生成一个新线程。然后操作系统将根据自己的策略调度这些线程。创建这些线程的好处是现在短请求不需要等待长请求完成;此外,当一个线程被阻塞(即等待磁盘 I/O 完成)时,其他线程可以继续处理其他请求。然而,每个请求一个线程的方法的缺点是 Web 服务器承担为每个请求创建一个新线程的开销。
    因此,多线程服务器通常首选的方法是在首次启动 Web 服务器时创建一个固定大小的工作线程池(a fixed-size pool of worker threads)。使用线程池方法,每个线程都会被阻塞,直到有一个 http 请求需要它处理。因此,如果工作线程比活动请求多,那么部分线程会被阻塞,等待新的 HTTP 请求到达;如果请求比工作线程多,那么这些请求将需要被缓冲(be buffered),直到有一个就绪线程。
    在您的实现中,您必须有一个主线程,它首先创建一个工作线程池(a pool of worker threads),其数量在命令行中指定。然后你的主线程负责通过网络接受新的 HTTP 连接,并将这个连接的描述符放入一个固定大小的缓冲区(fixed-size buffer)在您的基本实现中,主线程不应从此连接读取。缓冲区中的元素数量也在命令行中指定。请注意,现有的 Web 服务器有一个接受连接(connection)然后立即处理连接的线程;在您的 Web 服务器中,该线程应该将连接的描述符放入一个固定大小的缓冲区中并返回,以接受更多连接。
    每个工作线程都能够处理静态和动态请求。当队列中有 HTTP 请求时,一个工作线程被唤醒;当有多个 HTTP 请求可用时,处理哪个请求取决于调度策略,如下所述。工作线程唤醒后,对网络描述符执行读取,获取指定的内容(通过读取静态文件或执行 CGI 进程),然后通过写入描述符将内容返回给客户端。然后工作线程等待另一个 HTTP 请求。
    请注意,主线程和工作线程处于生产者-消费者关系中,并且要求它们对共享缓冲区的访问是同步的。具体来说,如果缓冲区已满,主线程必须阻塞并等待;如果缓冲区为空,工作线程必须等待。在这个项目中,你需要使用条件变量。注意:如果您的实现执行任何忙等待(或自旋等待),您将受到严重惩罚
    旁注:不要被我们提供的基础 Web 服务器为它运行的每个 CGI 进程派生一个新进程的事实所迷惑。尽管在非常有限的意义上,Web 服务器确实使用了多个进程,但它一次处理的请求永远不会超过一个;Web 服务器中的父进程在继续并接受更多 HTTP 请求之前明确等待子 CGI 进程完成。在使您的服务器成为多线程时,您不应修改这部分代码。

    第2部分:调度策略 Part 2: Scheduling Policies

    在这个项目中,您将实现许多不同的调度策略。请注意,当您的 Web 服务器运行多个工作线程(在命令行中指定数量)时,您将无法控制操作系统在任何给定时间实际调度的线程。您在调度中的作用是确定 Web 服务器中每个等待的工作线程应该处理哪个 HTTP 请求。
    调度策略由 web 服务器启动时的命令行参数决定,如下所示:

  • 先进先出 (FIFO):当工作线程唤醒时,它处理缓冲区中的第一个请求(即最旧的请求)。请注意,HTTP 请求不一定按 FIFO 顺序完成;请求完成的顺序取决于操作系统如何调度活动线程。

  • 最小文件优先(SFF):当工作线程唤醒时,它处理对最小文件的请求。此策略近似于最短任务优先,因为文件大小可以很好地预测为该请求提供服务所需的时间。对静态和动态内容的请求可能会混合在一起,具体取决于这些文件的大小。请注意,此算法可能导致对大文件的请求不足。您还将注意到 SFF 策略要求在安排请求之前了解每个请求的某些信息(例如文件的大小)。因此,为了支持这种调度策略,您需要在工作线程之外对请求进行一些初始处理(提示:在文件名上使用 stat());您可能希望主线程执行这项工作,这需要它从网络描述符中读取。

    安全 Security

    运行联网服务器可能很危险,尤其是如果您不小心的话。因此,在创建 Web 服务器时,您应该仔细考虑安全性。您应该始终确保做的一件事是不要让您的服务器运行超出测试范围,从而为系统中的文件打开一个潜在的后门。
    您的系统还应确保将文件请求限制在文件系统层次结构的子树中,以服务器启动的基本工作目录为根。您必须采取措施确保传入的路径名不会引用到此子树之外的文件。一种简单(可能过于保守)的方法是拒绝任何包含 .. 的路径名,从而避免在文件系统树上进行任何遍历。更复杂的解决方案可以使用 chroot() 或 Linux 容器,但也许这些超出了项目的范围。

    命令行参数 Command-line Parameters

    您的 C 程序必须完全按如下方式调用:
    prompt> ./wserver [-d basedir] [-p port] [-t threads] [-b buffers] [-s schedalg]
    Web 服务器的命令行参数将按如下方式解释。

  • basedir:这是 Web 服务器应该运行的根目录。服务器应尝试确保文件访问不会访问文件系统层次结构中此目录以上的文件。默认值:当前工作目录(例如,.)。

  • port:Web 服务器应该监听的端口号;基础的 Web 服务器已经处理了这个参数。默认值:10000。
  • 线程:应在 Web 服务器内创建的工作线程数。必须是正整数。默认值:1。
  • buffers:一次可以接受的请求连接数(the number of request connections)。必须是正整数。请注意,创建的线程比缓冲区多或少都不是错误。默认值:1。
  • schedalg:要执行的调度算法。必须是 FIFO 或 SFF 之一。默认值:FIFO。

例如,你可以这样运行你的程序:
prompt> server -d . -p 8003 -t 8 -b 16 -s SFF
在这种情况下,您的 Web 服务器将侦听端口 8003,创建 8 个工作线程来处理 HTTP 请求,为当前正在进行(或等待)的连接分配 16 个缓冲区,并对到达的请求使用 SFF 调度。

源代码总览 Source Code Overview

我们建议您了解我们提供给您的代码是如何工作的。我们提供以下文件:

  • wserver.c:包含用于 Web 服务器和基础服务循环的 main()。
  • request.c:执行基本 Web 服务器中处理请求的大部分工作。从 request_handle() 开始,从那里开始处理逻辑。
  • io_helper.h 和 io_helper.c:包含基本 Web 服务器和客户端调用的系统调用的封装函数。约定是将 _or_die 添加到现有调用以提供成功或退出的版本。例如,open() 系统调用用于打开文件,但由于多种原因可能会失败。包装器 open_or_die() 要么成功打开文件,要么在失败时退出。
  • wclient.c:包含 main() 和非常简单的 Web 客户端的支持例程。要测试您的服务器,您可能需要更改此代码,以便它可以同时向您的服务器发送请求。通过多次启动 wclient,您可以测试您的服务器如何处理并发请求。
  • spin.c:一个简单的 CGI 程序。大体上,它会自旋固定的时间,这可能有助于测试服务器的各个方面。
  • Makefile:我们还为您提供了一个示例 Makefile,用于创建 wserver、wclient 和 spin.cgi。您可以键入 make 来创建所有这些程序。您可以键入 make clean 以删除目标文件和可执行文件。您可以键入 make server 来创建服务器程序等。当您创建新文件时,您需要将它们添加到 Makefile 中。

了解代码的最好方法是编译并运行它。使用您首选的 Web 浏览器运行我们为您提供的服务器。使用我们提供给您的客户端代码运行此服务器。您甚至可以让我们提供给您的客户端代码联系任何其他使用 HTTP 的服务器。对服务器代码做一些小的改动(例如,让它打印出更多的调试信息),看看你是否理解它是如何工作的。

其他有用的阅读材料 Additional Useful Reading

我们预计您会发现以下对创建和同步线程有用的例程:pthread_create()、pthread_mutex_init()、pthread_mutex_lock()、pthread_mutex_unlock()、pthread_cond_init()、pthread_cond_wait()、pthread_cond_signal()。要查找有关这些库例程的信息,请阅读手册页 (RTFM)。

实验记录

一些函数和数据结构的用法

getopt()

参考:https://www.cnblogs.com/qingergege/p/5914218.html
原型:int getopt(int argc,char const argv[ ],const char optstring);
前两个是main函数的两个参数,第三个参数是个字符串,我们可以叫他选项字符串。
返回值为int类型,我们都知道char类型是可以转换成int类型的,每个字符都有他所对应的整型值,其实这个返回值返回的就是一个字符,什么字符呢,叫选项字符。
选项字符串:例如,”a:b:cd::e”,这就是一个选项字符串。对应到命令行就是-a ,-b ,-c ,-d, -e 。冒号又是什么呢? 冒号表示参数,一个冒号就表示这个选项后面必须带有参数(没有带参数会报错哦),但是这个参数可以和选项连在一起写,也可以用空格隔开,比如-a123 和-a 123(中间有空格) 都表示123是-a的参数;两个冒号的就表示这个选项的参数是可选的,即可以有参数,也可以没有参数,但要注意有参数时,参数与选项之间不能有空格(有空格会报错的哦),这一点和一个冒号时是有区别的。
extern char* optarg; 用来保存选项的参数。
extern int optind; 用来记录下一个检索位置。
extern int opterr; 表示的是是否将错误信息输出到stderr,为0时表示不输出。
extern int optopt; 表示不在选项字符串optstring中的选项。

gethostname()

include
int gethostname(char *name, size_t len);
参数说明:
这个函数需要两个参数:
接收缓冲区name,其长度必须为len字节或是更长,存获得的主机名。
接收缓冲区name的最大长度
返回值:
如果函数成功,则返回0。如果发生错误则返回-1。错误号存放在外部变量errno中。

sscanf()

从一个字符串中读进与指定格式相符的数据。
int sscanf( string str, string fmt, mixed var1, mixed var2 … );
int scanf( const char format [,argument]… );
说明:
sscanf与scanf类似,都是用于输入的,只是后者以屏幕(stdin)为输入源,前者以固定字符串为输入源。
其中的format可以是一个或多个 {%[
] [width] [{h | l | I64 | L}]type | ‘ ‘ | ‘\t’ | ‘\n’ | 非%符号}

strcasecmp()

表头文件 #include (不是C/C++的标准头文件,区别于string.h )
定义函数 int strcasecmp (const char s1, const char s2);
函数说明 strcasecmp()用来比较参数s1和s2字符串,比较时会自动忽略大小写的差异。
返回值 若参数s1和s2字符串相等则返回0。s1大于s2则返回大于0 的值,s1 小于s2 则返回小于0的值。

stat()

表头文件: #include
#include
定义函数: int stat(const char file_name, struct stat buf);
函数说明: 通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
返回值: 执行成功则返回0,失败返回-1,错误代码存于errno

off_t

用于指示文件的偏移量,常就是long类型,其默认为一个32位的整数,在gcc编译中会被编译为long int类型,在64位的Linux系统中则会被编译为long long int,这是一个64位的整数,其定义在unistd.h头文件中可以查看。

答案

https://github.com/xxyzz/ostep-hw/tree/master/projects/concurrency-webserver