TinywebServer代码详解–http连接处理-下(6)

该blog内容转自:最新版Web服务器项目详解 - 06 http连接处理(下)

该blog对上述内容进行补充(在本人的角度)

结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)

原项目地址(点击跳转)

博主添加注释后项目地址(点击跳转)



一、基础知识

1.stat函数

用于检索文件的元数据,如文件大小、权限和修改时间等。并将文件属性存储在结构体stat里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);

// struct stat 结构体定义
struct stat {
mode_t st_mode; // 文件类型和权限
off_t st_size; // 文件大小(以字节为单位)

dev_t st_dev; // 设备ID
ino_t st_ino; // inode节点号
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 所有者用户ID
gid_t st_gid; // 所有者组ID
dev_t st_rdev; // 设备ID(如果是特殊文件)
blksize_t st_blksize; // 文件系统I/O块大小
blkcnt_t st_blocks; // 占用的512字节块数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态更改时间
};

参数:

  • pathname:指向要获取信息的文件路径的字符串指针
  • buf:指向 stat 结构的指针,stat 结构将存储文件的状态信息

return:

  • 返回值为0表示成功
  • 返回值为-1表示失败,并设置 errno 来指示错误类型


2.内存映射

如果想要实现进程间通信,可以通过函数创建一块内存映射区,和管道不同的是管道对应的内存空间在内核中,而内存映射区对应的内存空间在进程的用户区(用于加载动态库的那个区域),也就是说进程间通信使用的内存映射区不是一块,而是在每个进程内部都有一块

由于每个进程的地址空间是独立的,各个进程之间也不能直接访问对方的内存映射区,需要通信的进程需要将各自的内存映射区和同一个磁盘文件进行映射,这样进程之间就可以通过磁盘文件这个唯一的桥梁完成数据的交互了

image-20240228204824768

如上图所示:磁盘文件数据可以完全加载到进程的内存映射区也可以部分加载到进程的内存映射区,当进程A中的内存映射区数据被修改了,数据会被自动同步到磁盘文件,同时和磁盘文件建立映射关系的其他进程内存映射区中的数据也会和磁盘文件进行数据的实时同步,这个同步机制保障了各个进程之间的数据共享


(1)mmap函数

使用内存映射可以实现进程间的通信,创建内存映射区的函数原型如下:

1
2
3
#include <sys/mman.h>
// 创建内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数:

  • addr: 从动态库加载区的什么位置开始创建内存映射区,一般指定为NULL, 委托内核分配
  • length: 创建的内存映射区的大小(单位:字节),实际上这个大小是按照4k的整数倍去分配的
  • prot: 对内存映射区的操作权限
    • PROT_READ: 读内存映射区
    • PROT_WRITE: 写内存映射区
    • 如果要对映射区有读写权限: PROT_READ | PROT_WRITE
  • flags:
    • MAP_SHARED: 多个进程可以共享数据,进行映射区数据同步
    • MAP_PRIVATE: 映射区数据是私有的,不能同步给其他进程
  • fd: 文件描述符, 对应一个打开的磁盘文件,内存映射区通过这个文件描述符和磁盘文件建立关联
  • offset: 磁盘文件的偏移量,文件从偏移到的位置开始进行数据映射,使用这个参数需要注意两个问题:
    • 偏移量必须是4k的整数倍, 写0代表不偏移
    • 这个参数必须是大于 0

返回值:

  • 成功: 返回一个内存映射区的起始地址
  • 失败: MAP_FAILED (that is, (void *) -1)

注意事项:

1
2
3
4
5
6
7
1. 第一个参数 addr 指定为 NULL 即可
2. 第二个参数 length 必须要 > 0
3. 第三个参数 prot,进程间通信需要对内存映射区有读写权限,因此需要指定为:PROT_READ | PROT_WRITE
4. 第四个参数 flags,如果要进行进程间通信, 需要指定 MAP_SHARED
5. 第五个参数 fd,打开的文件必须大于0,进程间通信需要文件操作权限和映射区操作权限相同
- 内存映射区创建成功之后, 关闭这个文件描述符不会影响进程间通信
6. 第六个参数 offset,不偏移指定为0,如果偏移必须是4k的整数倍

