跳到主要内容

Lab8 实现一个轻量级的WEB服务器

实验资源

已超过建议完成时间,请务必在2024-12-29前提交该实验报告与数据

提示

以下指导仅供参考,除实验报告中具体要求外,我们对具体实现方式没有要求,你可以自由设计与开发,文档内测试仅供你逐步验证实现正确性,评分以报告+代码为准,对于感觉较简单的步骤可以跳过相应测试

快速完整测试

http://

0 实验目的与意义

  • 理解HTTP协议及Web服务器工作原理:通过构建一个轻量级Web服务器,加深对HTTP请求与响应的理解,掌握Web服务器在网络中如何处理客户端请求的基本原理
  • 增强网络应用开发能力:动手实现HTTP协议的基本功能,掌握服务器资源管理与访问控制的技术,提升网络应用开发能力
  • 培养工程实践与调试能力:通过实验中的各个步骤,锻炼代码调试、日志分析以及利用工具进行性能测试的能力,培养开发中的工程化思维
  • 理解Web安全性与最佳实践:通过设计并实现资源的安全访问机制,了解如何防范常见的安全隐患,提高Web服务器的安全性和可靠性

1 初识HTTP——概念浅析

HTTP,即超文本传输协议(Hyper Text Transfer Protocol),定义了客户端(通常是Web浏览器)与服务器之间请求和响应的格式。HTTP的简单与强大使其成为了互联网上应用最广泛的网络协议之一,无论是访问网页,还是在线APP加载,都有HTTP的身影

过去三十年间,为了适应通讯与计算技术的迅猛发展,万维网联盟(W3C)和互联网工程任务组(IETF)也在对HTTP进行持续的迭代,目前最新的HTTP版本是HTTP/3.0;在实验中,你只需要实现HTTP/1.0版本的部分基本特性即可,其他HTTP版本的特性仅供你延伸学习,我们的理论课程也会覆盖这部分内容

HTTP/1.0

为了满足多媒体等日渐丰富的需求,增加了对多种请求方法的支持,如GET、POST和HEAD 引入了请求头和响应头,使客户端和服务器间通信更灵活 引入状态码,允许更复杂的错误处理和报告

对于各个版本的HTTP协议而言,我们使用浏览器用户访问网页的一般流程是:

  • DNS解析[本实验不涉及]:当用户在浏览器地址栏输入一个URL时,浏览器首先需要通过DNS解析得到相应服务器IP地址
  • 建立连接:浏览器根据获得的IP地址和指定的端口号(默认HTTP端口为80,HTTPS端口为443),通过TCP/IP协议(HTTP/3改用QUIC协议)与服务器建立连接
  • 浏览器发送请求:连接建立后,浏览器会构建HTTP请求报文,通过已建立的连接发送给服务器
  • 服务器处理请求:服务器接收到请求后,根据请求内容执行相应业务逻辑,如查询数据库、处理表单提交、生成动态页面等,处理完成后构建HTTP响应报文并通过连接返回客户端
  • 关闭连接:对于HTTP/1.0,每次请求后连接立即关闭,对于HTTP/1.1及后续版本,连接可以保持开启状态,以供后续请求复用,直到所有资源请求完成或连接达到预设的超时时间

以HTML为基础的网页是Web服务最基础的应用场景,而HTML网页除了本身是一个资源外,也包含了对其他资源(HTML、CSS、JavaScript、图片等)的链接与使用,为了保证网页的正确展示,仅仅请求HTML网页是不够的,浏览器接收到HTML文档后,会立即解析其中的资源链接(如CSS、JavaScript文件和图片等),并发起相应新的HTTP请求来获取这些资源,

HTTP_persistent_connection

对于不同的HTTP协议版本,这一过程的方式会有一些区别:HTTP/1.0每次请求都需要重新建立TCP连接;HTTP/1.1通过持久连接和请求管道化技术,允许在一个TCP连接上发送多对请求-响应,减少了连接建立的时间开销;HTTP/2在此基础上引入了多路复用技术,使得多个请求和响应可以在同一连接上任意交错传输,不需要等待前一对响应传输完成

