Socket编程日志 Week2

Socket编程日志 Week2

梦猫 Lv2

一、实验概要

第二周需要实现的功能模块主要有以下几部分。

请求响应模块

1.正确响应GET/HEAD/POST请求,并能建立持久连接
2.支持四种错误代码:400,404,501,505
3.妥善管理缓冲区,避免缓冲区溢出错误

文件错误处理模块

1.能够处理文件读写过程中遇到的错误,如权限问题、文件不存在、IO错误等

日志模块

1.按“Error Log”格式记录服务器的出错情况
2.按“Access Log”的“Common Log Format”格式记录服务器处理的请求
3.其它辅助调试的日志记录(不做格式要求)

二、协议设计

1. 响应生成模块

首先,需要实现对GET、HEAD、POST三种请求的响应生成。

GET响应生成

数据结构设计
  1. 响应数据结构
    为了实现HTTP GET响应,首先需要了解正确的响应结构是什么样的。如下所示是需要实现的HTTP GET响应结构示意图。

    GET Answer
    GET Answer

    根据手册要求,响应中必须包含"HTTP/1.1 200 OK\r\n",而响应实体部分则需从请求文件中获取。因此可以定义响应的结构为一个字符串,具体组成如下。
    "HTTP/1.1 200 OK\r\n" + Data_in_file
    此外,根据手册要求,缓冲区大小应设为8192

  2. 文件状态结构
    根据手册提示,使用stat函数来获取文件状态,并以此检测文件权限、大小等信息,处理可能产生的文件读写错误。而stat的返回值为一个自定义结构struct stat,该结构定义在头文件sys/stat.h中,其部分重要定义如下。

    struct stat
    1
    2
    mode_t		st_mode;                /* [XSI] Mode of file (see below) */ \
    off_t st_size; /* [XSI] file size, in bytes */ \

    其中,st_mode为该文件的模式码,包含该文件的类型、权限信息等重要信息;而st_stze为该文件的大小,可用于判断文件大小是否超限。
    而对st_mode值的定义,继续深入查看,可以在sys/stat.hsys/_types/_s_ifmt.h文件中找到对于该值的部分定义如下。

    st_mode
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* file "sys/stat.h" */
    #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) /* regular file */

    /* file "sys/_types/_s_ifmt.h" */
    /* File type */
    #define S_IFMT 0170000 /* [XSI] type of file mask */
    #define S_IFREG 0100000 /* [XSI] regular */

    /* File mode */
    #define S_IRWXU 0000700 /* [XSI] RWX mask for owner */
    #define S_IRUSR 0000400 /* [XSI] R for owner */

    根据定义,如果宏S_ISREG(st_mode)返回TRUE,则代表该文件为常规媒体文件,文件类型正确;
    同时,可以仿照宏S_ISREG(m),通过表达式
    ((st_mode) & S_IRWXU) == S_IRUSR
    定义宏S_ISRFL(m)检查文件的权限,若返回TRUE则代表用户有可读权限,权限类型正确。

协议规则设计

要生成针对HTTP GET请求的响应,大体可以分为一下几部分:

  1. 从请求中获取请求文件的URL,并以此设置文件路径
    若为默认URL,则将路径设为默认文件路径;
    若为指定的URL,则根据URL拼接路径;
    URL长度超限,返回生成失败。

  2. 根据路径打开文件,检查文件权限、大小等信息
    根据前面解析出的路径尝试获取该文件的stat信息,若获取失败则返回生成失败;
    比对文件的st_mode,判断其是否为常规媒体类型文件,且用户拥有可读权限,若不符合则返回生成失败;
    获取文件的st_size,计算其与200 OK响应的长度和是否小于缓冲区长度,若不符合则返回生成失败。

  3. 将文件内容和200 OK响应拷贝至缓冲区中作为响应
    打开该文件,打开失败则返回生成失败;
    清空缓冲区,将200 OK响应拷贝至缓冲区中,再将文件内容拼接至缓冲区中,生成响应。

根据以上设计,可以绘制出生成HTTP GET响应的流程图。

GET Generate
GET Generate

HEAD响应生成