内存映射区使用完之后也需要释放,释放函数原型如下:


(2)munmap函数

内存映射区使用完之后也需要释放,释放函数原型如下:

1
int munmap(void *addr, size_t length);

参数:

  • addr: mmap()的返回值, 创建的内存映射区的起始地址
  • length: 和mmap()第二个参数相同即可

返回值:

  • 函数调用成功返回 0,失败返回 -1

(3)进程通信实例

无血缘关系的进程

对于没有血缘关系的进程间通信,需要在每个进程中分别创建内存映射区,但是这些进程的内存映射区必须要关联相同的磁盘文件,这样才能实现进程间的数据同步

进程A code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>

#define FILENAME "./text.txt"

int main()
{
// 1. 打开一个磁盘文件
int fd = open(FILENAME, O_RDWR);
printf("1111");

// 2.创建内存映射区
char* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap()");
exit(1);
}

close(fd);

const char* pt = "aaaa";

// 将字符指针指向的内容写入 内存映射区
strcpy(ptr,pt);

// 释放内存映射区
munmap(ptr, 4000);

exit(0);
}

进程 B code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>

#define FILENAME "./text.txt"

int main()
{
// 1. 打开一个磁盘文件
int fd = open(FILENAME, O_RDWR);

// 2.创建内存映射区
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap()");
exit(1);
}

printf("从内存映射区读取数据: %s\n",(char *)ptr);

// 释放内存映射区
munmap(ptr, 4000);

exit(0);
}


3.iovec

iovec 结构体在 C++ 中用于描述一个向量(数组)缓冲区。它通常与 readvwritev 函数一起使用,用于执行分散/聚集 I/O 操作。分散/聚集I/O允许在一次系统调用中读取或写入多个缓冲区,从而提高I/O操作的效率

iovec结构体简介:

iovec 结构体定义在头文件 <sys/uio.h> 中,其定义如下:

1
2
3
4
struct iovec {
void *iov_base; // 指向缓冲区的起始地址
size_t iov_len; // 缓冲区的长度
};

(1)readv函数

readv 函数:从文件描述符读取数据到多个缓冲区

1
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

参数:

  • fd:文件描述符
  • iov:指向 iovec 结构体数组的指针
  • iovcntiovec 结构体数组中的元素个数

返回值:

  • 返回实际读取的字节数
  • 返回 -1,并设置 errno 来指示错误类型

(2)writev函数

writev 函数:从多个缓冲区写入数据到文件描述符

writev以顺序iov[0]iov[1]iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和

1
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

参数:

  • fd:文件描述符
  • iov:指向 iovec 结构体数组的指针
  • iovcntiovec 结构体数组中的元素个数

返回值:

  • 返回实际写入的字节数
  • 返回 -1,并设置 errno 来指示错误类型

(3)实例

读取数据:使用readv函数从文件描述符读取数据到多个缓冲区

example.txt文件内容为:

1
2
3
012345678
0123456789012345678
01234567890123456789012345678
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

int main() {
// 打开文件
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}

// 准备缓冲区
char buf1[10];
char buf2[20];
char buf3[30];

// 设置iovec结构体数组
struct iovec iov[3];
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1); // 留出一个位置给\0
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2); // 留出一个位置给\0
iov[2].iov_base = buf3;
iov[2].iov_len = sizeof(buf3); // 留出一个位置给\0

// 使用readv读取数据
ssize_t nread = readv(fd, iov, 3);
if (nread == -1) {
perror("readv");
close(fd);
return 1;
}

// 手动添加字符串结束符
buf1[iov[0].iov_len-1] = '\0';
buf2[iov[1].iov_len-1] = '\0';
buf3[iov[2].iov_len-1] = '\0';

std::cout << "Read " << nread << " bytes." << std::endl;

