Socket编程日志 Week1

Socket编程日志 Week1

梦猫 Lv2

这次实验要求所有源码不得外传和共享,因此在博客里就不再给出源码了。

一、实验概要

第一周需要实现一个简单的echo web server模块,可以对传入的消息进行解析,判断其合法性,并按消息的不同类别进行响应。
对于具体的消息解析部分,利用Lex&Yacc分词方法进行词法分析,以此解析出HTTP请求的种类。

二、协议设计

这次实验所要实现的功能比较简单,不需要额外设计数据结构来辅助实现。因此在数据结构部分主要描述在设计中使用到的数据结构以及对某些重要数据结构的分析。

1. 协议头结构

数据结构设计

在最初环境部署的阶段中,曾使用过example程序测试过对/samples/sample_request_example文件中HTTP请求的响应。

sample_request_example
1
2
GET /~prs/15-441-F15/ HTTP/1.1
Host: www.cs.cmu.edu

可以看到,该文件的输入只包含了一行HTTP请求行和一行HTTP请求头部。在此情况下,框架代码能够正确识别并处理这一HTTP请求。但在实际情况中,HTTP请求包含多行请求头部,因此需要在后续的词法分析设计中保证程序可以处理多行请求头部的语法规则匹配。

HTTP Request
HTTP Request

而对于Yacc解析出的数据,它通过一个特殊的数据结构Request来存储,该结构定义如下。

Request
1
2
3
4
5
6
7
8
9
10
// /include/parse.h
// HTTP Request Header
typedef struct
{
char http_version[50];
char http_method[50];
char http_uri[4096];
Request_header *headers;
int header_count;
} Request;

可以看到,http_versionhttp_methodhttp_uri将会分别存储HTTP请求行中的协议版本,请求方法和请求URI。而HTTP请求头部则被存储在一个Request_header结构的数组中。

Request_header
1
2
3
4
5
6
7
// /include/parse.h
// Header field
typedef struct
{
char header_name[4096];
char header_value[4096];
} Request_header;

Request_header中则会存储HTTP请求头部的头部字段名和值。
根据以上数据结构,可以在后续实验中更好的对解析后结果进行分类匹配并响应。

协议规则设计

对于解析后的HTTP请求消息,共分为三种情况处理:

GET/HEAD/POST

当解析后发现消息为合法HTTP请求,且方法为GET/HEAD/POST,为已实现的方法,此时将消息进行重新封装,并直接发送回客户端。

Not-Implemented

当解析后发现消息为合法的HTTP请求,但该消息请求的方法并未实现时,向客户端发送"HTTP/1.1 501 Not Implemented\r\n\r\n"

Bad-Request

当解析过程中发现消息格式错误,为非法消息时,向客户端发送"HTTP/1.1 400 Bad request\r\n\r\n"

在词法分析模块将输入消息进行解析后,将缓冲区中的解析结果与上述情况进行一一匹配,便能得到正确的响应。随后再根据上述要求进行响应即可。

2. Lex&Yacc词法分析结构

数据结构设计

总体来说,Lex&Yacc词法分析的框架是:Lex先扫描每个输入的字符串,按预先设定的种类对字符串进行匹配,并将其替换为该种类的标签符。
随后,Yacc扫描Lex处理后得到的标签串,根据实现定义的语法规则对标签串进行规约匹配,得到规约后的输出结果。

协议规则设计

在查看Yacc规约规则的源码/src/parser.y后,发现其中已经定义了用于识别HTTP请求行的规则request_line,和用于识别HTTP请求头部的规则request_header。仔细阅读后,发现已有的语法规则只能成功识别一行HTTP请求头部,这与实验要求不符,因此需要设计可识别多行请求头部的语法规则。
由于对于每行请求头部,它所遵循的语法规则都是相同的,且与单行请求头部的规则一致,因此使用递归的方式就能很好的完成多行识别,这在某些已有规则的定义中也有所体现。
由此,只需先对第一行按单行请求头部的语法规则进行匹配,再递归的对后续输入尝试用此规则进行匹配即可。

三、协议实现

1. 客户端输入模块

首先,在最初的环境配置阶段,以及阅读源码后可知,echo_client程序目前通过命令行输入的方式来读取输入,而example程序则可以通过文件来获取输入。因此,首先需要将echo_client更改为通过文件输入。这一部分的实现较为简单,只需参照/src/example.c中的代码实现即可,并注意在读取参数表时加入一项输入文件的路径。

2. 词法分析模块

在前面的设计部分提到过,通过递归的方式设计语法规则就可以很好的完成对多行请求头部的匹配。具体而言,在Yacc中,通过逻辑连接词就可以实现递归匹配。

Rules_Rec
1
2
3
4
5
6
rules_rec: rules {
body1;
}; |
rules_rec {
body2;
};