HTTP HEAD请求的响应生成与GET类似,只不过在最后生成响应时只需包含200 OK响应的内容即可。
因此,容易得到HTTP HEAD响应的流程图如下。

HEAD Generate
HEAD Generate

POST响应生成

HTTP POST响应的生成则更为简单,根据手册要求,只需将请求echo回客户端即可。而这一功能在Week 1中已经成功实现了,因此此处不再讨论。

2. 服务器响应模块

随后,还需要对Week 1中实现的服务器响应模块进行一定更改,以支持对GET、HEAD、POST请求的分别响应,以及对404、505这两种新错误的响应。
此次实验不需要在此模块中定义新的数据结构,因此跳过数据结构设计部分。

协议规则设计

根据实验要求,响应模块总共需要支持7种不同类型的响应,分别如下:

HTTP GET 请求

HTTP HEAD 请求

HTTP POST 请求

400 Bad Request 错误

501 Not Implement 错误

404 Not Found 错误

505 Version not supported 错误

对于GET、HEAD、POST请求,响应的主要功能已经实现,只需按照本次实验的要求对其进行分别处理,并通过前面设计的响应生成模块生成对应的响应内容,存储至缓冲区中发送即可;
对于400和501错误,前面已经实现,无需更改;
对于505错误,要求如下:
请求消息解析完成后,将其HTTP版本与服务器支持HTTP版本(HTTP/1.1)进行比对,若版本不匹配,则响应"HTTP/1.1 505 HTTP Version not supported\r\n\r\n"
对于404错误,要求如下:
若在GET和HEAD请求的响应生成模块中,对文件进行获取和读入时出现任何错误,导致响应生成失败,程序返回HTTP_FAIL,则响应"HTTP/1.1 404 Not Found\r\n\r\n"

3. 日志记录模块

最后,还需要按照给定格式记录服务器响应请求的Access Log,和服务器错误的Error Log。此外,也可以根据自己的需求记录相关日志信息,无格式要求。

数据结构设计

显然,为了顺利记录日志,需要在程序中对文件进行读写操作。在C语言中使用FILE结构即可定义一个文件,因此无需自行定义相关数据结构。
随后,还需理清Apache手册中Access LogError Log的格式。经过阅读,并按实验的实际情况作出适当调整后,可以得到格式如下。

Access Log: IP:Port - frank [Time] "http_method URL http_version" 200 OK body_size

Error Log: [Time] [core:error] [client: IP:Port] error_info

注:IP:Port在本实验环境中统一为127.0.0.1:9999

对于Access Log,使用之前分析或定义过的数据结构即可完成日志的生成和写入,无需进一步分析;
而对于Error Log,可以发现在其最后需要输出当前错误类型对应的错误信息error_info。由于错误种类较多,错误信息均为字符串,且需考虑到程序对支持其它错误的可扩展性,因此在此可以通过自定义枚举和查找表数据结构来更便捷的生成错误日志。
首先,需要设计一个枚举类型,用于存储每种错误类型,形如如下伪代码。

ERROR TYPE
1
2
3
4
5
enum ERROR_TYPE
ERROR_1,
ERROR_2,
ERROR_3,
......

随后,再定义一个字符串数组,按前面枚举类型的顺序依次存储对应的错误信息,形如如下伪代码。

ERROR INFO TABLE
1
2
3
4
5
6
7
8
9
10
11
log_error_1[] = Error_Info_1
log_error_2[] = Error_Info_2
log_error_3[] = Error_Info_3
......

error_table[] = {
log_error_1,
log_error_2,
log_error_3,
......
}

如此,便得到了一个错误信息的查找表。通过error_table[ERROR_TYPE]就能得到对应的错误信息,以结构化生成相应的日志记录。同时,可以便捷、快速的扩展新的错误类型,只需为其添加ERROR_TYPE和对应的log_error,并插入至error_table的正确位置即可。

协议规则设计

在完成日志记录格式和相关数据结构的定义后,很快便能完成对日志模块的设计。只需编写两个日志记录函数,用于参数化写入日志记录,并在需要记录的地方调用即可。此外,对于自定义的日志记录,直接在需要记录处对日志文件进行写入即可。

