Bootstrap

【项目-轻量级Web Server http连接处理类】

http连接处理类

在readme文档中提到,该类通过主从状态机封装了http连接类,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机

有限状态机

有限状态机(Finite_state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件,在计算机科学中,有限状态机被广泛运用于建模、硬件电路系统设计、软件工程、编译器、网络协议等

有限状态机主要有三个特征:

  • 状态总数是有限的
  • 任意时刻只处在一种状态之中
  • 某种条件下,会从一个状态转变到另一个状态

http模块中的主从状态机解析

http报文的结构如下图所示
在这里插入图片描述

以一个具体的报文为例

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Connection: keep-alive

其中"POST"为请求方法,“/v3/cloudconf”为URL,“HTTP/1.1”为协议版本,之后到空行前的为请求头,空行后的为请求包体

头文件中分别定义了主状态机的三种状态和从状态机的三种状态

enum CHECK_STATE{
    CHECK_STATE_REQUESTLINE = 0,
    CHECK_STATE_HEADER,
    CHECK_STATE_CONTENT
};

主状态机的状态表明当前正在处理请求报文的哪一部分

enum LINE_STATUS{
    LINE_OK = 0,
    LINE_BAD,
    LINE_OPEN
};

从状态机的状态表明对请求报文当前部分的处理是否出现问题

请求报文的解析

当webserver的线程池有空闲线程时,某一线程调用process()来完成请求报文的解析及响应

void http_conn::process(){
	HTTP_CODE read_ret = process_read();
	if (read_ret == NO_REQUEST){									//表示请求不完整,需要继续接收请求数据
		modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);		//注册并监听读事件
		return;
	}
	bool write_ret = process_write(read_ret);					//调用process_write完成报文响应
	if (!write_ret){
		close_connect();
	}
	modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);			//注册并监听写事件
}

主状态机的状态转换使用process_read()封装,从状态机则用parse_line()封装
process_read()函数中,主状态机初始化从状态机,然后通过while循环实现主从状态机的状态转换以及循环处理报文内容。从状态机负责解析指定报文内容,并根据解析结果更改从状态机的状态;主状态机根据从状态机的返回值判断是否退出循环(终止处理/结束处理),并根据从状态机的驱动更改自身状态

从状态机

在HTTP报文中,每一行的数据由“\r”、“\n”作为结束字符,空行则是仅仅是字符“\r”、“\n”。因此,可以通过查找“\r”、“\n”将报文拆解成单独的行进行解析。从状态机负责读取buffer中的数据,将每行数据末尾的“\r”、“\n”符号改为“\0”,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析

//从状态机,用于分析出一行内容
//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line(){
	char temp;
	for (; m_checked_idx < m_read_idx; ++m_checked_idx){
		//m_read_idx指向缓冲区m_read_buf的数据末尾的下一个字节
		//m_checked_idx指向从状态机目前正在分析的字节
		temp = m_read_buf[m_checked_idx];		//temp:将要分析的字节
		if (temp == '\r'){// \r有可能是完整行
			if ((m_checked_idx + 1) == m_read_idx)		//该行仍有内容,并未读完
				return LINE_OPEN;
			else if (m_read_buf[m_checked_idx + 1] == '\n'){//出现换行符,说明该行读完
				m_read_buf[m_checked_idx++] = '\0';		// \r、\n都改为结束符\0
				m_read_buf[m_checked_idx++] = '\0';
				return LINE_OK;
			}
			return LINE_BAD;
		}
		else if (temp == '\n'){
			if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r'){//前一个字符是\r,则接收完整
				m_read_buf[m_checked_idx - 1] = '\0';
				m_read_buf[m_checked_idx++] = '\0';
				return LINE_OK;
			}
			return LINE_BAD;
		}
	}
	return LINE_OPEN;		//未发现换行符,说明读取的行不完整
}
/*
LINE_OK:完整读取一行
LINE_BAD:报文语法有误
LINE_OPEN:读取的行不完整
*/

主状态机

主状态机初始状态为CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机。在主状态机解析前,从状态机已经将每一行末尾的“\r”、“\n”符号改为“\0”,以便主状态机直接取出对应字符串进行处理

