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 { mode_t st_mode; off_t st_size; dev_t st_dev; ino_t st_ino; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; blksize_t st_blksize; blkcnt_t st_blocks; time_t st_atime; time_t st_mtime; time_t st_ctime; };
|
参数:
pathname
:指向要获取信息的文件路径的字符串指针
buf
:指向 stat
结构的指针,stat
结构将存储文件的状态信息
return:
- 返回值为
0
表示成功
- 返回值为
-1
表示失败,并设置 errno
来指示错误类型
2.内存映射
如果想要实现进程间通信,可以通过函数创建一块内存映射区,和管道不同的是管道对应的内存空间在内核中,而内存映射区对应的内存空间在进程的用户区(用于加载动态库的那个区域),也就是说进程间通信使用的内存映射区不是一块,而是在每个进程内部都有一块
由于每个进程的地址空间是独立的,各个进程之间也不能直接访问对方的内存映射区,需要通信的进程需要将各自的内存映射区和同一个磁盘文件进行映射,这样进程之间就可以通过磁盘文件这个唯一的桥梁完成数据的交互了

如上图所示:磁盘文件数据可以完全加载到进程的内存映射区也可以部分加载到进程的内存映射区,当进程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()
第二个参数相同即可
返回值:
(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() { int fd = open(FILENAME, O_RDWR); printf("1111");
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() { int fd = open(FILENAME, O_RDWR);
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++ 中用于描述一个向量(数组)缓冲区。它通常与 readv
和 writev
函数一起使用,用于执行分散/聚集 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
结构体数组的指针
iovcnt
:iovec
结构体数组中的元素个数
返回值:
- 返回实际读取的字节数
- 返回 -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
结构体数组的指针
iovcnt
:iovec
结构体数组中的元素个数
返回值:
- 返回实际写入的字节数
- 返回 -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];
struct iovec iov[3]; iov[0].iov_base = buf1; iov[0].iov_len = sizeof(buf1); iov[1].iov_base = buf2; iov[1].iov_len = sizeof(buf2); iov[2].iov_base = buf3; iov[2].iov_len = sizeof(buf3);
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";
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);
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>
int sum(int count, ...) { va_list args; va_start(args, count);
int total = 0; for (int i = 0; i < count; ++i) { total += va_arg(args, int); }
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
void write_formatted(char* buffer, int buffer_size, const char* format, ...) { va_list args; va_start(args, format);
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(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_once
和http_conn::write
完成数据的读取与发送

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
- 服务器内部错误,该结果在主状态机逻辑
switch
的default
下,一般不会触发
三、代码详解
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/xxx
,xxx
通过html
文件的action
属性进行设置
m_url
为请求报文中解析出的请求资源,以/
开头,也就是/xxx
,项目中解析后的m_url
有8种情况:
/
GET
请求,跳转到judge.html
,即欢迎访问页面(在parse_request_line
处理请求行时,当请求报文的请求头的url
只有/
则将m_url
与judge.html
界面进行拼接)
/0
POST
请求,跳转到register.html
,即注册页面
/1
/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
|
http_conn::HTTP_CODE http_conn::do_request() { strcpy(m_real_file, doc_root); int len = strlen(doc_root);
const char *p = strrchr(m_url, '/');
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3')) { char flag = m_url[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);
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') { 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, "')");
if (users.find(name) == users.end()) { m_lock.lock(); 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"); } else if (*(p + 1) == '2') { if (users.find(name) != users.end() && users[name] == password) strcpy(m_url, "/welcome.html"); else strcpy(m_url, "/logError.html"); }
}
if (*(p + 1) == '0') { char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/register.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real); }
else if (*(p + 1) == '1') { char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/log.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real); }
else if (*(p + 1) == '5') { char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/picture.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real); }
else if (*(p + 1) == '6') { char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/video.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real); }
else if (*(p + 1) == '7') { char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/fans.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real)); free(m_url_real); } else strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
if (stat(m_real_file, &m_file_stat) < 0) return NO_RESOURCE;
if (!(m_file_stat.st_mode & S_IROTH)) return FORBIDDEN_REQUEST;
if (S_ISDIR(m_file_stat.st_mode)) return BAD_REQUEST;
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
|
bool http_conn::process_write(HTTP_CODE ret) { switch (ret) { case INTERNAL_ERROR: { add_status_line(500, error_500_title); add_headers(strlen(error_500_form)); if (!add_content(error_500_form)) return false; break; }
case BAD_REQUEST: { add_status_line(404, error_404_title); add_headers(strlen(error_404_form)); if (!add_content(error_404_form)) return false; break; }
case FORBIDDEN_REQUEST: { add_status_line(403, error_403_title); add_headers(strlen(error_403_form)); if (!add_content(error_403_form)) return false; break; }
case FILE_REQUEST: { add_status_line(200, ok_200_title); if (m_file_stat.st_size != 0) { add_headers(m_file_stat.st_size);
m_iv[0].iov_base = m_write_buf; m_iv[0].iov_len = m_write_idx; m_iv[1].iov_base = m_file_address; m_iv[1].iov_len = m_file_stat.st_size; m_iv_count = 2; bytes_to_send = m_write_idx + m_file_stat.st_size; return true; } break; }
default: return false; } 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_length
和add_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
|
bool http_conn::add_response(const char *format, ...) { if (m_write_idx >= WRITE_BUFFER_SIZE) return false; va_list arg_list; va_start(arg_list, 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(arg_list); return false; } 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
|
bool http_conn::add_status_line(int status, const char *title) { return add_response("%s %d %s\r\n", "HTTP/1.1", status, title); }
bool http_conn::add_headers(int content_len) { return add_content_length(content_len) && add_linger() && add_blank_line(); }
bool http_conn::add_content_length(int content_len) { return add_response("Content-Length:%d\r\n", content_len); }
bool http_conn::add_content_type() { return add_response("Content-Type:%s\r\n", "text/html"); }
bool http_conn::add_linger() { return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close"); }
bool http_conn::add_blank_line() { return add_response("%s", "\r\n"); }
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_send
和iovec
结构体的指针索引偏移和指向缓冲区剩余数据长度,并判断响应报文整体是否发送成功
若writev
单次发送成功,更新byte_to_send
和byte_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
|
bool http_conn::write() { int temp = 0; if (bytes_to_send == 0) { modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); init(); return true; }
while (1) { temp = writev(m_sockfd, m_iv, m_iv_count); if(temp < 0) { if(errno == EAGAIN) { modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode); return true; } unmap(); return false; }
bytes_have_send += temp; bytes_to_send -= temp;
if (bytes_have_send >= m_iv[0].iov_len) { m_iv[0].iov_len = 0; m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx); m_iv[1].iov_len = bytes_to_send; } else { 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) { init(); return true; } else { return false; } } } }
|