4. 缓冲区管理

协议设计

实验手册中要求对缓冲区进行合理的管理,以避免通信过程中出现缓冲区溢出错误。按照手册要求,共有3种潜在的缓冲区溢出风险。

URL长度超限

响应内容长度超限

请求头部长度超限

对于前两种情况,在先前的设计中已经进行了相关的设计和说明,不再赘述,此处只对请求头部长度超限的情况进行设计。
根据要求,请求头部大小应不超过8192字节。因此首先需要将缓冲区大小更改为8192字节。随后,在从客户端接收到请求后,只需对接收到的消息长度进行判断,若等于8192(由于缓冲区长度为8192,故接收消息的长度最大也为8192,但当长度等于8192时,消息占据了最后一位空字符位,即长度超限,会产生缓冲区溢出错误),则不进行词法分析,直接返回400错误。

三、协议实现

1. 响应生成模块

前面提到过,在此模块中需要对GET请求和HEAD请求生成对应的响应,而POST响应的生成与Week 1中已实现的一致,因此无需再作更改。同时,对于HEAD请求,其整体处理流程和GET请求的一致,只需在最终生成响应时去掉拼接文件实体的部分即可。因此下面只给出对GET请求生成响应的详细实现过程。

URL解析部分

首先,需要定义函数来对请求中的URL进行解析,以生成文件路径。请求文件的URL包括两部分,第一部分是服务器的根目录(在此实验中为./static_site),第二部分为请求行中提供的uri。当uri/时,使用默认文件路径(/index.xml)与根目录拼接得到URL,否则使用请求给出的urirequest->http_uri)与根目录拼接生成URL,解析URL成功后应返回HTTP_SUCC。而在生成时,还应注意URL的长度不得超限,若超限则应返回HTTP_FAIL
根据以上描述,可以得到URL解析函数的伪代码如下。

Get URL Function
1
2
3
4
5
6
7
8
9
10
11
12
13
root = "./static_site"
file = "/index.xml"

get_url(request, url)
IF request->http_uri == "/" // 默认URL
THEN url = root + file
RETURN HTTP_SUCC
ELSE IF root.length + request->http_uri.length < max size of URL // 请求给出uri
THEN url += root + request->http_uri
RETURN HTTP_SUCC;
ELSE // URL长度超限
RETURN HTTP_FAIL;
END IF

响应生成部分

随后,根据解析出的URL,可以开始获取对应文件并生成响应。此处的设计流程在协议设计部分已经有了详细的说明,也给出了清晰的流程图,故不再赘述,只给出一些实现时需要掌握的框架代码细节。
1. 使用stat结构变量存储文件信息;
2. 文件属性st_mode和文件内容长度st_size均应从stat结构中获取;
3. 使用open函数根据URL打开文件;
4. 使用read函数从文件中读取内容至缓冲区;
5. 若响应生成成功,应返回HTTP_SUCC,若在上述任一步骤中出错,应返回HTTP_FAIL

由此,可以顺利得到GET请求的响应生成函数伪代码如下。

GET Answer Generator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_200 = "HTTP/1.1 200 OK\r\n"

http_get(URL, buf)
GET file info BY stat()
IF stat() FAILED // gei info failed
THEN RETURN HTTP_FAIL
END IF

IF !S_ISREG(st_mode) OR !S_ISRFL(st_mode) // type or permission wrong
THEN RETURN HTTP_FAIL
END IF

IF _200.length + st_size >= BUF_SIZE // file too large
THEN RETURN HTTP_FAIL
END IF

OPEN file BY open()
IF open() FAILED // open file failed
THEN RETURN HTTP_FAIL
END IF

READ file TO body BY read()
buf = _200 + body
RETURN HTTP_SUCC

根据以上伪代码,就可以顺利的实现HTTP GET响应的生成,而稍加修改后就能实现HTTP HEAD响应的生成。

2. 服务器响应模块