为了避免用户名和密码直接暴露在url中,项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装
而在POST请求报文中,消息体的末尾没有任何字符,不能使用从状态机的状态作为主状态机的while判断条件,因此在process_read()中额外添加了使用主状态机的状态进行判断的条件
解析完消息体后,报文的完整解析就完成了,但主状态机的状态还是CHECK_STATE_CONTENT,符合循环条件会再次进入循环,因此增加了“line_status == LINE_OK”并在完成消息体解析后将该变量更改为LNE_OPEN,此时可以跳出循环完成报文解析任务

//通过while循环,封装主状态机,对每一行进行循环处理
//此时,从状态机已经修改完毕,主状态机可以取出完整的行进行解析
http_conn::HTTP_CODE http_conn::process_read(){
	LINE_STATUS line_status = LINE_OK;						//初始化从状态机的状态
	HTTP_CODE ret = NO_REQUEST;
	char* text = 0;
	//判断条件,从状态机驱动主状态机
	while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK)){
		text = get_line();
		m_start_line = m_checked_idx;		//m_start_line:每一个数据行在m_read_buf中的起始位置
											//m_checked_idx:从状态机在m_read_buf中的读取位置
		LOG_INFO("%s", text);
		switch (m_check_state)						//三种状态转换逻辑
		{
		case CHECK_STATE_REQUESTLINE:				//正在分析请求行
		{
			ret = parse_request_line(text);			//解析请求行
			if (ret == BAD_REQUEST)
				return BAD_REQUEST;
			break;
		}
		case CHECK_STATE_HEADER:					//正在分析头部字段
		{
			ret = parse_headers(text);				//解析请求头
			if (ret == BAD_REQUEST)
				return BAD_REQUEST;
			else if (ret == GET_REQUEST)			//get请求,需要跳转到报文响应函数
			{
				return do_request();				//响应客户请求
			}
			break;
		}
		case CHECK_STATE_CONTENT:					//解析消息体
		{
			ret = parse_content(text);
			if (ret == GET_REQUEST)					//post请求,跳转到报文响应函数
				return do_request();
			line_status = LINE_OPEN;				//更新,跳出循环,代表解析完了消息体
			break;
		}
		default:
			return INTERNAL_ERROR;
		}
	}
	return NO_REQUEST;
}
解析请求行
  • 主状态机所处状态:CHECK_STATE_REQUESTLINE
  • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标以及HTTP版本号
  • 解析完成后主状态机的状态变为CHECK_STATE_HEADER
//解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char* text){
	m_url = strpbrk(text, " \t");//请求该行中最先含有空格和\t任一字符的位置并返回	
	if (!m_url)	{//没有目标字符,则代表报文格式有问题
		return BAD_REQUEST;
	}
	*m_url++ = '\0';										//将前面的数据取出,后移找到请求资源的第一个字符
	char* method = text;
	if (strcasecmp(method, "GET") == 0)	//确定请求方式
		m_method = GET;
	else if (strcasecmp(method, "POST") == 0){
		m_method = POST;
		cgi = 1;											
	}
	else
		return BAD_REQUEST;
	m_url += strspn(m_url, " \t");						//得到url地址
	m_version = strpbrk(m_url, " \t");
	if (!m_version)
		return BAD_REQUEST;
	*m_version++ = '\0';
	m_version += strspn(m_version, " \t");				//得到http版本号
	if (strcasecmp(m_version, "HTTP/1.1") != 0)
		return BAD_REQUEST;								//只接受HTTP/1.1版本 
	if (strncasecmp(m_url, "http://", 7) == 0)			{
		m_url += 7;
		m_url = strchr(m_url, '/');
	}

	if (strncasecmp(m_url, "https://", 8) == 0){
		m_url += 8;
		m_url = strchr(m_url, '/');
	}
	if (!m_url || m_url[0] != '/')				//不符合规则的报文
		return BAD_REQUEST;
	//当url为/时,显示判断界面
	if (strlen(m_url) == 1)						//url为/,显示欢迎界面
		strcat(m_url, "judge.html");
	m_check_state = CHECK_STATE_HEADER;			//主状态机状态转移
	return NO_REQUEST;
}
解析请求头
  • 主状态机所处状态:CHECK_STATE_HEADER
  • 判断是空行还是请求头
    • 是空行,则判断content-length是否为0
      • 不为零,则是POST请求
      • 为零,则是GET请求
    • 是请求头,则主要分析connection、content-length等字段
      • connection字段判断连接类型是长连接还是短连接
      • content-length用于读取POST请求的消息体长度