动手试1 观察加载网页过程中的HTTP请求与响应

参考开发人员工具使用说明打开开发人员工具-网络,刷新页面,观察网络活动列表中的变化;从中选中一个网络活动,查看其请求/响应的版本等信息

查看加载网页使用的HTTP版本

为了方便你上手了解Web服务器的基本运行方式,我们不妨再实现一个“朴素”的Web服务端(相信我,真的很简单),在本步骤中,我们的Web服务端将会支持以下能力:

  • 接受多个客户端的并发请求
  • 与客户端建立连接后,返回Hello World消息并结束连接

是不是和我们在Lab7最开始的要求非常相似?没错,由于HTTP/2及以前的版本中整个通信过程都基于TCP协议,我们可以充分利用Lab7 Socket编程的余热,只消在其服务端代码的基础上简单修改一下,就能立刻搭建起一个简单的“Hello World”Web服务器

具体来说,此时的客户端由我们的浏览器充当,而服务端则需要简单调整连接处理线程的实现,在建立Socket连接后,服务器固定返回一段符合HTTP响应格式的数据包,然后关闭连接即可

// retrive request and dispach tasks
void connectionHandler(int socket) {
while (!shouldExit) {
// construct and provide response
string response = "HTTP/1.0 200\r\nContent-Length: 12\r\n\r\nHello World!";
// send response & close the connection
}
}

测试1 测试Hello World Web服务器

在浏览器内输入127.0.0.1:[你学号的后4位,首位为0则在前面补1],观察浏览器是否显示了“Hello World”,如果你的实现正确,浏览器将会显示“Hello World!”

2 让我们说HTTP🔊——HTTP协议格式解析

HTTP协议格式

Lab7的实验中我们已经亲自进行了通信协议的设计和实现,不知道你感觉如何?相比我们Socket服务端所使用的协议,Web服务几十年持续演进中扩展的丰富功能显然对协议的表达能力与灵活性提出了极高的要求,而HTTP协议不负众望地满足了用户与企业的这些需要,不仅为我们的日常网页浏览提供了坚实的基础,也在许多场景被用作通用的通讯协议

本章节中,我们将带你了解HTTP协议的整体结构,为接下来实现解析与组装做好准备

2.1 HTTP请求

  • 请求行:请求方法 URI HTTP版本
  • 请求头:以key: value键值对的形式,描述请求的属性,键值对以CRLF(\r\n)结尾
  • 空行:使用一个CRLF表示报头结束,接下来是正文内容(这个CRLF不是最后一个头字段末尾的CRLF)
  • 请求正文:请求相关的信息和数据,正文可以为空,如果存在请求正文,则请求头会使用一个Content-Length属性标记正文长度

2.2 HTTP响应

  • 状态行:HTTP版本 状态码 状态码描述
  • 响应头:以key: value键值对的形式,描述响应的属性,键值对以CRLF(\r\n)结尾
  • 空行:使用一个CRLF表示报头结束,接下来是正文内容(这个CRLF不是最后一个头字段末尾的CRLF)
  • 响应正文:响应的数据,正文可以为空,如果存在正文,则响应头会使用一个Content-Length属性标记正文长度

动手试2 观察一对HTTP请求与响应

你可以打开开发人员工具-网络,刷新一下页面,观察每个请求和响应的情况

一对HTTP请求与响应

可以注意到,HTTP协议很好的定义了每个数据包自身的几个关键信息:我要去哪(请求URI)以怎样的方式(头部属性)做什么(HTTP方法),具体是(正文),而使用CRLF分隔各部分及字段的设计不仅使其能够轻松进行扩展,也让我们可以轻松地实现对HTTP请求与响应的解析与组装