cout << buf1 << endl;
cout << buf2 << endl;
cout << buf3 << endl;

// 关闭文件
close(fd);

return 0;
}

运行结果:

1
2
3
4
5
(base) zxz@ubuntu:~/Proj/C_C++/C++_Demo/3$ ./1
Read 60 bytes.
012345678
0123456789012345678
01234567890123456789012345678

写入数据:将多个缓冲区的数据写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>

int main() {
// 打开文件
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}

// 准备数据
const char *buf1 = "Hello, ";
const char *buf2 = "world!\n";
const char *buf3 = "This is an example of writev.\n";

// 设置iovec结构体数组
struct iovec iov[3];
iov[0].iov_base = (void*)buf1;
iov[0].iov_len = strlen(buf1);
iov[1].iov_base = (void*)buf2;
iov[1].iov_len = strlen(buf2);
iov[2].iov_base = (void*)buf3;
iov[2].iov_len = strlen(buf3);

// 使用writev写入数据
ssize_t nwritten = writev(fd, iov, 3);
if (nwritten == -1) {
perror("writev");
close(fd);
return 1;
}

std::cout << "Wrote " << nwritten << " bytes." << std::endl;

// 关闭文件
close(fd);

return 0;
}

编译运行,查看output.txt文件内容为:

1
2
Hello, world!
This is an example of writev.


4.va_start

va_start 是一个宏,用于处理变长参数函数。它定义在 <cstdarg> 头文件中(在 C 中则是 <stdarg.h> 头文件)。va_start 用于初始化一个 va_list 对象,以便在变长参数函数中访问可变数量的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <cstdarg>

// 定义一个函数,计算多个数的总和
// 定义一个接收变长参数的函数。第一个参数 count 指定后续参数的数量
int sum(int count, ...) {
// 声明一个 va_list (列表) 类型的变量,用于访问变长参数,va_list是用于存储参数的列表
va_list args;
// 初始化 va_list 变量,使其指向变长参数列表的第一个参数,初始化args来访问额外的参数
va_start(args, count);

int total = 0;
for (int i = 0; i < count; ++i) {
// 使用 va_arg 宏访问变长参数列表中的每个参数。在此示例中,每个参数都是 int 类型
total += va_arg(args, int);
}

// va_end(args);:结束 va_list 的使用
va_end(args);
return total;
}

int main() {
std::cout << "Sum of 1, 2, 3, 4, 5: " << sum(5, 1, 2, 3, 4, 5) << std::endl;
std::cout << "Sum of 10, 20, 30: " << sum(3, 10, 20, 30) << std::endl;
return 0;
}


5.vsnprintf()

vsnprintf 是一个函数,用于将格式化的数据写入到字符缓冲区中,类似于 snprintf,但它使用一个 va_list 类型的参数列表。这在处理可变参数函数时非常有用。vsnprintf 函数定义在头文件 <cstdio><stdio.h>

函数原型:

1
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

str:目标缓冲区的指针,用于存储格式化后的字符串

size:缓冲区的大小,以字符为单位。确保这个值足够大,以便存储最终的字符串和一个空终止字符

format:格式字符串,类似于 printf 的格式字符串

ap:一个 va_list 类型的参数列表,包含了变长参数

返回值:

  • 如果成功,则返回将要写入的字符数,不包括终止的空字符。如果返回值大于或等于 size,则表示输出被截断
  • 如果出错,则返回一个负值

展示如何将格式化的数据写入缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <cstdarg>
#include <cstdio>

#define WRITE_BUFFER_SIZE 1024

// write_formatted 函数接收一个缓冲区、缓冲区大小、格式字符串以及变长参数列表
void write_formatted(char* buffer, int buffer_size, const char* format, ...) {
va_list args;
// 使用 va_start 初始化 va_list 变量 args
va_start(args, format);

// 使用 vsnprintf 将格式化的数据写入缓冲区
int len = vsnprintf(buffer, buffer_size, format, args);

// 检查返回值以检测错误或输出被截断的情况
if (len < 0) {
std::cerr << "Formatting error occurred." << std::endl;
} else if (len >= buffer_size) {
std::cerr << "Output was truncated. Needed size: " << len + 1 << std::endl;
} else {
std::cout << "Formatted string: " << buffer << std::endl;
}

// 使用 va_end 结束对 va_list 的处理
va_end(args);
}

