web服务器HTTP协议处理部分
// 处理http_conn::HTTP_CODE http_conn::process_read(){LINE_STATUS line_status = LINE_OK;//行解析状态HTTP_CODE ret = NO_REQUEST;//HTTP请求解析结果char *text = 0;//指向当前解析到的行数据的指针LOG_TRACE << "in process_read: m_check_state: " << m_check_state << " line_status: " << line_status;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;LOG_TRACE << "while => pares_line : text: " << text;LOG_TRACE << "m_start_line: " << m_start_line;LOG_TRACE << "m_check_state: " << m_check_state << " " << getCheckState(m_check_state);switch (m_check_state){case CHECK_STATE_REQUESTLINE:{ret = parse_request_line(text);LOG_INFO << "parse_request_line ret: " << static_cast<int>(ret) << " " << Get_HTTPCODE(ret);if (ret == BAD_REQUEST){return BAD_REQUEST;}break;}case CHECK_STATE_HEADER:{ret = parse_headers(text);LOG_INFO<<" parse_headers(text) text: "<<text<<" ret: "<<static_cast<int>(ret) << " " << Get_HTTPCODE(ret);if (ret == BAD_REQUEST){return BAD_REQUEST;}else if (ret == GET_REQUEST){return do_request();}break;}case CHECK_STATE_CONTENT:{ret = parse_content(text);if (ret == GET_REQUEST){return do_request();}line_status = LINE_OPEN;break;}default:return INTERNAL_ERROR;}}LOG_TRACE << " end http_conn::process_read ";return NO_REQUEST;}
这段代码是http_conn
类中的process_read
方法,主要功能是处理 HTTP 请求的读取和解析过程,是 HTTP 请求处理的核心逻辑之一。
核心作用
按照 HTTP 协议规范,逐步解析请求数据(请求行、请求头、请求体),并根据解析状态决定后续操作(如调用do_request
处理请求或返回错误)。
关键变量
line_status
:行解析状态(LINE_OK
表示解析到完整行,LINE_BAD
表示格式错误,LINE_OPEN
表示未解析完成)。ret
:HTTP 请求解析结果(NO_REQUEST
表示需继续解析,GET_REQUEST
表示解析完成,BAD_REQUEST
等表示错误)。text
:指向当前解析到的行数据的指针。m_check_state
:当前解析阶段(状态机状态),包括:CHECK_STATE_REQUESTLINE
:解析请求行阶段CHECK_STATE_HEADER
:解析请求头阶段CHECK_STATE_CONTENT
:解析请求体阶段
逻辑流程
循环解析行数据通过
while
循环持续解析请求数据中的行:- 条件 1:若当前处于解析请求体阶段(
CHECK_STATE_CONTENT
)且上一行解析正常(LINE_OK
),继续解析。 - 条件 2:调用
parse_line()
解析新行,若成功解析到完整行(LINE_OK
),继续处理。
- 条件 1:若当前处于解析请求体阶段(
按阶段解析 HTTP 请求根据
m_check_state
的不同,分阶段处理:请求行解析(
CHECK_STATE_REQUESTLINE
):- 调用
parse_request_line(text)
解析请求方法(GET/POST)、URL、HTTP 版本。 - 若解析失败(返回
BAD_REQUEST
),直接返回错误。 - 解析成功后,状态机自动进入
CHECK_STATE_HEADER
(请求头解析阶段)。
- 调用
请求头解析(
CHECK_STATE_HEADER
):- 调用
parse_headers(text)
解析各请求头(如Connection
、Content-Length
等)。 - 若解析失败(
BAD_REQUEST
),返回错误。 - 若解析完成(
GET_REQUEST
,如无请求体或请求头已读完),调用do_request()
处理请求。
- 调用
请求体解析(
CHECK_STATE_CONTENT
):- 调用
parse_content(text)
解析请求体数据(如 POST 表单数据)。 - 若解析完成(
GET_REQUEST
),调用do_request()
处理请求。 - 若未完成,将
line_status
设为LINE_OPEN
,等待更多数据。
- 调用
异常处理
- 若
m_check_state
为未知状态,返回INTERNAL_ERROR
(服务器内部错误)。 - 循环退出(如数据不完整)时,返回
NO_REQUEST
,表示需继续读取数据。
- 若
总结
该方法通过状态机模式分阶段解析 HTTP 请求,配合parse_line
(行解析)、parse_request_line
(请求行解析)等辅助函数,逐步完成请求的解析工作。解析过程中若发现错误则立即返回,若解析完成则调用do_request
处理具体业务(如返回静态资源或处理 CGI 请求),是 HTTP 服务器处理请求的核心逻辑实现。
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, '/');LOG_INFO<<"m_real_file: "<<m_real_file;LOG_INFO<<"m_url: "<<m_url;// 处理cgiif (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);LOG_INFO<<"m_real_file: "<<m_real_file;free(m_url_real);// 将用户名和密码提取出来// user=123&passwd=123char 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");elsestrcpy(m_url, "/registerError.html");}elsestrcpy(m_url, "/registerError.html");}// 如果是登录,直接判断// 若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0else if (*(p + 1) == '2'){if (users.find(name) != users.end() && users[name] == password)strcpy(m_url, "/welcome.html");elsestrcpy(m_url, "/logError.html");}}LOG_INFO<<"p "<<p;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);}elsestrncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);if (stat(m_real_file, &m_file_stat) < 0)//若 stat 调用失败(返回值 <0),说明文件不存在return NO_RESOURCE;//获取 m_real_file 对应的文件元信息(如是否存在、类型、权限等),存入 m_file_stat 结构体if (!(m_file_stat.st_mode & S_IROTH))//检查文件是否允许其他用户读取return FORBIDDEN_REQUEST;if (S_ISDIR(m_file_stat.st_mode))//用于判断文件是否为目录return BAD_REQUEST;LOG_INFO<<" int fd = open(m_real_file, O_RDONLY): "<<m_real_file;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);LOG_INFO<<"m_file_address: "<<m_file_address;close(fd);LOG_TRACE<<" end: ";return FILE_REQUEST;}
这段代码是http_conn
类中的do_request
方法,主要功能是处理 HTTP 请求,根据请求的 URL 路径确定要访问的资源文件,处理 CGI 相关的登录 / 注册逻辑,并准备好对应的文件资源供后续响应使用。
核心流程概述
- 基于请求的 URL 路径(
m_url
)和网站根目录(doc_root
),构建实际要访问的本地文件路径(m_real_file
)。 - 处理 CGI 相关的登录(
URL
路径含/2
)和注册(URL
路径含/3
)请求,与数据库交互验证或插入用户信息,并跳转至对应结果页面。 - 处理其他静态页面请求(如注册页
/0
、登录页/1
、图片页/5
等),映射到对应的 HTML 文件。 - 验证目标文件的存在性、访问权限,最终通过内存映射(
mmap
)加载文件内容,为响应做准备。
关键代码解析
1. 初始化文件路径
strcpy(m_real_file, doc_root); // 将网站根目录(如/var/www/)复制到实际文件路径
int len = strlen(doc_root); // 获取根目录长度
const char *p = strrchr(m_url, '/'); // 找到URL中最后一个'/'的位置(用于解析路径)
m_real_file
:存储本地文件系统中实际要访问的文件路径。strrchr(m_url, '/')
:定位 URL 中最后一个/
,用于提取路径中的关键标识(如/2
、/3
等)。
2. 处理 CGI 登录 / 注册请求(cgi == 1
)
当cgi
标志为 1,且 URL 路径中最后一个/
后为2
(登录)或3
(注册)时,触发 CGI 处理逻辑:
(1)解析 URL 与表单数据
// 提取URL中的标志(如URL为"/2xxx"时,标志为'2')
char flag = m_url[1];
// 构建实际访问的CGI文件路径(忽略标志部分)
char *m_url_real = (char *)malloc(...);
strcpy(m_url_real, "/");
strcat(m_url_real, m_url + 2); // 跳过标志部分(如从"/2login"中提取"login")
strncpy(m_real_file + len, m_url_real, ...); // 拼接根目录与实际路径
(2)提取用户名和密码(表单数据格式:user=xxx&passwd=xxx
)
char name[100], password[100];
// 从m_string(表单数据)中提取用户名(跳过"user=")
for (i = 5; m_string[i] != '&'; ++i)name[i - 5] = m_string[i];
name[i - 5] = '\0';// 提取密码(跳过"&passwd=")
for (i = i + 10; m_string[i] != '\0'; ++i, ++j)password[j] = m_string[i];
password[j] = '\0';
- 假设表单数据为
user=alice&passwd=123
,则name
为alice
,password
为123
。
(3)注册逻辑(*(p + 1) == '3'
)
// 构建SQL插入语句:INSERT INTO user(username, passwd) VALUES('name', 'password')
char *sql_insert = (char *)malloc(...);
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, "')");// 检查用户是否已存在(users为内存中的用户映射表)
if (users.find(name) == users.end()) {m_lock.lock(); // 加锁保证线程安全int res = mysql_query(mysql, sql_insert); // 执行SQL插入users.insert(pair<string, string>(name, password)); // 更新内存映射表m_lock.unlock();// 根据插入结果跳转页面(成功→登录页,失败→注册失败页)strcpy(m_url, res ? "/registerError.html" : "/log.html");
} else {strcpy(m_url, "/registerError.html"); // 用户名已存在
}
(4)登录逻辑(*(p + 1) == '2'
)
// 检查用户名是否存在且密码匹配(users为内存映射表)
if (users.find(name) != users.end() && users[name] == password)strcpy(m_url, "/welcome.html"); // 登录成功→欢迎页
elsestrcpy(m_url, "/logError.html"); // 失败→登录错误页
3. 处理静态页面请求
根据 URL 中最后一个/
后的标识,映射到对应的 HTML 文件:
if (*(p + 1) == '0') // 标识0→注册页面strcpy(m_url_real, "/register.html");
else if (*(p + 1) == '1') // 标识1→登录页面strcpy(m_url_real, "/log.html");
else if (*(p + 1) == '5') // 标识5→图片页面strcpy(m_url_real, "/picture.html");
else if (*(p + 1) == '6') // 标识6→视频页面strcpy(m_url_real, "/video.html");
else if (*(p + 1) == '7') // 标识7→粉丝页面strcpy(m_url_real, "/fans.html");
else // 其他URL直接拼接路径strncpy(m_real_file + len, m_url, ...);
4. 验证文件并加载资源
// 检查文件是否存在
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; // 是目录,请求无效// 打开文件并通过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; // 成功准备好文件资源
总结
该方法是 HTTP 请求处理的核心环节,负责解析请求路径、处理动态 CGI 逻辑(登录 / 注册)、映射静态资源,并通过文件验证和内存映射确保资源可被正确响应。代码中通过 URL 路径的标识字符(如2
、3
、0
等)实现不同功能的路由,同时使用内存映射提升文件读取效率。
mmap函数建立高效通道,让你在内存里直接查看和修改文件内容,而这个改动会自动同步到磁盘上。
传统方式 (read/write): 像用桶从井里(磁盘)打水,倒进你家的水缸(内存)里。你要用水(访问数据)只能从水缸里取。想改变井里的水,得把水缸的水打回井里。
内存映射 (mmap): 像给井装了一个魔法镜面。你低头看镜子(内存),直接就看到井底(磁盘)的水,并且你伸手就能直接碰到井水。你在镜面上做的任何改动(修改内存),井里的水(磁盘文件)会自动跟着改变。
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){LOG_INFO << "types_to_send : " << bytes_to_send;LOG_INFO << "m_sockfd: " << m_sockfd;temp = writev(m_sockfd, m_iv, m_iv_count);LOG_INFO<<"temp: "<<temp;LOG_INFO<<"m_iv[0]: "<<(char*)(m_iv[0].iov_base)<<" "<<m_iv[0].iov_len;LOG_INFO<<"m_iv[1]: "<<(char*)(m_iv[1].iov_base)<<" "<<m_iv[1].iov_len;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;}}}}
http_conn
类中的write()
方法,用于处理 HTTP 响应数据的发送,是 Web 服务器向客户端返回数据的核心逻辑。
核心功能
通过writev
系统调用高效发送响应数据(包括响应头和响应体),支持非阻塞 I/O 模式,处理发送过程中的部分写入、缓冲区满等情况,并根据连接状态维护 epoll 事件监听模式。
关键变量
bytes_to_send
:待发送的总字节数(响应头 + 响应体)。bytes_have_send
:已发送的字节数。m_iv
:iovec
结构体数组,用于分散 / 聚集 I/O(writev
的参数),通常包含两部分:m_iv[0]
:指向响应头缓冲区(m_write_buf
)及长度。m_iv[1]
:指向响应体(如通过mmap
映射的文件内容m_file_address
)及长度。
m_iv_count
:m_iv
数组的元素个数(通常为 1 或 2)。m_linger
:连接是否保持长连接(keep-alive
)的标志。
逻辑流程
检查是否无数据可发若
bytes_to_send == 0
(所有数据已发送完毕):- 将套接字
m_sockfd
的 epoll 事件重新设置为EPOLLIN
(等待新的请求)。 - 初始化连接状态(
init()
),返回true
。
- 将套接字
循环发送数据通过
while(1)
循环持续发送数据,直到所有数据发送完毕或遇到错误:调用
writev
发送数据:writev
可一次性发送分散在多个缓冲区的数据(响应头 + 响应体),提高效率。处理发送错误:
- 若
temp < 0
(发送失败):- 若错误码为
EAGAIN
(缓冲区暂时不可用,非阻塞 I/O 的典型情况):将套接字事件改为EPOLLOUT
(等待可写),返回true
(后续可继续发送)。 - 其他错误:释放
mmap
映射的文件(unmap()
),返回false
(连接需关闭)。
- 若错误码为
- 若
更新发送进度:
bytes_have_send
累加已发送字节数,bytes_to_send
减去已发送字节数。- 调整缓冲区指针(根据已发送数据量更新
m_iv
的指向和长度):- 若已发送数据超过响应头长度(
bytes_have_send >= m_iv[0].iov_len
):响应头已发完,后续只需发送响应体,调整m_iv[1]
指向剩余未发的响应体部分。 - 否则:响应头未发完,调整
m_iv[0]
指向剩余未发的响应头部分。
- 若已发送数据超过响应头长度(
发送完成处理:
- 当
bytes_to_send <= 0
(所有数据发送完毕):- 释放
mmap
映射(unmap()
)。 - 将套接字事件改回
EPOLLIN
(等待新请求)。 - 若为长连接(
m_linger == true
):初始化连接状态,返回true
(保持连接)。 - 若为短连接:返回
false
(触发连接关闭)。
- 释放
- 当
技术特点
- 分散 / 聚集 I/O:使用
writev
同时发送响应头(内存缓冲区)和响应体(文件映射),减少系统调用次数,提升性能。 - 非阻塞 I/O 适配:通过检测
EAGAIN
错误,配合 epoll 事件切换(EPOLLOUT
),实现非阻塞模式下的高效数据发送。 - 连接状态管理:根据
m_linger
标志决定是否保持连接,符合 HTTP 的keep-alive
机制。 - 资源释放:发送完成后及时释放
mmap
映射的文件资源,避免内存泄漏。
总结
该方法是 HTTP 响应发送的核心实现,通过高效的 I/O 操作和事件管理,确保响应数据可靠发送,并根据连接类型维护连接状态,是 Web 服务器处理输出的关键逻辑。
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;}
这段代码的作用是在非阻塞 I/O 模式下动态调整发送缓冲区,确保 HTTP 响应数据(包括响应头和响应体)能分批次正确发送。我们通过一个具体例子来理解:
假设场景
假设服务器需要向客户端发送一个 HTTP 响应,包含两部分数据:
- 响应头(m_write_buf):长度为
500字节
(例如包含HTTP/1.1 200 OK
、Content-Length
等信息)。 - 响应体(m_file_address):长度为
2000字节
(例如一个 HTML 文件内容)。
初始化时,m_iv
(iovec 数组,用于分散写)的状态为:
m_iv[0].iov_base = m_write_buf
(指向响应头起始地址)m_iv[0].iov_len = 500
(响应头总长度)m_iv[1].iov_base = m_file_address
(指向响应体起始地址)m_iv[1].iov_len = 2000
(响应体总长度)bytes_to_send = 500 + 2000 = 2500
(总待发送字节数)bytes_have_send = 0
(已发送字节数)
第一次发送(部分数据)
调用writev
后,假设只成功发送了300字节
(非阻塞模式下常见,数据未一次性发完):
bytes_have_send = 300
bytes_to_send = 2500 - 300 = 2200
此时判断bytes_have_send (300) < m_iv[0].iov_len (500)
,执行else
分支:
- 调整响应头缓冲区指针:
m_iv[0].iov_base = m_write_buf + 300
(从响应头的第 300 字节开始发送剩余部分) - 调整响应头剩余长度:
m_iv[0].iov_len = 500 - 300 = 200
(还剩 200 字节响应头未发)
m_iv
状态变为:
m_iv[0]
:指向响应头第 300 字节,长度 200m_iv[1]
:仍指向响应体起始地址,长度 2000
第二次发送(完成响应头,开始响应体)
再次调用writev
,发送剩余的 200 字节响应头和部分响应体,假设共发送了1000字节
:
- 其中 200 字节来自响应头,800 字节来自响应体
bytes_have_send = 300 + 1000 = 1300
bytes_to_send = 2200 - 1000 = 1200
此时判断bytes_have_send (1300) >= m_iv[0].iov_len (500)
(响应头已发完),执行if
分支:
- 标记响应头缓冲区已用完:
m_iv[0].iov_len = 0
- 调整响应体缓冲区指针:
m_iv[1].iov_base = m_file_address + (1300 - 500) = m_file_address + 800
(从响应体的第 800 字节开始发送剩余部分) - 调整响应体剩余长度:
m_iv[1].iov_len = 1200
(还剩 1200 字节响应体未发)
m_iv
状态变为:
m_iv[0]
:长度 0(已用完)m_iv[1]
:指向响应体第 800 字节,长度 1200
第三次发送(完成所有数据)
最后一次调用writev
,发送剩余的 1200 字节响应体:
bytes_have_send = 1300 + 1200 = 2500
bytes_to_send = 0
(所有数据发送完成)
总结
通过动态调整m_iv
的指针和长度,代码确保了:
- 即使数据分多次发送,也能从上次中断的位置继续发送(避免重复发送或遗漏)。
- 优先发送响应头,再发送响应体,符合 HTTP 协议的传输顺序。
- 高效利用
writev
的分散写特性,减少系统调用次数。
这种逻辑是处理非阻塞网络 I/O 的典型实现,适配了网络传输中 “数据分批到达 / 发送” 的场景。
//响应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: " << m_write_buf;return true;}
这段代码定义了http_conn
类中的add_response
方法,其核心功能是向 HTTP 响应缓冲区中格式化添加数据(如响应头、响应体内容等),并确保缓冲区不溢出。以下是详细解析:
函数作用
add_response
是一个可变参数函数,用于将格式化的字符串(如 HTTP 状态行、头部字段、HTML 内容等)追加到响应缓冲区m_write_buf
中,为后续发送 HTTP 响应做准备。
代码逻辑拆解
缓冲区溢出检查
if (m_write_idx >= WRITE_BUFFER_SIZE) {return false; }
m_write_idx
:记录当前响应缓冲区中已使用的字节数(即下一个待写入位置的索引)。WRITE_BUFFER_SIZE
:响应缓冲区的总大小(宏定义,固定值)。- 若当前已使用空间超过缓冲区总大小,直接返回
false
,表示写入失败。
处理可变参数
va_list arg_list; va_start(arg_list, format);
va_list
:用于存储可变参数列表的类型。va_start
:初始化可变参数列表,format
是最后一个固定参数,作为可变参数的起始标记。- 目的是解析函数传入的可变参数(如
add_response("Content-Length:%d\r\n", len)
中的len
)。
格式化写入缓冲区
int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
vsnprintf
:格式化可变参数并写入指定缓冲区,返回实际写入的字节数(不包含终止符\0
)。- 参数说明:
- 第一个参数:写入位置(从缓冲区当前已用位置
m_write_idx
开始)。 - 第二个参数:最大可写入字节数(避免溢出,计算方式为 “缓冲区总大小 - 已用大小 - 1”,预留 1 字节给终止符
\0
)。 - 第三个参数:格式化字符串(如
"HTTP/1.1 %d %s\r\n"
)。 - 第四个参数:可变参数列表。
- 第一个参数:写入位置(从缓冲区当前已用位置
检查写入结果
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)) {va_end(arg_list);return false; }
- 若
vsnprintf
返回的实际写入长度len
大于等于最大可写入字节数,说明缓冲区空间不足,写入失败,清理参数列表并返回false
。
- 若
更新缓冲区状态
m_write_idx += len; va_end(arg_list);
- 成功写入后,更新
m_write_idx
(累加实际写入的字节数)。 va_end
:清理可变参数列表,释放资源。
- 成功写入后,更新
日志输出与返回
LOG_INFO << "request: " << m_write_buf; return true;
- 输出当前缓冲区内容到日志(便于调试)。
- 返回
true
表示数据成功添加到缓冲区。
示例场景
例如在构建 HTTP 响应头时:
add_response("HTTP/1.1 %d %s\r\n", 200, "OK");
- 函数会将
"HTTP/1.1 200 OK\r\n"
格式化后追加到m_write_buf
中,并更新m_write_idx
记录写入长度。 - 若缓冲区空间足够,返回
true
;否则返回false
,避免缓冲区溢出。
总结
add_response
是 HTTP 响应构建的核心工具函数,通过可变参数和格式化字符串,安全地向缓冲区添加响应数据,同时通过严格的边界检查确保内存安全,为后续writev
发送响应数据提供了正确的缓冲区内容。
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);}
这段代码是http_conn
类中用于构建 HTTP 响应头部和内容的一系列辅助函数,它们基于之前提到的add_response
方法,封装了 HTTP 响应各组成部分的生成逻辑,确保响应格式符合 HTTP 协议规范。
各函数功能解析
add_status_line
:生成 HTTP 响应状态行bool http_conn::add_status_line(int status, const char *title) {return add_response("%s %d %s\r\n", "HTTP/1.1", status, title); }
- 作用:构造 HTTP 响应的第一行(状态行),格式为
HTTP版本 状态码 状态描述\r\n
。 - 示例:调用
add_status_line(200, "OK")
会生成HTTP/1.1 200 OK\r\n
,表示请求成功。 - 参数:
status
是 HTTP 状态码(如 200、404),title
是对应的状态描述(如 "OK"、"Not Found")。
- 作用:构造 HTTP 响应的第一行(状态行),格式为
add_headers
:批量添加 HTTP 响应头部字段bool http_conn::add_headers(int content_len) {return add_content_length(content_len) && add_linger() && add_blank_line(); }
- 作用:组合调用多个头部生成函数,一次性添加核心响应头部,包括内容长度、连接状态,并以空行结束头部区域。
- 逻辑:通过
&&
确保所有头部字段都添加成功才返回true
,只要有一个失败则整体失败。
add_content_length
:添加内容长度头部bool http_conn::add_content_length(int content_len) {return add_response("Content-Length:%d\r\n", content_len); }
- 作用:生成
Content-Length: 长度值\r\n
头部,告知客户端响应体的字节数。 - 示例:
add_content_length(1024)
生成Content-Length:1024\r\n
,帮助客户端判断数据是否接收完整。
- 作用:生成
add_content_type
:添加内容类型头部bool http_conn::add_content_type() {return add_response("Content-Type:%s\r\n", "text/html"); }
- 作用:生成
Content-Type: 类型\r\n
头部,这里固定为text/html
,表示响应体是 HTML 文本。 - 说明:实际应用中可能根据文件类型(如图片、视频)动态修改类型值(如
image/jpeg
),此处简化为固定 HTML 类型。
- 作用:生成
add_linger
:添加连接状态头部bool http_conn::add_linger() {return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close"); }
- 作用:生成
Connection: 状态\r\n
头部,决定 TCP 连接是否保持(长连接 / 短连接)。 - 逻辑:根据成员变量
m_linger
判断:- 若
m_linger为true
:生成Connection: keep-alive
,表示连接保持,可复用。 - 若
m_linger为false
:生成Connection: close
,表示响应后关闭连接。
- 若
- 作用:生成
add_blank_line
:添加头部结束标记bool http_conn::add_blank_line() {return add_response("%s", "\r\n"); }
- 作用:生成
\r\n
(空行),这是 HTTP 协议规定的头部与响应体的分隔符,标志头部区域结束。
- 作用:生成
add_content
:添加响应体内容bool http_conn::add_content(const char *content) {return add_response("%s", content); }
- 作用:将具体内容(如 HTML 文本、错误信息)添加到响应体中。
- 示例:调用
add_content("<h1>Hello World</h1>")
会将 HTML 内容追加到响应缓冲区。
整体逻辑与作用
这些函数通过封装add_response
,实现了 HTTP 响应的模块化构建:
- 先调用
add_status_line
生成状态行; - 再通过
add_headers
添加核心头部字段(内容长度、连接状态等); - 最后通过
add_content
添加响应体内容。
整个过程严格遵循 HTTP 协议格式,确保客户端能正确解析响应。例如,一个完整的成功响应构建流程可能是:
add_status_line(200, "OK"); // 状态行
add_headers(1024); // 头部字段(含长度、连接状态、空行)
add_content("<html>...</html>"); // 响应体内容
bool http_conn::process_write(HTTP_CODE ret){LOG_TRACE << " ret : " << static_cast<int>(ret) << " " << Get_HTTPCODE(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);LOG_INFO<<"m_file_stat.st_size: "<<m_file_stat.st_size;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;LOG_INFO<<"bytes_to_send: "<<bytes_to_send;LOG_INFO<<"m_write_buf: "<<m_write_buf;LOG_INFO<<"m_write_idx: "<<m_write_idx;LOG_INFO<<"m_file_address: "<<m_file_address;LOG_INFO<<"m_file_stat.st_size: "<<m_file_stat.st_size;return true;}else{const char *ok_string = "<html><body></body></html>";add_headers(strlen(ok_string));if (!add_content(ok_string))return false;}}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;}
这段代码是http_conn
类中的process_write
方法,核心功能是根据 HTTP 请求处理的结果(HTTP_CODE
)构建对应的 HTTP 响应数据,为后续通过write
方法发送响应做准备。以下是详细解析:
函数作用
process_write
接收一个HTTP_CODE
类型的参数ret
(表示请求处理的结果,如成功、错误等),根据不同的结果生成对应的 HTTP 响应内容(包括状态行、响应头、响应体),并初始化用于发送数据的缓冲区结构(iovec
数组)和发送状态变量(待发送字节数等)。
核心逻辑拆解
1. 基于HTTP_CODE
的分支处理
通过switch
语句根据ret
的值(请求处理结果)生成不同的响应:
INTERNAL_ERROR
(500 服务器内部错误)add_status_line(500, error_500_title); // 添加状态行:HTTP/1.1 500 Internal Error add_headers(strlen(error_500_form)); // 添加响应头(含内容长度等) if (!add_content(error_500_form)) // 添加响应体(500错误页面HTML)return false;
- 当服务器处理请求时发生内部错误(如代码异常),生成 500 响应,包含预设的错误页面内容。
BAD_REQUEST
(404 资源未找到)add_status_line(404, error_404_title); // 状态行:HTTP/1.1 404 Not Found add_headers(strlen(error_404_form)); // 响应头(含错误页面长度) if (!add_content(error_404_form)) // 响应体(404错误页面HTML)return false;
- 当请求的资源不存在时,生成 404 响应。
FORBIDDEN_REQUEST
(403 禁止访问)add_status_line(403, error_403_title); // 状态行:HTTP/1.1 403 Forbidden add_headers(strlen(error_403_form)); // 响应头 if (!add_content(error_403_form)) // 响应体(403错误页面HTML)return false;
- 当请求的资源存在但客户端无访问权限时,生成 403 响应。
FILE_REQUEST
(200 成功请求文件)这是处理成功请求的核心分支,分两种情况:请求的文件非空:
add_status_line(200, ok_200_title); // 状态行:HTTP/1.1 200 OK add_headers(m_file_stat.st_size); // 响应头(含文件实际大小) // 初始化iovec数组(分散写结构) m_iv[0].iov_base = m_write_buf; // 第1个缓冲区:响应头(已写入m_write_buf) m_iv[0].iov_len = m_write_idx; // 响应头长度 m_iv[1].iov_base = m_file_address; // 第2个缓冲区:文件内容(内存映射地址) m_iv[1].iov_len = m_file_stat.st_size; // 文件大小 m_iv_count = 2; // 缓冲区数量为2 bytes_to_send = m_write_idx + m_file_stat.st_size; // 总待发送字节数(头+文件) return true;
- 这里使用
iovec
数组(分散写)将响应头(在m_write_buf
中)和文件内容(通过内存映射m_file_address
指向)合并发送,减少系统调用次数,提高效率。
- 这里使用
请求的文件为空:
const char *ok_string = "<html><body></body></html>"; // 空文件默认响应体 add_headers(strlen(ok_string)); // 响应头(含空内容长度) if (!add_content(ok_string)) // 添加空响应体return false;
- 若文件大小为 0,返回一个空的 HTML 页面作为响应体。
default
(未处理的状态):直接返回false
,表示响应构建失败。
2. 响应发送结构初始化(错误响应和空文件场景)
对于非FILE_REQUEST
的场景(错误响应)或空文件场景,执行以下逻辑:
m_iv[0].iov_base = m_write_buf; // 缓冲区指向响应头+响应体(均在m_write_buf中)
m_iv[0].iov_len = m_write_idx; // 总长度(头+体)
m_iv_count = 1; // 仅需1个缓冲区
bytes_to_send = m_write_idx; // 待发送字节数等于缓冲区长度
return true;
- 这些场景中,响应内容(头 + 体)都存储在
m_write_buf
中,因此只需一个iovec
缓冲区即可。
关键变量说明
m_iv
:iovec
类型数组,用于writev
系统调用的分散写操作,可同时发送多个不连续的缓冲区。m_iv_count
:m_iv
数组中有效缓冲区的数量。bytes_to_send
:记录当前需要发送的总字节数,供write
方法判断发送进度。m_write_buf
:存储响应头和小响应体(如错误页面)的缓冲区。m_file_address
:通过mmap
映射的文件内容地址(用于大文件高效发送)。
总结
process_write
是 HTTP 响应构建的 “总装厂”:
- 根据请求处理结果(
HTTP_CODE
)生成对应状态的响应内容(状态行、头、体)。 - 针对不同响应类型(文件 / 错误)优化缓冲区结构:文件响应使用双缓冲区(头 + 文件映射),错误响应使用单缓冲区(统一存储在
m_write_buf
)。 - 初始化发送相关的变量(
m_iv
、bytes_to_send
等),为后续write
方法的实际发送提供数据基础。
这一设计既符合 HTTP 协议规范,又通过分散写(writev
)和内存映射(mmap
)提升了大文件传输的效率。
void http_conn::process(){LOG_TRACE << "in process";HTTP_CODE read_ret = process_read();LOG_INFO << "read_ret: " << static_cast<int>(read_ret) << Get_HTTPCODE(read_ret);if (read_ret == NO_REQUEST){modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);return;}bool write_ret = process_write(read_ret);LOG_INFO << "read_ret : " << static_cast<int>(read_ret) << Get_HTTPCODE(read_ret);LOG_INFO<<"write_ret: "<<write_ret;if (!write_ret){close_conn();}modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);LOG_TRACE << "end process";}
这段代码是http_conn
类中的process
方法,是 HTTP 请求处理的核心流程控制器,负责协调请求的读取、处理结果的响应以及事件监听模式的切换。以下是详细解析:
函数核心作用
process
方法是单次请求处理的主入口,通过调用请求读取函数(process_read
)和响应构建函数(process_write
),完成 “读取请求→处理请求→构建响应” 的完整流程,并根据处理状态调整 epoll 对套接字的事件监听模式。
代码逻辑拆解
日志记录与请求读取
LOG_TRACE << "in process"; HTTP_CODE read_ret = process_read(); LOG_INFO << "read_ret: " << static_cast<int>(read_ret) << Get_HTTPCODE(read_ret);
- 首先通过
LOG_TRACE
记录进入处理流程的日志。 - 调用
process_read()
读取并解析客户端的 HTTP 请求,返回HTTP_CODE
类型的处理结果(如NO_REQUEST
、FILE_REQUEST
、BAD_REQUEST
等)。 - 记录
process_read
的返回值(状态码及对应描述),便于调试和监控。
- 首先通过
处理 “未完成请求” 场景
if (read_ret == NO_REQUEST) {modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);return; }
NO_REQUEST
表示请求未完全读取(如数据不完整),需要继续等待客户端发送数据。- 调用
modfd
调整 epoll 对当前套接字(m_sockfd
)的监听事件为读事件(EPOLLIN),并保持原触发模式(m_TRIGMode
,边缘触发或水平触发)。 - 直接返回,等待下次 epoll 通知有可读数据时再继续处理。
构建响应并处理结果
bool write_ret = process_write(read_ret); LOG_INFO << "read_ret : " << static_cast<int>(read_ret) << Get_HTTPCODE(read_ret); LOG_INFO<<"write_ret: "<<write_ret;
- 根据
process_read
返回的请求处理结果(read_ret
),调用process_write
构建对应的 HTTP 响应(状态行、头部、响应体等)。 process_write
返回bool
值,表示响应是否成功构建。- 记录响应构建的结果日志,便于追踪问题。
- 根据
处理响应构建失败的情况
if (!write_ret) {close_conn(); }
- 如果
process_write
返回false
(响应构建失败),调用close_conn
关闭当前连接,释放相关资源。
- 如果
切换为写事件监听
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode); LOG_TRACE << "end process";
- 无论响应构建是否成功(若失败已在上述步骤关闭连接),将 epoll 对当前套接字的监听事件调整为写事件(EPOLLOUT),以便后续通过
write
方法将响应数据发送给客户端。 - 记录流程结束的日志。
- 无论响应构建是否成功(若失败已在上述步骤关闭连接),将 epoll 对当前套接字的监听事件调整为写事件(EPOLLOUT),以便后续通过
关键设计思路
- 事件驱动切换:通过
modfd
动态调整 epoll 监听的事件类型(EPOLLIN→EPOLLOUT),符合非阻塞 I/O 的事件驱动模型,避免资源浪费。 - 分步处理:将请求处理拆分为 “读取解析” 和 “响应构建” 两个阶段,通过
HTTP_CODE
传递状态,逻辑清晰。 - 容错处理:对请求未完成(
NO_REQUEST
)和响应构建失败的场景分别处理,保证连接状态的正确性。
总结
process
方法是 HTTP 请求处理的 “调度中心”,它:
- 触发请求的读取与解析;
- 根据解析结果决定是否继续等待数据或构建响应;
- 动态调整 epoll 的事件监听模式,为下一步的读 / 写操作做准备;
- 通过日志和错误处理确保流程的可追踪性和稳定性。
这一设计是高性能 Web 服务器中 “一次请求 - 响应” 流程的典型实现,适配了 epoll 的 I/O 多路复用机制。