对于接收到的HTTP请求包,首个CRLF前的是请求行,连续两个CRLF后的是请求正文(可能不存在),二者之间的则是请求头,你可以通过一些C++的字符串操作,从请求数据包中将这几部分剥离开来

void getHandler(info, pkt) {}
void postHandler(info, pkt) {}
...

// retrive request and dispach tasks
void connectionHandler(int socket) {
while (!shouldExit) {
// receive full HTTP request
...
// split request to get each part of HTTP request
...
// construct and provide response
string response = "HTTP/1.0 200 OK\r\nConnection: close\r\n\r\n" + ? + ?;
// send response
// close the connection
}
}

为了检验你对HTTP协议请求的解析是否正确,我们的测试框架将会向你的服务端发送一个完全随机的、无请求正文的HTTP请求,并观察其返回的响应,对于该请求,你需要完成以下任务:

  • 接收完整的请求数据包,并解析请求行、请求头的内容

  • 按照以下的方式,组装响应数据包返回给客户端(测试框架)

    响应字符串 = HTTP/1.0 200 OK\r\nConnection: close\r\n\r\n + 完整请求头 + 完整请求行

    响应字符串开头需要与要求完全匹配,而后半部分拼接的请求头和请求行,你可以自由决定是否要保留包含的CRLF

  • 完成响应发送后,关闭连接

测试2 HTTP请求结构解析

http://

请在下方输入框中,分别填入测试框架运行地址(如127.0.0.1:5000)、你的Web服务器运行的地址(如127.0.0.1:8080),点击发起测试,观察测试用例通过情况

3 什么事要办? —— HTTP方法解析

在Lab7中,我们设计了一套自己的通信协议,尽管在具体实现上可能存在一些差异,但为了指示服务端/客户端完成特定的操作,我们的协议中一定会存在特定的一个部分对数据包的类型进行描述

对于HTTP协议而言,这种对类型的描述叫做“方法”,具体来说,方法是指客户端与服务器之间交互时使用的动词,它们定义了请求的目的和期望的行为

通过这些方法,客户端可以向服务器表达不同的操作意图,如获取资源、提交数据或删除资源等,一般来说,我们最为常用的是GET和POST方法,另一些方法在特定情况下会非常有用

GET方法

用于从服务器请求获取资源,GET方法的请求参数通常作为查询字符串附加在URL之后

需要注意的是,GET请求不适合传输大量数据(URL通常只能承载2048个字符),也不应用于传输敏感信息(可能被记录在服务器日志或浏览器的历史记录中,造成敏感数据泄露)

动手试3 使用不同HTTP方法并观察请求与响应

你可以点击测试按钮,发起相应方法的请求,并在开发人员工具中观察请求与响应情况

你可能已经注意到了——POST方法能携带数据并返回响应,看起来我们提到的其他方法也可以通过POST方法加参数实现。那么,为什么要专门分为这些不同的HTTP方法呢?除了遵守RESTful架构,进行面向资源的标准化设计的目的以外,也有对以下特性的考量:

  • 安全性:相应方法只表示获取资源信息的意图,不包含任何请求副作用的意图,需要注意的是这并不是一种保障,而是责任的划分,使用安全方法的用户不应当为其副作用承担责任

  • 幂等性:无错误等意外情况时,多次相同请求无副作用/和单次请求的相同,如,删除某文件的操作无论请求多少次,该文件在服务器都处于删除状态,没有生成新文件等诡异的额外副作用;不甚可靠的网络环境下请求可能会因网络延迟、重试机制等原因而被重复发送,幂等性能确保数据的一致性和可预测性

  • 缓存:缓存机制允许将响应结果存储在客户端或其他中间节点(如CDN),以便后续请求可以直接使用缓存的数据,而无需再次向服务器发起请求,提升用户体验和系统性能;这不仅减少了服务器的负载,还提高了响应速度,如,GET 请求通常被认为是可缓存的,因为它们主要用于获取资源,且不会改变服务器状态