int main() {
char write_buffer[WRITE_BUFFER_SIZE];
write_formatted(write_buffer, WRITE_BUFFER_SIZE, "Hello, %s! You have %d new messages.", "Alice", 5);
return 0;
}



二、流程图

1.程序流程

浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,进入相应的逻辑和模块

其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_oncehttp_conn::write完成数据的读取与发送

image-20240711090353137



2.HTTP_CODE含义

表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析与响应中只用到了七种

NO_REQUEST

  • 请求不完整,需要继续读取请求报文数据
  • 跳转主线程继续监测读事件

GET_REQUEST

  • 获得了完整的HTTP请求
  • 调用do_request完成请求资源映射

NO_RESOURCE

  • 请求资源不存在
  • 跳转process_write完成响应报文

BAD_REQUEST

  • HTTP请求报文有语法错误或请求资源为目录
  • 跳转process_write完成响应报文

FORBIDDEN_REQUEST

  • 请求资源禁止访问,没有读取权限
  • 跳转process_write完成响应报文

FILE_REQUEST

  • 请求资源可以正常访问
  • 跳转process_write完成响应报文

INTERNAL_ERROR

  • 服务器内部错误,该结果在主状态机逻辑switchdefault下,一般不会触发


三、代码详解

1.do_request()

这是一个功能逻辑单元, 当得到一个完整、正确的HTTP请求时,就需要分析目标文件的属性,如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其映射到内存地址m_file_address处(可以把文件的某个部分直接映射到进程的地址空间中,这样可以通过指针直接访问文件内容,而无需调用读写文件的系统调用。这通常可以提高文件操作的性能,因为减少了内核与用户空间之间的数据拷贝),并告诉调用者获取文件成功