对于服务器响应模块,只需将Week 1已实现的模块进行更改,将GETHEADPOST三种请求分开响应,并额外增加对505错误和404错误的判断和响应即可。
1. 对于三种不同请求的响应,在Week 1中已经阐述了如何判断,只需将其拆分为单独判断即可;
2. 对于505错误的判断,只需在解析后得到的HTTP版本号(存储在request->http_vision中)与支持版本号(HTTP/1.1)进行匹配,若匹配失败则产生505错误,响应对应的错误信息;
3. 而对于404错误的判断则较为简单,如果在调用GET响应生成函数或HEAD响应生成函数时返回了HTTP_FAIL,则产生404错误,响应对应的错误信息。
由此,可以绘制服务器响应模块的响应结构图如下。

Server Answer
Server Answer

根据结构图,就能顺利的完成对服务器响应模块的修改和实现。

3. 日志记录模块

对于日志记录模块,前面的设计已经较为详尽,在此补充一些实现细节。
1. 使用fprintf()函数可以向日志文件中写入数据,而在每次写入后,还应调用fflush()函数来刷新缓冲区,以保证写入内容的正确性;
2. 对于Access Log记录函数,其请求方法,URI和HTTP版本等信息,应使用Request结构体中的http_methodhttp_urihttp_version来生成,而文件大小的部分则应使用stat结构中的file_size来生成;
3. 对于Error Log记录函数,则只需传入对应的ERROR_TYPE,便可以完成日志记录的生成。
由此,可以得到两种日志记录的伪代码如下。

Log Recored
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ok_recored(request, file_stat)
IF file_stat != NULL
THEN body_size = file_stat->file_size
ELSE
THEN body_size = 0;
END IF

WRITE INTO log BY ffprintf:
127.0.0.1:9999 - frank [Time] "request->http_method request->http_uri request->http_version" 200 OK body_size

CALL fflush

error_record(error_type)
WRITE INTO log BY ffprintf:
[Time] [core:error] [client: 127.0.0.1:9999] error_table[error_type]

CALL fflush

随后,只需在正确的地方调用以上两种日志记录函数便可以完成日志的记录工作。

4. 缓冲区管理

前面提到过,如果在接收HTTP请求消息后发现请求头部过长(超过8192字节),则不对其进行词法分析,直接返回400错误。因此,应在接收消息后、词法分析前对消息的长度进行判断,并决定是否响应400错误。
在框架代码中,readret用于存储读取到的文件长度,而BUF_SIZE则存储了缓冲区的最大长度(8192),因此容易写出此部分的伪代码如下。

Check File Size
1
2
3
4
5
WHILE(...)	// 框架函数中接收消息的循环
IF readret >= BUF_SIZE
THEN answer 400 Bad Request // 使用服务器响应模块的对应功能实现
BREAK WHILE // 跳出消息读取循环,等待接收下一次请求
END IF

由此,就能成功对长度超限的请求报错,以避免出现缓冲区溢出错误。

结果分析

根据功能要求,分别需要对GETHEADPOST请求进行测试,对400404501505四种错误进行测试,以及对因请求头部过长而产生的400错误进行测试,并查看是否正确生成了相应的日志记录。
各部分测试结果如下:

GET/HEAD/POST请求

HTTP GET
HTTP GET

HTTP HEAD
HTTP HEAD

HTTP POST
HTTP POST

四种错误请求

400 ERROR
400 ERROR

404 ERROR
404 ERROR

501 ERROR
501 ERROR

505 ERROR
505 ERROR

请求头部超限的请求

Request Too Large
Request Too Large

日志记录

Log
Log

Autolab测试

Autolab
Autolab

实验总结

了解并实践了如何根据请求查找服务器中的文件,并按正确的格式为客户端发送响应,对网络通信有了更清晰的认识;同时,新增了对404和505两种错误类型的判断和响应,为服务器添加了更多功能;在响应过程中涉及到许多对缓冲区的操作,对缓冲区管理有了更好的掌握;新增了日志模块,了解了如何通过日志记录服务器所执行的各种操作,对分析程序行为有很大帮助。

  • Title: Socket编程日志 Week2
  • Author: 梦猫
  • Created at : 2024-05-22 11:57:57
  • Updated at : 2024-05-31 22:00:43
  • Link: https://mengmaor.github.io/2024/05/22/Socket编程记录-Week2/
  • License: All Rights Reserved © 梦猫