如上伪代码给出了一种递归语法规则rules_rec的定义方式。其中,rules是对单行定义的语法规则,而在rules_rec匹配时,会尝试匹配一个rules或是一个rules_rec,这就形成了递归调用,以此可以根据一个语法规则对多行中的每行都按rules规则进行匹配。
由于在框架代码中,对于单行HTTP请求头部的语法规则request_header已经定义,因此只需在/src/parser.y参照上面给出的定义方式,定义递归匹配多行的语法规则request_header_rec,并使用此规则进行匹配即可。
此外,前面提到过,/src/parser.c会将解析完毕的HTTP请求存储在Request -> headers中,而在框架代码中,由于只进行一行的请求头部解析,所以程序只为Request -> headers申请了一个元素的空间。因此还需增加程序为Request -> headers申请的空间大小,以存储全部的HTTP请求头部信息。在这次实验中,暂且将空间大小设为64行,应该可以顺利实现实验所要求的功能。

3. 服务器响应模块

服务器响应模块应在/src/echo_server.c中实现。在服务器响应部分中,已有的框架代码会从客户端接收请求消息,将其存储至缓冲区buf中,并直接发送回客户端。因此,需要为其加入词法分析和对应响应功能。首先,利用在/src/parser.h中定义的parser函数,将buf中的内容使用词法分析模块进行解析,并将解析结果存储在专门的数据结构Request中。随后,可以对解析结果进行判断。在解析失败时,request应为空,此时返回400报错;否则解析成功,此时判断request -> http_method是否为"GET"\"HEAD"\"POST",若字符串比对成功,则应将解析后的消息复制至缓冲区中,并直接发送回客户端;否则,则代表请求为未实现的HTTP方法,此时返回501报错。此外,在对解析后消息进行匹配和响应时,会涉及到对缓冲区的匹配识别和修改问题。因此可以提前定义一些用于匹配和填充的字符串,用以更便捷和安全的操作缓冲区。同时,在每次发送响应时还应检查发送是否成功,此部分代码在框架代码中已经给出。由此可以写出响应模块的伪代码如下:

Response
1
2
3
4
5
6
7
8
9
10
11
12
IF request IS NULL
THEN SEND ERROR 400
CHECK SEND
ELSE IF request -> http_method IS "GET"
OR IS "HEAD" OR IS "POST"
THEN COPY request TO buf
SEND buf
CHECK SEND
ELSE
THEN SEND ERROR 501
CHECK SEND
END IF

4. 缓冲区清除

在每完成一个HTTP请求信息的解析和响应后,为避免上一次的消息影响对下一次消息的解析和响应结果,因此需要对解析结果request和缓冲区都进行清空。这一部分很容易就可以在/src/echo_server.c进行实现。但在一次测试中,发现如果先传输一个格式错误,响应ERROR 400的HTTP请求,再传输格式正确的HTTP请求,程序依然会判断该消息格式错误,响应ERROR 400。经过检查,发现此时request为空,即错误出现在词法分析模块。仔细检查测试中的输出可以发现,在输入格式正确的HTTP请求后词法分析模块进行匹配的字符串并非新输入的字符串,而是前面格式错误输入文件的后续部分。因此可以断定出现此问题的原因是词法分析模块存在未被清空的缓冲区。

Wrong Output Log
Wrong Output Log

于是再度深入语法分析模块进行检查,最终发现是Lex在进行匹配时存在隐藏的队列结构的缓冲区(/src/lex.yy.c),这会导致每次产生解析错误后,其后续消息会被作为下一次消息解析的开头,进而再次导致解析错误。不过,框架代码中也已经定义了用于清除该隐藏缓冲区的函数yylex_destroy,因此只需在/src/parser.c文件中,开始进行语法分析前调用上述函数,清空Lex的隐藏缓冲区即可。随后再次测试即可得到正确的结果。

四、结果分析

在结果测试部分,分别进行了如下测试:

GET/HEAD/POST请求

HTTP GET
HTTP GET

HTTP HEAD
HTTP HEAD

HTTP POST
HTTP POST

未实现方法PUT/WRONG

501 PUT
501 PUT

501 WRONG
501 WRONG

6种格式错误

400 1
400 1

400 2
400 2

400 3
400 3

400 4
400 4

400 5
400 5

400 6
400 6

Autolab测试

Autolab
Autolab

五、实验总结

大概了解了一下实验环境,学了点正则表达式相关的知识和运用。明白了框架代码中服务器和客户端是如何通过socket进行通信的,以及源码的一些数据结构和程序框架,算是一个简单的入门。

  • Title: Socket编程日志 Week1
  • Author: 梦猫
  • Created at : 2024-05-15 16:54:59
  • Updated at : 2024-05-25 15:38:16
  • Link: https://mengmaor.github.io/2024/05/15/Socket编程记录-Week1/
  • License: All Rights Reserved © 梦猫