属性GETPOSTOPTIONSHEADPUTDELETEPATCH
请求体可选通常可选可选可选
响应体 ×
安全性 × × × ×
幂等性 × ×
可缓存有时 × × × ×

我们的实验只需要你支持GET和POST两个方法;在前序步骤中,你已经成功解析得到了请求行,相信对请求行的进一步解析对你而言也会非常简单

和Lab7中的实现相似,我们可以根据请求行中的方法,将请求数据包分发到对应的处理函数中,完成相应的业务逻辑,随后将返回的响应数据发送给相应的客户端

void getHandler(info, pkt) {}
...

void connectionHandler(int socket) {
while (!shouldExit) {
// receive full HTTP request, split request to get each part of HTTP request
// extract HTTP methods
// handle request according to method, construct response
if (method == "GET") getHandler(info, pkt);
...
// return response, close the connection
}
}

4 事情怎样办?——HTTP头字段解析

HTTP方法能够有效地表示请求的类型,然而,客户端与服务端双方的细节需求难以用具体的方法完全表达,对于请求本身及其携带的数据,我们也需要更多的属性信息才能高效地解析与应用

本章节中,我们将带你逐步了解一些常见的HTTP头字段,从而对HTTP请求与响应更好地解读与解析

提示

以下内容供你学习参考,实验中,你只需要支持必需的Content-LengthContent-Type字段即可,其他字段可以忽略

对于HTTP协议而言,请求/响应自身的属性表达通常是通过头字段实现的,HTTP的头字段由键值对(key: value)组成,名称不区分大小写,值区分大小写,键值对之间使用CRLF分隔,一些头字段只能用于请求头或响应头,而另一些头字段则在请求头和响应头均可使用,具体来说,头字段可以描述的属性包括但不限于:

  • 资源属性 如资源的类型、编码方式、最后修改时间等,帮助接收方正确处理接收到的数据

  • 缓存属性 指示资源是否能/如何被缓存,从而提高网络性能,优化用户体验

  • 安全/隐私属性 如要求身份验证、限制跨域请求等,用于增强用户的安全性和隐私保护

  • 机制协商 如接收方的内容类型偏好或处理能力,以便发出方选择最适合的内容进行响应

动手试4 分析一个HTTP请求的属性

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.7
accept-encoding: gzip, deflate, br, zstd
accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
cache-control: max-age=0
cookie: cf_clearance=tKTgM....bR5B9S4QIw
if-modified-since: Thu, 21 Nov 2024 06:48:58 GMT
priority: u=0, i
sec-ch-ua: "Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: same-origin
sec-fetch-user: ?1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0

在前序步骤中,我们已经将HTTP请求的各部分进行了分离,得到了完全由头字段组成的请求头,由于头字段使用CRLF分隔,你可以使用algorithm头文件提供的find逐个找到起始位置,并依次读入处理(键值对的切分与之类似),得到全部头字段

完成对请求行、请求头、请求正文的处理后,我们就基本完成了对HTTP请求的解析,与我们在Socket实验中的建议类似,你可以编写HTTPRequest/HTTPResponse两个类,用于结构化地存储请求与响应数据包,并实现相应的序列化与反序列化操作

class HTTPRequest {
public:
HTTPRequest(std::string& is); // Ctor - Parse input string & construct
// Getters - Retrive info from object ...
const std::string& operator[](const std::string& key) const { } // Optional - Reload [] for easier header fields access ?
private: // Necessary data elements
};
class HTTPResponse {
public:
HTTPResponse(const std::string& version, int code, const std::string& reasonPhrase);
// Setters - modify response ...
std::string serialize() const; // Serialize to bytestream for send
std::string& operator[](const std::string& key) { } // Optional - Reload [] for easier header fields access & modification ?
private: // Necessary data elements
};