process_read函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的BAD_REQUEST,一部分是do_request的返回结果(在主状态机位于请求头状态时,获得一个完整的GET请求,则会retutn do_request;或者在主状态机位于请求体状态时,获得一个完整的POST请求,则会return do_request

do_request函数将网站根目录和url文件拼接,然后通过stat判断该文件属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址

为了更好的理解请求资源的访问流程,这里对各种各页面跳转机制进行简要介绍。其中,浏览器网址栏中的字符,即url,可以将其抽象成ip:port/xxxxxx通过html文件的action属性进行设置

m_url为请求报文中解析出的请求资源,以/开头,也就是/xxx,项目中解析后的m_url有8种情况

/

  • GET请求,跳转到judge.html,即欢迎访问页面(在parse_request_line处理请求行时,当请求报文的请求头的url只有/则将m_urljudge.html界面进行拼接)

/0

  • POST请求,跳转到register.html,即注册页面

/1

  • POST请求,跳转到log.html,即登录页面

/2CGISQL.cgi

  • POST请求,进行登录校验
  • 验证成功跳转到welcome.html,即资源请求成功页面
  • 验证失败跳转到logError.html,即登录失败页面

/3CGISQL.cgi

  • POST请求,进行注册校验
  • 注册成功跳转到log.html,即登录页面
  • 注册失败跳转到registerError.html,即注册失败页面

/5

  • POST请求,跳转到picture.html,即图片请求页面

/6

  • POST请求,跳转到video.html,即视频请求页面

/7

  • POST请求,跳转到fans.html,即关注页面

具体的登录和注册校验功能会在第12节进行详解,到时候还会针对html进行介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/*
* @func:功能逻辑单元
* 当得到一个完整、正确的HTTP请求时,就需要分析目标文件的属性
* 如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其
* 映射到内存地址m_file_address处,并告诉调用者获取文件成功
* 内存映射,将文件内容直接映射到进程地址空间
* 这样服务端进程可以像访问普通内存一样访问数据文件
* @note:doc_root网站根目录,文件夹内存放请求的资源和跳转的html文件
*/
http_conn::HTTP_CODE http_conn::do_request()
{
// 网站目录(资源目录)
strcpy(m_real_file, doc_root);
int len = strlen(doc_root);

// strrchr 函数查询 m_url中最后一次出现 / 位置的指针
// 找到m_url中/的位置
const char *p = strrchr(m_url, '/');

// 处理cgi
// 实现登录和注册校验
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
{
// 根据标志判断是登录检测还是注册检测
char flag = m_url[1]; // 等价于*(p+1)

char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/");
strcat(m_url_real, m_url + 2);
strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
free(m_url_real);

//将用户名和密码提取出来
//eg:user=123&passwd=123
char name[100], password[100];
int i;
for (i = 5; m_string[i] != '&'; ++i)
name[i - 5] = m_string[i];
name[i - 5] = '\0';

int j = 0;
for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
password[j] = m_string[i];
password[j] = '\0';

// 是初次注册情况
if(*(p+1) == '3')
{
//如果是注册,先检测数据库中是否有重名的
//没有重名的,进行增加数据
//构建一个用于插入数据到数据库的user表中的 SQL 语句
char *sql_insert = (char *)malloc(sizeof(char) * 200);
strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
strcat(sql_insert, "'");
strcat(sql_insert, name);
strcat(sql_insert, "', '");
strcat(sql_insert, password);
strcat(sql_insert, "')");

// find函数会返回一个迭代器,在map中找name键
// 若找到了,迭代器会指向该键对应的键值对
// 如果没有找到,迭代器将等于 users.end(),即指向 map 结尾的迭代器
if (users.find(name) == users.end())
{
// 没有找到,则注册的为新用户
m_lock.lock();
// mysql_query 向数据库执行指令 sql_insert,成功return 0
int res = mysql_query(mysql, sql_insert);
users.insert(std::pair<std::string, std::string>(name, password));
m_lock.unlock();

if (!res)
// 成功
strcpy(m_url, "/log.html");
else
strcpy(m_url, "/registerError.html");
}
else
// 注册失败,用户存在
strcpy(m_url, "/registerError.html");
}
//如果是登录,直接判断
//若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0
else if (*(p + 1) == '2')
{
if (users.find(name) != users.end() && users[name] == password)
strcpy(m_url, "/welcome.html");
else
// m_url指向登陆失败的页面
strcpy(m_url, "/logError.html");
}

}

//如果请求资源为/0,表示跳转注册界面
if (*(p + 1) == '0')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);

// 将网站目录和/register.html进行拼接,更新到m_real_file中
strcpy(m_url_real, "/register.html");
// m_url_real字符串复制到m_real_file字符串的len索引位置,进行拼接
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}

//如果请求资源为/1,表示跳转登录界面
else if (*(p + 1) == '1')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);

// 将网站目录和/log.html进行拼接,更新到m_real_file中
strcpy(m_url_real, "/log.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}

//如果请求资源为/5,表示跳转pic
else if (*(p + 1) == '5')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);

// 将网站目录和/picture.html进行拼接,更新到m_real_file中
strcpy(m_url_real, "/picture.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}

//如果请求资源为/6,表示跳转video
else if (*(p + 1) == '6')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);

// 将网站目录和/video.html进行拼接,更新到m_real_file中
strcpy(m_url_real, "/video.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}

//如果请求资源为/7,表示跳转weixin
else if (*(p + 1) == '7')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);

// 将网站目录和/fans.html进行拼接,更新到m_real_file中
strcpy(m_url_real, "/fans.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else
// 在parse_request_line处理请求行时,当url只有/则将m_url与judge.html界面进行拼接
// 如果以上均不符合,即不是登录和注册,直接将url与网站目录拼接
// 这里的情况是跳转到judge.html界面即欢迎界面,请求服务器上的一个图片
// 是一个GET请求
strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);

