一、协议设计
具体要求:
掌握课本有关 HTTP 的内容;阅读 HTTP/1.1 的标准文档 RFC2616[1];
搭建编程环境(参见“讲解 PPT-环境安装配置.pptx);
熟悉 Socket 编程方法;
掌握 lex 和 yacc[7]正确解析消息(message)的方法;
实现简单的 echo web server。
(一)代码源文件架构
文件目录结构
1 | WEBSERVER |
代码源文件架构分析
- 根目录文件
README.md
:包含项目的基本信息、使用方法和开发者指南。Makefile
:用于编译和链接项目中的各个源文件,自动化构建流程。DockerFile
:用于创建Docker镜像,提供一致的开发和部署环境。
- .vscode目录
- 存放VS Code的配置文件,如任务、调试配置等。
- cgi目录
- 用于存放CGI(Common Gateway Interface)相关的文件,实现动态网页生成。
- include目录
parse.h
:头文件,声明解析器相关的函数和数据结构。
- obj目录
- 存放编译生成的中间目标文件(object files),如
.o
文件。
- 存放编译生成的中间目标文件(object files),如
- samples目录
- 存放示例代码或测试样例。
- src目录
- 源文件
echo_client.c
:实现客户端代码,发送请求并接收服务器响应。echo_server.c
:实现服务器代码,接收并处理客户端请求,返回响应。example.c
:示例代码,用于展示如何使用某些功能或库。lex.yy.c
:由lexer.l
生成的词法分析器代码。lexer.l
:词法分析器定义文件,定义如何将输入的文本流分解为标记(tokens)。parse.c
:解析器代码,包含语法分析的实现。parser.y
:语法分析器定义文件,使用Yacc或Bison生成解析器。y.tab.c
:由parser.y
生成的语法分析器代码。y.tab.h
:由parser.y
生成的语法分析器头文件,包含语法分析器使用的符号常量。
- 静态站点文件
static_site/
:存放静态网页文件,如HTML、CSS、JavaScript等。
- 源文件
功能模块分析
- 客户端模块(echo_client.c)
- 实现客户端功能,主要负责发送HTTP请求到服务器并接收响应。
- 服务器模块(echo_server.c)
- 实现服务器功能,主要负责接收和解析客户端请求,并返回相应的HTTP响应。
- 支持处理GET, HEAD, POST等HTTP方法,并返回相应的数据。
- 词法分析模块(lexer.l -> lex.yy.c)
- 使用词法分析器将输入的文本流分解为标记,供语法分析器使用。
- 语法分析模块(parser.y -> y.tab.c, y.tab.h)
- 使用语法分析器解析输入的标记序列,生成对应的语法树或执行相应的操作。
- 结合词法分析器,完成对HTTP请求的解析。
- 解析模块(parse.c, parse.h)
- 实现具体的解析逻辑,包括解析HTTP头部和消息体。
- 使用前面生成的词法和语法分析器,对输入消息进行全面解析。
消息解析方法
Lex 和 Yacc 介绍
- Lex(词法分析器生成器):
- 作用:负责将输入的字符流分解成一个个标记(tokens),这些标记是语法分析器(Yacc)能够处理的基本单元。
- 工作原理:
- 定义词法规则:在
.l
文件中定义词法规则,描述不同标记的模式。 - 生成词法分析器:使用Lex工具读取
.l
文件,生成相应的词法分析器代码(如lex.yy.c
)。 - 标记识别:运行词法分析器代码,将输入的字符流转换成标记序列。
- 定义词法规则:在
- Yacc(Yet Another Compiler-Compiler,语法分析器生成器):
- 作用:负责根据词法分析器提供的标记序列,按照预定义的语法规则生成语法树,或执行相应的动作。
- 工作原理:
- 定义语法规则:在
.y
文件中定义语法规则,描述标记的组合方式。 - 生成语法分析器:使用Yacc工具读取
.y
文件,生成相应的语法分析器代码(如y.tab.c
和y.tab.h
)。 - 语法分析:运行语法分析器代码,解析标记序列,生成语法树或执行解析操作。
- 定义语法规则:在
消息解析流程
- 词法分析(Lex):
- 输入:HTTP请求的字符流。
- 输出:标记序列(如方法名、URL、HTTP版本、头部字段名、字段值等)。
- 语法分析(Yacc):
- 输入:Lex生成的标记序列。
- 输出:解析后的语法树,或直接执行相应操作(如存储请求方法、路径、HTTP版本、头部字段等)。
- 请求处理:
- 根据语法分析器解析的结果,处理不同的HTTP方法(GET、HEAD、POST)。
- 对于未实现的方法,返回501 Not Implemented响应。
- 对于格式错误的请求,返回400 Bad Request响应。
Lex 和 Yacc 工作流程图
1 | +--------------------------+ |
二、协议实现
1.消息解析
在使用 ./example /samples/...
进行测试时,发现 start code 中的代码只能解析 request_line
和单个 request_head
,无法进一步解析多条 request_head
。因此,该代码仅能正确处理 sample_request_example
。
为了解析多条 request_head
,我们需要修改 parser.y
中的 request_header
解析规则。关键在于正确使用 yacc 中的 |
符号,通过递归解析所有的 request_header
。
在消息解析部分,还有一个重要任务是在 parse.c
中处理畸形请求。在 parse.c
中,有一个 TODO
是关于处理畸形请求(Handle Malformed Requests)。
1 | //TODO Handle Malformed Requests |
为此,我们需要使用 yystart
语句来实现,并在解析完成后使用 memset
清空 buf
。
另外,代码中的 malloc
语句一开始只分配了处理一行的内存(malloc(sizeof(request_header) * 1)
),为了能够解析多个 request_header
,我们需要将其修改为 19 行(malloc(sizeof(request_header) * 19)
)。
1 | request->headers = (Request_header *) malloc(sizeof(Request_header)*19); |
具体修改为:
解析规则修改:在 parser.y
中,修改 request_header
的解析规则,使用 |
符号递归解析多条 request_header
。
处理畸形请求:在 parse.c
中,使用 yystart
语句处理畸形请求,并在解析完成后使用 memset
清空 buf
。
内存分配:将 malloc(sizeof(request_header) * 1)
修改为 malloc(sizeof(request_header) * 19)
,以便解析多个 request_header
。
2.实现服务端与客户端的通信
服务端与客户端的通信可以分为两个部分:echo_server
和 echo_client
。
1)echo_client
Start code 中的 echo_client
部分实现较为完善,但在读取报文时使用了 fgets
函数,这种参数传递方式并不理想。更好的方法是通过 open()
将 argv[3]
中的内容读入 fd_in
(文件描述符),然后通过 read()
将 fd_in
和缓冲区写入 readRet
。
2)echo_server
在 echo_server
部分,需要实现对 GET
、HEAD
、POST
请求的 echo 功能,以及对未实现请求返回 501 状态码和格式错误请求返回 400 状态码。以下是状态码的相关描述:
- 1xx: 报告的 - 接收到请求,继续处理。
- 2xx: 成功 - 请求成功接收、理解并接受。
- 3xx: 重定向 - 需要进一步操作以完成请求。
- 4xx: 客户端错误 - 请求包含语法错误或无法完成。
- 5xx: 服务器错误 - 服务器无法完成显然有效的请求。
这些状态码和原因短语一起为客户端提供了响应信息,客户端只需检查状态码,不必解析原因短语。
Start code 中的 echo_server
部分已经基本实现,现在需要修改的是服务器在发现还有未处理的报文时的行为。
伪代码实现
以下是服务端处理未处理报文时的伪代码:
1 | while ((readret = recv(client_sock, buf, BUF_SIZE, 0)) >= 1) { |
为了改善 echo_client
的报文读取,使用 open()
将 argv[3]
中的内容读入文件描述符 fd_in
,然后通过 read()
将 fd_in
和缓冲区写入 readRet
。
在 echo_server
部分,需要实现对 GET
、HEAD
、POST
请求的 echo 功能,并对未实现的请求返回 501 状态码,对格式错误的请求返回 400 状态码。
在服务器发现未处理报文时,首先通过 parse
函数解析报文,如果请求为空,向 client_sock
发送 "HTTP/1.1 400 Bad request" 响应;如果请求为 POST
、HEAD
、GET
方法之一,则回显请求内容;否则,向 client_sock
发送 "HTTP/1.1 501 Not Implemented" 响应。最后,释放 request
相关的内存。
最后,修改 echo_server.c
文件的顶部包含 parse.h
,以及MakeFile文件确保 parse.h
和 parse.c
在 Makefile
中的依赖关系
三、实验结果和分析
GET
以下分别为 GET 在 server 端(左边)和 client 端(右边)的实验结果。由于server端检查过长,所以只截屏了一部分


HEAD
下面这个分别为HEAD 在 server 端(左边)和 client 端(右边)的实验结果。

POST
下面这个分别为POST 在 server 端(左边)和 client 端(右边)的实验结果。

以下分别为几个错误类型在 client 端和 server 端的测试结果。
400

501

Error

在自动测试平台上的结果如下图所示:

四.进度总结
本周任务完成表
在“完成”“没完成”列对应打“√”
本周任务要求 | 完成 | 没完成 | 备注 |
---|---|---|---|
1、阅读HTTP/1.1的标准文档RFC2616 | √ | ||
2、搭建编程环境 | √ | ||
3、熟悉Socket编程方法; | √ | ||
4、掌握lex和yacc正确解析消息(message)的方法 | √ | ||
5.1实现简单的echo web server。Echo GET, HEAD, POST | √ | ||
5.2 响应没有实现的方法 | √ | ||
5.3 响应错误的方法 | √ | ||
6、功能测试 | √ |