通过剥离功能上低相关度的字符串处理代码,我们可以使连接处理函数的逻辑更加清晰:

void getHandler(info, response) { /* modify response obj based on info */ }
...
// retrive request and dispach tasks
void connectionHandler(int socket) {
while (!shouldExit) {
// receive & parse full HTTP request
HTTPRequest request(receivedMsg);
HTTPResponse response; // handle request according to method, construct response
if (request.getMethod() == "GET") getHandler(info, response);
...
// return response & close the connection
}
}

为了检验你对头字段的解析,我们不妨实现一个简单的WebEcho功能,具体步骤如下:

  • 解析头字段,得到Content-Length字段的值,并根据该值读入相应长度的正文(保证由ASCII字母+数字组成)
  • 组装响应数据包,至少要包含Content-LengthContent-Type字段,且相应字段的值需要正确设置;响应正文为解析到的请求正文的完整内容
  • 返回响应数据包并关闭连接

我们的测试服务对你的WebEcho功能有以下几点要求:

  • 测试服务可能会随机添加各种头字段,对于不要求支持的字段,你的程序应当能正确处理(直接忽略相应字段提出的要求),并正常返回响应
  • 头字段名称不区分大小写,请求中的名称采用完全随机的大小写,请确保你的程序均能够正常处理
  • 测试服务会生成随机长度的请求正文,你返回的Content-LengthContent-Type字段及响应正文内容需要与其完全匹配

测试3 服务器的WebEcho功能

http://

5 东西怎么找&送?——资源访问处理与响应

5.1 URI解析与映射

Lab7中,我们如果要向特定的主机发送消息,就需要在源客户端选择某个与其他客户端唯一关联的标识(如:序号/句柄等),并用这种标识指示服务端消息要转发到的目标主机,想一想,如果服务器上有多个连接的客户端对应同一个标识,我们的服务器还能正确地完成消息转发的任务吗?

随着互联网的发展,人类每天在互联网上产生的数据体量也在急速膨胀,面对浩如烟海的资源,如果没有合理的方式对其进行标记,那么类似这样的问题必然会更加严重,让我们的组织、管理和访问陷入无异于大海捞针的境地

在这个背景下,统一资源标识符URI(Uniform Resource Identifier)应运而生,它为互联网上的资源提供了一种标准化的命名和定位机制,极大地便利了资源的发现与访问过程

URI是一种用于唯一地标识互联网上资源的字符串,它可以指向任何类型的资源,包括文档、图片、视频流、服务入口点等,URI的设计目的是为了确保每个资源在全球范围内都能被唯一识别,根据其功能和结构的不同,URI中可以进一步划分为URL和URN两种类型:

统一资源定位符URL(Uniform Resource Locator)

最常见的URI形式,提供了访问特定资源的路径和方法,URL不仅告诉计算机资源是什么,更重要的是说明了如何找到并获取这个资源,一个典型的URL结构是:scheme://host[:port]/path?query#fragment

  • scheme:指定访问资源时使用的协议类型,如HTTPHTTPSFTP
  • host:资源所在的主机名或IP地址
  • port:可选字段,指定主机上的端口号,默认情况下,不同的协议会使用不同的端口,例如HTTP默认使用80端口,HTTPS使用443端口
  • path:资源在服务器上的具体位置,如path/to/file
  • query:可选字段,用于传递给服务器的查询参数,通常以键值对的形式出现,如key1=value1&key2=value2
  • fragment:可选字段,用于指示页面内部的一个特定部分或元素,通常用于页面内的导航,比如通过https://zjucomp.net/docs/Lab8_page#51-uri解析与映射 访问本文档时,可以直接跳转到第5部分

URL又可以分为绝对URL和相对URL,这和绝对路径/相对路径的差别非常相似,绝对URL指向特定主机上一个特定的资源,而相对URL对于不同的主机可能对应不同的资源,我们在解析请求行中的URI时,实际上得到的是一个相对URL