// 通过stat获取请求资源文件信息,成功则将信息更新到m_file_stat结构体
// 失败返回NO_RESOURCE状态,表示资源不存在
if (stat(m_real_file, &m_file_stat) < 0)
return NO_RESOURCE;

// 判断文件的权限,是否可读,不可读则返回FORBIDDEN_REQUEST状态
if (!(m_file_stat.st_mode & S_IROTH))
return FORBIDDEN_REQUEST;

// 判断文件类型,如果是目录,则返回BAD_REQUEST,表示请求报文有误
if (S_ISDIR(m_file_stat.st_mode))
return BAD_REQUEST;

// 以只读方式获取文件描述符,通过mmap将该文件映射到内存中
int fd = open(m_real_file, O_RDONLY);
m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
//避免文件描述符的浪费和占用
close(fd);

//表示请求文件存在,且可以访问
return FILE_REQUEST;
}


2.process_write()

该函数在process函数中被调用,根据process函数中先调用的process_read函数解析请求报文,返回的HTTP状态码作为参数传入process_write函数进行判断,生成对应的响应报文存入m_write_buf

process_read函数解析请求报文,会返回的状态码如下,process_write会根据传入的HTTP状态码进行处理

INTERNAL_ERROR/BAD_REQUEST/FORBIDDEN_REQUEST

  • 服务器内部错误/请求报文语法错误/URL中访问的资源没有访问权限
  • 这些请求出错的情况,响应报文只包含一种,即写入到m_write_buf中的相关报错信息
  • 只申请一个iovec(向量缓冲区中的缓冲区数量为1),指向m_write_buf
  • 服务器就需要为响应报文填入相关的错误状态码,错误信息

FILE_REQUEST

  • URL中访问的资源文件存在且可以访问
  • 服务器为响应报文添加相关的正常的状态码
  • 当前URL中请求资源文件存在且可以正常访问的情况,响应报文包含两种,写入到m_write_buf中的相关正常的状态信息,以及请求报文中URL申请访问的资源文件内容
  • 会通过向量缓冲区iovec的两个iovec指针分别指向m_write_buf以及m_file_address
  • process_read函数中之前若请求报文解析正常,获得一个完整的请求报文,会通过调用do_request函数中使用mmap函数将请求报文中URL访问的文件映射到服务器进程内存中使用m_file_address进行指向,内存映射之后,访问m_file_address指针指向内容就是访问URL请求的资源文件内容
  • iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据。
  • 成员iov_len表示实际写入的长度