//解析http请求的一个头部信息
http_connect::HTTP_CODE http_connect::parse_headers(char* text){
	if (text[0] == '\0'){//判断是空头还是请求头
		if (m_content_length != 0){ //具体判断是get请求还是post请求
			m_check_state = CHECK_STATE_CONTENT;//post请求需要改变主状态机的状态
			return NO_REQUEST;
		}
		return GET_REQUEST;
	}
	else if (strncasecmp(text, "Connection:", 11) == 0){//解析头部连接字段
		text += 11;
		text += strspn(text, " \t");
		if (strcasecmp(text, "keep-alive") == 0){//判断是否为长连接
			m_linger = true;//为长连接,设置延迟关闭连接
		}
	}
	else if (strncasecmp(text, "Content-length:", 15) == 0){//解析请求头的内容长度字段		
		text += 15;
		text += strspn(text, " \t");
		m_content_length = atol(text);//atol(const char*str):将str所指的字符串转换为一个long int的长整数
	}
	else if (strncasecmp(text, "Host:", 5) == 0){//解析请求头部host字段
		text += 5;
		text += strspn(text, " \t");
		m_host = text;
	}
	else{
		LOG_INFO("oop!unknow header: %s", text);
	}
	return NO_REQUEST;
}
解析消息体
  • 主状态机所处状态:CHECK_STATE_CONTENT
  • 仅用于解析POST请求
  • 用于保存POST请求消息体,为登录和注册做准备
//判断http请求是否被完整读入
http_connect::HTTP_CODE http_connect::parse_content(char* text){
	if (m_read_idx >= (m_content_length + m_checked_idx)){
		text[m_content_length] = '\0';
		//POST请求中最后为输入的用户名和密码
		m_string = text;
		return GET_REQUEST;
	}
	return NO_REQUEST;
}

请求报文的响应

在完成请求报文的解析后,明确用户想要登录/注册,需要跳转到相应的界面、添加用户名、验证用户等等,并将相应的数据写入相应报文返回给浏览器,具体流程图如下:
在这里插入图片描述

在头文件中根据HTTP请求的处理结果初始化了几种情形

  • NO_REQUEST
    • 请求不完整,需要继续读取请求报文数据
    • 跳转主线程继续监测读事件
  • GET_REQUEST
    • 获得了完整的HTTP请求
    • 调用do_request完成请求资源映射
  • BAD_REQUEST
    • HTTP请求报文有语法错误或请求资源为目录
    • 跳转process_write完成响应报文
  • NO_RESOURCE
    • 请求资源不存在
    • 跳转process_write完成响应报文
  • FORBIDDEN_REQUEST
    • 请求资源禁止访问,没有读取权限
    • 跳转process_write完成响应报文
  • FILE_REQUEST
    • 请求资源可以正常访问
    • 跳转process_write完成响应报文
  • INTERNAL_ERROR
    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发
  • CLOSED_CONNECTION
    • 客户端关闭连接

do_request函数

在process_read()中完成请求报文的解析后,状态机调用do_request()函数,该函数负责处理功能逻辑,具体做法为:将网站根目录与url文件拼接,然后通过stat判断文件属性。浏览器网址栏中的字符,即url,可以将其抽象成ip:port/xxx,xxx通过html文件的action属性进行设置。另外为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。