统一资源名称URN(Uniform Resource Name)

URN更关注于资源的身份标识而非物理位置,旨在提供一种持久不变的名字空间,即使资源的实际位置发生变化,其URN仍然保持不变,它的格式通常为:<nns>:<specifics>

  • nns:命名空间标识符,用于定义URN所属的命名空间
  • specifics:命名空间内的具体标识符,用于唯一确定资源

你可能会问,既然得到的相对URL和相对路径这么相似,那么我们可不可以直接把资源和服务端程序的相对路径当作相对URL用来请求对应的资源呢?答案是“可以,但最好不要”

尽管这样的命名方式非常简单直接,可以极大降低提供资源访问的难度,但这同时也带来了安全风险,通过对服务器的爆破扫描,脚本小子们可以摸索出服务器上的文件结构,从而针对性地选择潜在漏洞进行渗透,带来较大的安全风险,因此,为了保护服务器的安全,避免泄露不必要的信息,通常不建议直接将服务端程序的相对路径作为相对URL来使用

小心!这样的攻击远比你想象中更常见

自动化的漏洞扫描非常简易、低成本,而攻破后加密勒索的收益又相当高,因此全球范围内这样的攻击相当普遍,如果你准备在公网提供Web服务,尤其是存在CDN/OSS的使用时,请务必谨慎配置安全策略,以免因为勒索/服务商账单而不幸“破产”

试探扫描示例

实际上,我们的实验文档网站每天都在受到这样的爆破,高峰时2k请求/小时(很难理解爆破Cloudflare和Github Pages且被安全策略屏蔽后还在锲而不舍地扫描是怎样一种心态( ̄_ ̄|||))

正确的做法是通过配置服务器或应用程序来映射URL到内部路径,同时确保敏感文件和目录不受未授权访问的影响

实验报告中要求你准备了一些文件,我们将其打包好提供给你,你可以下载解压后直接放置于与服务端可执行文件同一目录下,用于接下来的测试

实验测试素材
44.2KB

为了满足自动化测试的需求,请你按照以下表格中的映射关系,对相应资源URL进行映射,我们测试的请求将使用映射后的URL

文件描述文件路径映射后URL
带有图片的首页HTML文件/html/test.html/index.html
去掉图片的首页HTML文件/html/noimg.html/index_noimg.html
纯文本文件/txt/test.txt/info/server
浙大校标图片文件/img/logo.jpg/assets/logo.jpg

你的服务端应当能够通过一定的方式,从请求行中解析出映射后URL,并反向映射回相应的文件路径

5.2 状态码处理

响应请求的过程往往并不是一帆风顺,来自请求方、响应方及中间节点的问题都可能阻碍响应的顺利完成,为了将这些问题直观简洁地反馈给客户端,HTTP协议设计了一些状态码,并在其后附加一个原因短语reason-phrase,描述响应的结果

一般来说,响应码首位表示一种通用分类:1——提供信息;2——成功;3——重定向;4——客户端错误;5——服务器错误,常见的HTTP响应码可以参考下表:

响应码
原因短语 Reason-Phrase描述
100Continue请求正在进行中
200OK请求成功
202Accepted请求已被接受且正在处理中,但尚未完成
301Moving Permanently资源有个新地址
302Moving Temporarily资源有个新的临时地址
400Bad Request服务器不认可这个请求
401Unauthorized授权失败
404Not Found所请求的资源不存在
406Not Acceptable内容将不被浏览器所接受
500Internal Server Error服务器遭遇错误
503Service Unavailable服务器过载或不工作

关于响应码的详细描述,你可以参考MDN的这份文档:

avatar
HTTP response status codes
HTTP response status codes indicate whether a specific HTTP request has been successfully completed.

实验中,你需要对请求URL进行验证,如果解析得到的是GET方法,则验证相应文件是否存在;如果解析得到的是POST方法,则验证接口URI是否为/dopost,如不匹配,则返回404 Not Found,否则均返回200 OK