process_write中的iovec结构体数组设置好之后,服务器会通过writev函数将两个缓冲区的数据写入到用于通信的套接字写缓冲区,发送给客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/*
* func:生成响应报文
* (根据process函数中调用process_read解析请求报文,返回的HTTP_CODE状态码,
* 传入process_write函数判断进行对应响应报文的生成)
* 服务器子线程调用process_write向m_write_buf中写入响应报文
* note:响应报文组成-状态行-消息报头-空行-响应正文
*/
bool http_conn::process_write(HTTP_CODE ret)
{
switch (ret)
{
// 服务器内部错误
case INTERNAL_ERROR:
{
// 状态行--500 Internal Server Error:服务器在执行请求时出现错误
add_status_line(500, error_500_title);
// 消息报头
add_headers(strlen(error_500_form));
if (!add_content(error_500_form))
return false;
break;
}

// 报文语法错误,404
case BAD_REQUEST:
{
// 状态行--400 Bad Request:请求报文存在语法错误
add_status_line(404, error_404_title);
// 消息报头
add_headers(strlen(error_404_form));
if (!add_content(error_404_form))
return false;
break;
}

// 资源没有访问权限,403
case FORBIDDEN_REQUEST:
{
// 状态行--403 Forbidden:请求被服务器拒绝
add_status_line(403, error_403_title);
// 消息报头
add_headers(strlen(error_403_form));
if (!add_content(error_403_form))
return false;
break;
}

// 文件存在且可以访问,200
case FILE_REQUEST:
{
// 状态行--200 OK:客户端请求被正常处理
add_status_line(200, ok_200_title);
// GET请求,请求访问的文件有内容
if (m_file_stat.st_size != 0)
{
// 消息报头
add_headers(m_file_stat.st_size);

// 将向量缓冲区分别指向 m_write_buf与m_file_address
// 之后可以通过writev函数将两个缓冲区的数据写入到用于通信的套接字写缓冲区,返回给客户端
// 第一个iovec指针指向响应报文缓冲区,长度指向m_write_idx
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
// 第二个iovec指针指向mmap返回的文件指针,长度指向文件大小
m_iv[1].iov_base = m_file_address;
m_iv[1].iov_len = m_file_stat.st_size;
m_iv_count = 2;
// 发送的全部数据大小,为响应报文m_write_buf中的数据大小,加上文件的大小
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
break;
}

default:
return false;
}
// 除FILE_REQUEST状态外,其余状态只申请一个iovec,指向响应报文缓冲区
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv_count = 1;
bytes_to_send = m_write_idx;
return true;
}


3.为响应报文添加内容的相关函数

add_status_line函数,添加状态行:http/1.1 状态码 状态消息

add_headers函数添加消息报头,内部调用add_content_lengthadd_linger函数

  • content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据
  • connection记录连接状态,用于告诉浏览器端保持长连接

add_blank_line添加空行

(1)add_response()

该函数非常重要,为响应报文的公共函数,为响应报文按照format格式添加一行数据,写入到m_write_buf中,并且更新m_write_idx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* func:添加响应报文的公共函数
* --为响应报文按照format格式添加一行数据,写入到m_write_buf中,并且更新m_write_idx
*/
bool http_conn::add_response(const char *format, ...)
{
//如果写入内容超出m_write_buf大小则报错
if (m_write_idx >= WRITE_BUFFER_SIZE)
return false;
// 定义可变参数列表
va_list arg_list;
// 将变量arg_list初始化为传入参数(初始化 va_list 变量,使其指向变长参数列表的第一个参数)
va_start(arg_list, format);
// 将数据format从可变参数列表写入缓冲区写,返回写入数据的长度
int len=vsnprintf(m_write_buf+m_write_idx,WRITE_BUFFER_SIZE-1-m_write_idx,format,arg_list);
//如果写入的数据长度超过缓冲区剩余空间,则报错
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
{
// va_end结束 va_list 的使用
va_end(arg_list);
return false;
}
//更新m_write_idx位置
m_write_idx += len;
// 清空可变参列表
va_end(arg_list);

LOG_INFO("request:%s", m_write_buf);

return true;
}

(2)其他函数

如下的7个函数,均是内部调用add_response函数更新m_write_idx指针和缓冲区m_write_buf中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* func:为响应报文添加状态行
* --响应报文的第一部分(仅一行)
*/
bool http_conn::add_status_line(int status, const char *title)
{
return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}

/*
* func:为响应报文添加消息报头--具体的添加文本长度、连接状态和空行
* --消息报头为响应报文第二部分(可以由多行组成)
* --空行为响应报文第三部分(仅一行)
*/
bool http_conn::add_headers(int content_len)
{
return add_content_length(content_len) &&
add_linger() &&
add_blank_line();
}

/*
* func:为响应报文添加消息报头(第二部分)
* --添加Content-Length,表示响应报文的长度
*/
bool http_conn::add_content_length(int content_len)
{
return add_response("Content-Length:%d\r\n", content_len);
}

/*
* func:为响应报文添加消息报头(第二部分)
* --添加文本类型,这里是html
*/
bool http_conn::add_content_type()
{
return add_response("Content-Type:%s\r\n", "text/html");
}

/*
* func:为响应报文添加消息报头(第二部分)
* --添加连接状态,通知浏览器端是保持连接还是关闭
*/
bool http_conn::add_linger()
{
return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}

/*
* func:为响应报文添加消息报头(第二部分)
* --添加空行
*/
bool http_conn::add_blank_line()
{
return add_response("%s", "\r\n");
}

/*
* func:为响应报文添加响应正文(第四部分)
* --添加响应正文文本
*/
bool http_conn::add_content(const char *content)
{
return add_response("%s", content);
}

(3)write()

proactor模式下,是服务器主线程检测写事件,并且调用http_conn::write函数将响应报文,写入http对象的通信套接字的写缓冲区,发送给浏览器端

该函数具体逻辑如下:

生成响应报文时初始化byte_to_send,包括m_write_buf中数据大小和文件数据大小。通过writev函数循环发送响应报文数据给浏览器端,根据返回值更新byte_have_sendiovec结构体的指针索引偏移和指向缓冲区剩余数据长度,并判断响应报文整体是否发送成功

writev单次发送成功,更新byte_to_sendbyte_have_send的大小,若响应报文整体发送成功,则取消mmap映射,并判断是否是长连接.

  • 长连接重置http类实例,注册读事件,不关闭连接
  • 短连接直接关闭连接

若writev单次发送不成功,因为套接字为非阻塞模式,需要判断是否是写缓冲区满了

  • 若不是因为缓冲区满了而失败,取消mmap映射,关闭连接
  • errno == EAGAIN表示套接字写缓冲区满了,更新iovec结构体的指针索引偏移和指向缓冲区剩余数据长度,并注册写事件,等待下一次写事件触发(套接字边沿触发工作模式下,当写缓冲区从不可写变为可写,触发epollout),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
* func:将响应报文写入到通信套接字的写缓冲区,发送给浏览器(客户)端
* note:proActor模式下,是主线程进行I/O操作数据完成,将m_read_buf/m_write_buf数据准备好
*/
bool http_conn::write()
{
int temp = 0;
// 表示响应报文为空,一般不会出现这种情况
if (bytes_to_send == 0)
{
// 重新为套接字注册EPOLLONESHOT事件,并且监听读事件
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
init();
return true;
}

while (1)
{
// 将响应报文的状态行、消息头、空行和响应正文发送给浏览器端
// 从process_write函数中指定的iovec向量缓冲区,写入数据到M_socket文件描述符的写缓冲区
// 发送数据给浏览器端
temp = writev(m_sockfd, m_iv, m_iv_count);
// 发送数据失败
if(temp < 0)
{
// 非阻塞模式下,判断errno == EAGAIN 即写缓冲区满
if(errno == EAGAIN)
{
// m_sockfd重新注册EPOLLONESHOT事件,监听写事件
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
return true;
}
// 取消do_request函数中开启的内存映射
unmap();
// return false,之后关闭连接
return false;
}

// 更新已经发送的字节数量
bytes_have_send += temp;
// 更新还未发送字节
bytes_to_send -= temp;

// iovec向量缓冲区第一个头部信息的数据已发送完,发送iovec第二个数据
if (bytes_have_send >= m_iv[0].iov_len)
{
// 不再继续发送头部信息
m_iv[0].iov_len = 0;
// iovec向量缓冲区 第二个指向URL访问资源文件的指针索引进行偏移
// iovec向量缓冲区 第一个指针指向的是m_write_buf,此时m_write_buf数据已经发送完毕
// 因此用 整体已经发送的字节数量bytes_have_send - m_write_buf中的字节数量可以得到 第二个指向URL访问资源文件的指针索引进行偏移
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
// 缓冲区剩余未发送数据的大小
m_iv[1].iov_len = bytes_to_send;
}
// 继续发送iovec向量缓冲区第一个头部信息的数据
else
{
// iovec向量缓冲区 第一个指向m_write_buf的指针索引进行偏移
m_iv[0].iov_base = m_write_buf + bytes_have_send;
// 缓冲区剩余未发送数据的大小
m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
}

// 判断条件,数据已全部发送完
if (bytes_to_send <= 0)
{
// 取消内存映射
unmap();
// 重新注册写事件
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);

// 浏览器的请求为长连接
if (m_linger)
{
// 重新初始化HTTP对象
init();
return true;
}
else
{
// return false,之后关闭连接
return false;
}
}
}
}