//对客户请求进行响应
http_connect::HTTP_CODE http_connect::do_request(){
	strcpy(m_real_file, doc_root);					//将初始化的m_real_file赋值为网站根目录
	int len = strlen(doc_root);
	//printf("m_url:%s\n", m_url);
	const char* p = strrchr(m_url, '/');			//找到m_url中“/”的位置
	//处理cgi
	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);
		//将用户名和密码提取出来
		//user=123	password=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'){
			//如果是注册,先检测数据库中是否有重名的
			//没有重名的,进行增加数据
			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(pair<string, 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
				strcpy(m_url, "/logError.html");
		}
	}
	//如果请求资源为/0,表示跳转注册界面
	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));	//将网站目录和/register.html进行拼接,更新到m_real_file中
		free(m_url_real);
	}
	else if (*(p + 1) == '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));	//将网站目录和/log.html进行拼接,更新到m_real_file中
	
		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)		//通过stat获取请求资源文件信息,成功则将信息更新到m_file_stat结构体;失败返回NO_RESOURCE状态,表示资源不存在
		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;
	
	//os.open(file,flags[,mode]):打开一个文件
	int fd = open(m_real_file, O_RDONLY);			//以只读方式获取文件描述符,通过mmap将该文件映射到内存中		
	m_file_address = (char*)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	close(fd);										//避免文件描述符的浪费和占用
	return FILE_REQUEST;							//表示请求文件存在且可以访问
}
/*
open(file,flags[,mode])中flags的参数
O_RDONLY: 以只读的方式打开
O_WRONLY: 以只写的方式打开
O_RDWR : 以读写的方式打开
O_NONBLOCK: 打开时不阻塞
O_APPEND: 以追加的方式打开
O_CREAT: 创建并打开一个新文件
O_TRUNC: 打开一个文件并截断它的长度为零(必须有写权限)
O_EXCL: 如果指定的文件存在,返回错误
O_SHLOCK: 自动获取共享锁
O_EXLOCK: 自动获取独立锁
O_DIRECT: 消除或减少缓存效果
O_FSYNC : 同步写入
O_NOFOLLOW: 不追踪软链接
*/

stat函数用于取得指定文件的文件属性,并将文件属性存储在结构体stat里

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

//获取文件属性,存储在statbuf中
int stat(const char *pathname, struct stat *statbuf);
struct stat {
 	mode_t    st_mode;        /* 文件类型和权限 */
 	off_t     st_size;        /* 文件大小,字节数*/
};

mmap用于将文件等映射到内存,提高访问速度

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);
/*
start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址
length:映射区的长度,从被映射文件开头offset个字节算起
prot:期望的内存保护标志,不能与文件的打开模式冲突,可取以下几个值的或:PROT_READ(可读), PROT_WRITE(可写), PROT_EXEC(可执行), PROT_NONE(不可访问)
	PROT_READ表示页内容可以被读取
flags:指定映射对象的类型,映射选项和映射页是否可以共享,可以是以下几个常用值的或:MAP_FIXED(使用指定的映射起始地址), MAP_SHARED(与其它所有映射这个对象的进程共享映射空间), MAP_PRIVATE(建立一个写入时拷贝的私有映射)
	MAP_PEIVATE建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件
fd:有效地文件描述符,一般是由open()函数返回
offset:被映射对象内容的起点
*/

iovec定义向量元素,通常该结构用作一个多元素的数组

struct iovec {
    void      *iov_base;      /* starting address of buffer */
    size_t    iov_len;        /* size of buffer */
};
/*
iov_base指向数据的地址
iov_len表示数据的长度
*/

writev用于在一次函数调用中写多个非连续缓冲区,有时将该函数称为聚集写。若成功则返回已写的字节数,通常等于所有缓冲区长度之和;否则返回-1

#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
/*
filedes表示文件描述符
iov为io向量机制结构体iovec
iovcnt为结构体的个数
*/

特别注意: 循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然需要通过遍历iovec来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界iovec的io_base和io_len

process_write函数

根据do_request的返回状态,服务器子线程调用process_write向m_write_buf中写入响应报文,在生成响应报文的过程中主要调用add_reponse()函数更新m_write_idx和m_write_buf

以下几个函数为内部调用add_response函数更新m_write_idx指针和缓冲区m_write_buf中的内容

bool http_connect::add_response(const char* format, ...)
{
	if (m_write_idx >= WRITE_BUFFER_SIZE)					//如果写入内容超出m_write_buf大小则报错
		return false;
	va_list arg_list;										//定义可变参数列表
	va_start(arg_list, format);								//将变量arg_list初始化为传入参数
	int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);	//将数据format从可变参数列表写入缓冲区写,返回写入数据的长度
	if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))		//如果写入的数据长度超过缓冲区剩余空间,则报错
	{
		va_end(arg_list);
		return false;
	}
	m_write_idx += len;										//更新m_write_idx位置
	va_end(arg_list);										//清空可变参列表

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

}
  • add_status_line函数:添加状态行(http/1.1 状态码 状态消息)