测试4 资源URI映射

http://

我们的测试框架将会对给定URL检验映射的正确性,具体来说,对于GET方法的请求,请你检验URL合法性,根据请求URL映射得到相应的文件路径,并将相应的文件路径的字符串(不是文件本身)返回给客户端,请求头需正确设置Content-Type字段(字符串长度)和Content-Type字段(text/plain)的值

例:GET /index.html HTTP/1.0的请求,响应正文应当为/html/test.htmlGET /what.png HTTP/1.0的请求,应返回404 Not Found,响应正文为空

6 具体功能实现

6.1 文件资源访问

对于正确的资源请求,我们需要返回目标资源,同时设置响应头,以指示客户端如何处理该资源,具体来说,分为以下几步:

  • 资源映射:将请求的URI映射到服务器本地的文件系统路径上,以便定位请求的资源文件
  • 文件读取:使用文件I/O操作读取指定路径上的资源文件,如果文件存在,则将其内容读取到内存中,以便后续进行响应
  • 构建HTTP响应:根据读取到的文件内容构建HTTP响应报:文件字节流完整置于响应正文中;根据文件类型,设置响应头Content-Type,以指示客户端如何处理该资源,例如文本文件应设置Content-Type: text/plain,图片文件应设置Content-Type: image/jpeg等;响应头Content-Length设置为文件大小(以字节为单位)

测试5 资源访问

http://

我们的测试框架将会验证是否能从指定URI访问到相应文件,并对文件完整性、响应头正确性进行验证

注意

请你在Web服务器访问的/txt/test.txt文件、测试框架访问的/txt/test.txt文件中留空位置均填入英文姓名、学号,修改需保持一致,否则无法通过测试

如果你想返回包含中文的内容,可能需要注意处理编码问题/添加额外的头字段信息

根据实验报告要求,运行服务器后,请用netstat -an显示服务器的监听端口,并截图记录,如果项目过多,可通过netstat -an | grep [端口号]进行过滤

启用Wireshark捕获,在浏览器中,分别访问你的Web服务器IP:端口/index.html、IP:端口/index_noimg.html、IP:端口/info/server,并记录以下内容:

  • 页面上显示内容截图
  • Wireshark的TCP流追踪(每个页面分别截图、HTML文件和图片文件分别截图)
  • Wireshark的请求/响应包(每个页面分别截图、HTML文件和图片文件分别截图)

6.2 登录请求处理

登录功能需要实现一个简单的POST请求处理,用户通过一个表单向/dopost接口URI提交用户名和密码,你的服务器接收请求并验证信息是否正确,具体步骤如下:

  • 解析POST请求:从POST请求中解析出请求路径和请求体的内容,验证请求路径是否为/dopost,如果不匹配,则返回404 Not Found,响应正文为空
  • 验证登录信息:从请求正文中,解析用户名login和密码pass的值,与你在代码中预设的值进行比对验证,你可以将用户名和密码硬编码在服务器代码中,或者通过配置文件进行模拟
  • 构建响应:响应正文为<html><body>响应消息</body></html> ,根据验证结果,设置响应消息为“Login Success”或“Login Failed”
如果你想返回包含中文的内容,可能需要注意处理编码问题/添加额外的头字段信息

测试6 用户登录测试

启用Wireshark捕获,在浏览器中,访问你的Web服务器IP:端口/index.html,在页面中分别输入正确/错误的账号密码,并记录以下内容:

  • 页面上显示的提示信息截图
  • Wireshark的TCP流追踪(正确/错误情况分别截图)
  • Wireshark的请求/响应包(正确/错误情况分别截图)

测试7 多线程访问测试

打开多个浏览器窗口,同时访问包含图片的HTML文件(Web服务器IP:端口/index.html),并截图浏览器显示内容、TCP链接情况(命令同测试5,截取与监听端口相关的)