//添加状态行
bool http_connect::add_status_line(int status, const char* title){
	return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}
  • add_headers函数:添加消息报头,内部调用add_content_length和add_linger函数
    • content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据
    • connection记录连接状态,用于告诉浏览器端保持长连接
bool http_connect::add_headers(int content_len){//添加消息报头,具体的添加文本长度、连接状态和空行
	return add_content_length(content_len) && add_linger() &&
		add_blank_line();
}

bool http_connect::add_content_length(int content_len){//添加Content-Length,表示响应报文的长度
	return add_response("Content-Length:%d\r\n", content_len);
}

bool http_connect::add_linger()	{//添加连接状态,通知浏览器端是保持连接还是关闭
	return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}

bool http_connect::add_content_type() {//添加文本类型,这里是html
	return add_response("Content-Type:%s\r\n", "text/html");
}
  • add_blank_line:添加空行
bool http_connect::add_blank_line()										//添加空行{
	return add_response("%s", "\r\n");
}
  • add_content:添加文本content
bool http_connect::add_content(const char* content){
	return add_response("%s", content);
}

响应报文分为两种,一种是请求文件的存在,通过io向量机制iovec,声明两个iovec,第一个指向m_write_buf,第二个指向mmap的地址m_file_address ;另一种是请求出错,这时候只申请一个iovec,指向m_write_buf

  • iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据,成员iov_len表示实际写入的长度(详见do_request函数部分中iovec结构的描述)

往响应报文里写的是服务器中html的文件数据,浏览器端对其进行解析、渲染并显示在浏览器页面上

bool http_connect::process_write(HTTP_CODE ret){
	switch (ret)
	{
	case INTERNAL_ERROR:														//内部错误,500
	{
		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:															//报文语法有误,404
	{
		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:														//资源没有访问权限,403
	{
		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://文件存在,200
	{
		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;	//第一个iovec指针指向响应报文缓冲区,长度指向m_write_idx
			m_iv[0].iov_len = m_write_idx;
			m_iv[1].iov_base = m_file_address;//第二个iovec指针指向mmap返回的文件指针,长度指向文件大小
			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;
		}
		else
		{
			const char* ok_string = "<html><body></body></html>";//请求的资源大小为0,则返回空白html文件
			add_headers(strlen(ok_string));
			if (!add_content(ok_string))
				return false;
		}
	}
	default:
		return false;
	}
	m_iv[0].iov_base = m_write_buf;												//除FILE_REQUEST状态外,其余状态只申请一个iovec,指向响应报文缓冲区
	m_iv[0].iov_len = m_write_idx;
	m_iv_count = 1;
	bytes_to_send = m_write_idx;
	return true;
}

write函数

服务器子线程调用process_write完成响应报文,随后注册epollout事件。服务器主线程检测写事件,并调用http_conn::write函数将响应报文发送给浏览器端

该函数具体逻辑如下:
生成响应报文时初始化byte_to_send(包括头部信息和文件数据),通过writev函数循环发送响应报文数据,根据返回值更新byte_have_send和iovec结构体的指针和长度,并判断响应报文整体是否发送成功

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

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

    • 短连接直接关闭连接

  • 若writev单次发送不成功,判断是否是写缓冲区满了

    • 若不是因为缓冲区满了而失败,取消mmap映射,关闭连接;

    • 若eagain则满了,更新iovec结构体的指针和长度,并注册写事件,等待下一次写事件触发(当写缓冲区从不可写变为可写,触发epollout),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性;

bool http_connect::write(){
	int temp = 0;
	//要发送的数据长度为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;		//更新已发送字节数
        //第一个iovec头部信息的数据已发送完,发送第二个iovec数据
		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 {	//继续发送第一个iovec头部信息的数据
			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);//在epoll树上重置EPOLLONESHOT事件
			if (m_linger){	//浏览器的请求为长连接
				init();	//重新初始化HTTP对象
				return true;
			}
			else {
				return false;
			}
		}
	}
}
;