Linux下的高性能轻量级Web服务器(三)
3.处理用户的HTTP请求
客户端和服务器建立连接后,服务器等待客户端发送HTTP请求,并给出响应。
代码块
http_conn.h 文件
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"
class http_conn
{
public:
/* 文件名的最大长度 */
static const int FILENAME_LEN = 200;
/* 读缓冲区的大小 */
static const int READ_BUFFER_SIZE = 2048;
/* 写缓冲区的大小 */
static const int WRITE_BUFFER_SIZE = 1024;
/* HTTP请求方法 */
enum METHOD
{
GET = 0,
POST,
HEAD,
PUT,
DELETE,
TRACE,
OPTIONS,
CONNECT,
PATH
};
/* 解析客户请求时,主状态机所处状态 */
enum CHECK_STATE
{
CHECK_STATE_REQUESTLINE = 0,
CHECK_STATE_HEADER,
CHECK_STATE_CONTENT
};
/* 服务器处理HTTP请求的可能结果 */
enum HTTP_CODE
{
NO_REQUEST,
GET_REQUEST,
BAD_REQUEST,
NO_RESOURCE,
FORBIDDEN_REQUEST,
FILE_REQUEST,
INTERNAL_ERROR,
CLOSED_CONNECTION
};
/* 行的读取状态 */
enum LINE_STATUS
{
LINE_OK = 0,
LINE_BAD,
LINE_OPEN
};
public:
http_conn() {}
~http_conn() {}
public:
/* 初始化新接受的连接,函数内部会调用私有方法init */
void init(int sockfd, const sockaddr_in &addr);
/* 关闭连接 */
void close_conn(bool real_close = true);
/* 处理客户请求 */
void process();
/* 非阻塞读操作 */
bool read_once();
/* 非阻塞写操作 */
bool write();
sockaddr_in *get_address()
{
return &m_address;
}
void initmysql_result(connection_pool *connPool);
private:
/* 初始化连接 */
void init();
/* 解析 HTTP 请求 */
HTTP_CODE process_read();
/* 填充 HTTP 应答 */
bool process_write(HTTP_CODE ret);
/* 下面一组函数被 process_read 调用以分析 HTTP 请求 */
HTTP_CODE parse_request_line(char *text);
HTTP_CODE parse_headers(char *text);
HTTP_CODE parse_content(char *text);
HTTP_CODE do_request();
char *get_line() { return m_read_buf + m_start_line; };
LINE_STATUS parse_line();
/* 下面一组函数被 process_write 调用以填充 HTTP 应答 */
void unmap();
bool add_response(const char *format, ...);
bool add_content(const char *content);
bool add_status_line(int status, const char *title);
bool add_headers(int content_length);
bool add_content_type();
bool add_content_length(int content_length);
bool add_linger();
bool add_blank_line();
public:
/* 所有 socket 上的事件均注册到同一个 epoll 内核事件表中,所以将 epoll 文件描述符设置为静态 */
static int m_epollfd;
/* 统计用户数量 */
static int m_user_count;
MYSQL *mysql;
private:
/* 该 HTTP 连接的 socket 地址和对方的 socket 地址 */
int m_sockfd;
sockaddr_in m_address;
/* 读缓冲区 */
char m_read_buf[READ_BUFFER_SIZE];
/* 标识读缓冲区中已经读入的客户数据的最后一个字节的下一个位置 */
int m_read_idx;
/* 当前正在分析的字符在读缓冲区的位置 */
int m_checked_idx;
/* 当前正在解析的行的起始位置 */
int m_start_line;
/* 写缓冲区 */
char m_write_buf[WRITE_BUFFER_SIZE];
/* 写缓冲区中待发送的字节数 */
int m_write_idx;
/* 主状态机当前所处状态 */
CHECK_STATE m_check_state;
/* 请求方法 */
METHOD m_method;
/* 客户请求的目标文件的完整路径,其内容为 doc_root + m_url,doc_root是网站根目录 */
char m_real_file[FILENAME_LEN];
/* 客户请求的文件名称 */
char *m_url;
/* HTTP 协议版本号 */
char *m_version;
/* 主机名 */
char *m_host;
/* HTTP 请求的消息体的长度 */
int m_content_length;
/* HTTP 请求是否保持连接 */
bool m_linger;
/* 客户请求的目标文件被 mmap 到内存中的起始位置 */
char *m_file_address;
/* 目标文件状态。通过它可以判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息 */
struct stat m_file_stat;
/* 我们将采用 writev 来执行写操作,m_iv_count 表示被写内存块的数量 */
struct iovec m_iv[2];
int m_iv_count;
int cgi; //是否启用的POST
char *m_string; //存储请求头数据
int bytes_to_send;
int bytes_have_send;
};
#endif
http_conn.cpp 文件
#include "http_conn.h"
#include "../log/log.h"
#include <map>
#include <mysql/mysql.h>
#include <fstream>
//#define connfdET //边缘触发非阻塞
#define connfdLT //水平触发阻塞
//#define listenfdET //边缘触发非阻塞
#define listenfdLT //水平触发阻塞
//定义http响应的一些状态信息
const char *ok_200_title = "OK";
const char *error_400_title = "Bad Request";
const char *error_400_form = "Your request has bad syntax or is inherently impossible to staisfy.\n";
const char *error_403_title = "Forbidden";
const char *error_403_form = "You do not have permission to get file form this server.\n";
const char *error_404_title = "Not Found";
const char *error_404_form = "The requested file was not found on this server.\n";
const char *error_500_title = "Internal Error";
const char *error_500_form = "There was an unusual problem serving the request file.\n";
//当浏览器出现连接重置时,可能是网站根目录出错或http响应格式出错或者访问的文件中内容完全为空
const char *doc_root = "/home/zyz/WebServer/root";
//将表中的用户名和密码放入map
map<string, string> users;
locker m_lock;
void http_conn::initmysql_result(connection_pool *connPool)
{
//先从连接池中取一个连接
MYSQL *mysql = NULL;
connectionRAII mysqlcon(&mysql, connPool);
//在user表中检索username,passwd数据,浏览器端输入
if (mysql_query(mysql, "SELECT username,passwd FROM user"))
{
LOG_ERROR("SELECT error:%s\n", mysql_error(mysql));
}
//从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);
//返回结果集中的列数
int num_fields = mysql_num_fields(result);
//返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);
//从结果集中获取下一行,将对应的用户名和密码,存入map中
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
}
//对文件描述符设置非阻塞
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void addfd(int epollfd, int fd, bool one_shot)
{
epoll_event event;
event.data.fd = fd;
#ifdef connfdET
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
#endif
#ifdef connfdLT
event.events = EPOLLIN | EPOLLRDHUP;
#endif
#ifdef listenfdET
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
#endif
#ifdef listenfdLT
event.events = EPOLLIN | EPOLLRDHUP;
#endif
if (one_shot)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
//从内核时间表删除描述符
void removefd(int epollfd, int fd)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
close(fd);
}
//将事件重置为EPOLLONESHOT
void modfd(int epollfd, int fd, int ev)
{
epoll_event event;
event.data.fd = fd;
#ifdef connfdET
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
#endif
#ifdef connfdLT
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
#endif
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}
int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;
//关闭连接,关闭一个连接,客户总量减一
void http_conn::close_conn(bool real_close)
{
if (real_close && (m_sockfd != -1))
{
removefd(m_epollfd, m_sockfd);
m_sockfd = -1;
m_user_count--;
}
}
//初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const sockaddr_in &addr)
{
m_sockfd = sockfd;
m_address = addr;
//int reuse=1;
//setsockopt(m_sockfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
addfd(m_epollfd, sockfd, true);
m_user_count++;
init();
}
//初始化新接受的连接
//check_state默认为分析请求行状态
void http_conn::init()
{
mysql = NULL;
bytes_to_send = 0;
bytes_have_send = 0;
m_check_state = CHECK_STATE_REQUESTLINE;
m_linger = false;
m_method = GET;
m_url = 0;
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
cgi = 0;
memset(m_read_buf, '\0', READ_BUFFER_SIZE);
memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
memset(m_real_file, '\0', FILENAME_LEN);
}
//从状态机,用于分析出一行内容
//返回值为行的读取状态,有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)
{
temp = m_read_buf[m_checked_idx];
if (temp == '\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';
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')
{
m_read_buf[m_checked_idx - 1] = '\0';
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
}
return LINE_OPEN;
}
//循环读取客户数据,直到无数据可读或对方关闭连接
//非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once()
{
if (m_read_idx >= READ_BUFFER_SIZE)
{
return false;
}
int bytes_read = 0;
#ifdef connfdLT
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
m_read_idx += bytes_read;
if (bytes_read <= 0)
{
return false;
}
return true;
#endif
#ifdef connfdET
while (true)
{
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
if (bytes_read == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
return false;
}
else if (bytes_read == 0)
{
return false;
}
m_read_idx += bytes_read;
}
return true;
#endif
}
//解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
m_url = strpbrk(text, " \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");
m_version = strpbrk(m_url, " \t");
if (!m_version)
return BAD_REQUEST;
*m_version++ = '\0';
m_version += strspn(m_version, " \t");
if (strcasecmp(m_version, "HTTP/1.1") != 0)
return BAD_REQUEST;
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)
strcat(m_url, "judge.html");
m_check_state = CHECK_STATE_HEADER;
return NO_REQUEST;
}
//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
/* 遇到空行,表示头部字段解析完成 */
if (text[0] == '\0')
{
/* 如果 HTTP 请求有消息体,则还需要读取 m_content_length 字节的消息体,状态机转移到 CHECK_STATE_CONTENT 状态 */
if (m_content_length != 0)
{
m_check_state = CHECK_STATE_CONTENT;
return NO_REQUEST;
}
/* 否则说明已经得到一个完整的 HTTP 请求 */
return GET_REQUEST;
}
/* 处理 Connection 头部字段 */
else if (strncasecmp(text, "Connection:", 11) == 0)
{
text += 11;
text += strspn(text, " \t");
if (strcasecmp(text, "keep-alive") == 0)
{
m_linger = true;
}
}
/* 处理 Content-length 头部字段 */
else if (strncasecmp(text, "Content-length:", 15) == 0)
{
text += 15;
text += strspn(text, " \t");
m_content_length = atol(text);
}
/* 处理 Host 头部字段 */
else if (strncasecmp(text, "Host:", 5) == 0)
{
text += 5;
text += strspn(text, " \t");
m_host = text;
}
else
{
LOG_INFO("oop!unknow header: %s", text);
Log::get_instance()->flush();
}
return NO_REQUEST;
}
//判断http请求是否被完整读入
http_conn::HTTP_CODE http_conn::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_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;
LOG_INFO("%s", text);
Log::get_instance()->flush();
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)
{
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;
}
}
return NO_REQUEST;
}
/*
当得到一个完整、正确的 HTTP 请求时,分析目标文件的属性。
如果目标文件存在、当前用户可读,且不是目录,则使用 mmap 将其映射到内存地址 m_file_address 处,并告诉调用者获取文件成功
*/
http_conn::HTTP_CODE http_conn::do_request()
{
strcpy(m_real_file, doc_root);
int len = strlen(doc_root);
//printf("m_url:%s\n", m_url);
const char *p = strrchr(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&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')
{
//如果是注册,先检测数据库中是否有重名的
//没有重名的,进行增加数据
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");
}
}
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;
}
/* 对内存映射区执行 munmap 操作 */
void http_conn::unmap()
{
if (m_file_address)
{
munmap(m_file_address, m_file_stat.st_size);
m_file_address = 0;
}
}
/* 发送 HTTP 响应 */
bool http_conn::write()
{
int temp = 0;
//表示响应报文为空,一般不会出现这种情况
if (bytes_to_send == 0)
{
modfd(m_epollfd, m_sockfd, EPOLLIN);
init();
return true;
}
while (1)
{
//将响应报文的状态行、消息头、空行和响应正文发送给浏览器端,temp为发送的字节数
temp = writev(m_sockfd, m_iv, m_iv_count);
//发送成功
if (temp > 0)
{
//更新已发送字节
bytes_have_send += temp;
}
//发送失败
if (temp <= -1)
{
//判断缓冲区是否满了
if (errno == EAGAIN)
{
//判断第一个iovec头部信息的数据是否发送完
if (bytes_have_send >= m_iv[0].iov_len)
{
//头部信息已发送完,改变iov指针,发送第二个iovec数据
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;
}
//重新注册写事件
modfd(m_epollfd, m_sockfd, EPOLLOUT);
return true;
}
//发送失败,但不是缓冲区问题,取消映射
unmap();
return false;
}
//更新还需发送的字节数
bytes_to_send -= temp;
//判断数据是否全部发送完
if (bytes_to_send <= 0)
{
//数据已经全部发送完,取消映射
unmap();
//在epoll树上重置EPOLLIN事件
modfd(m_epollfd, m_sockfd, EPOLLIN);
//浏览器的请求为长连接
if(m_linger)
{
//重新初始化HTTP对象
init();
return true;
}
else
{
return false;
}
}
}
}
/* 往写缓冲区中写入待发送的数据 */
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);
Log::get_instance()->flush();
return true;
}
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)
{
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 请求的结果,决定返回给客户的内容 */
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;
}
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 请求的入口函数 */
void http_conn::process()
{
HTTP_CODE read_ret = process_read();
if (read_ret == NO_REQUEST)
{
modfd(m_epollfd, m_sockfd, EPOLLIN);
return;
}
bool write_ret = process_write(read_ret);
if (!write_ret)
{
close_conn();
}
modfd(m_epollfd, m_sockfd, EPOLLOUT);
}
HTTP报文格式
HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能被浏览器端识别。其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。
请求报文
HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。其中,请求分为两种,GET和POST。
- GET
GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/,/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空
- POST
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
详细说明
- 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。
- 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
- HOST,给出请求资源所在服务器的域名。
- User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
- Accept,说明用户代理可处理的媒体类型。
- Accept-Encoding,说明用户代理支持的内容编码。
- Accept-Language,说明用户代理能够处理的自然语言集。
- Content-Type,说明实现主体的媒体类型。
- Content-Length,说明实现主体的大小。
- Connection,连接管理,可以是Keep-Alive或close。
- 空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
- 请求数据也叫主体,可以添加任意的其他数据。
注意:在 HTTP 报文中,每一行的数据由 \r\n 作为结束字符,空行则是仅仅是字符\r\n。
响应报文
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行
<html>
<head></head>
<body>
<!--body goes here-->
</body>
</html>
详细说明
- 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
- 消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。
- 空行,消息报头后面的空行是必须的。
- 响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。
HTTP有5种类型的状态码,具体的:
- 1xx:指示信息--表示请求已接收,继续处理。
- 2xx:成功--表示请求正常处理完毕。
- 200 OK:客户端请求被正常处理。
- 206 Partial content:客户端进行了范围请求。
- 3xx:重定向--要完成请求必须进行更进一步的操作。
- 301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
- 302 Found:临时重定向,请求的资源现在临时从不同的URI中获得。
- 4xx:客户端错误--请求有语法错误,服务器无法处理请求。
- 400 Bad Request:请求报文存在语法错误。
- 403 Forbidden:请求被服务器拒绝。
- 404 Not Found:请求不存在,服务器上找不到请求的资源。
- 5xx:服务器端错误--服务器处理请求出错。
- 500 Internal Server Error:服务器在执行请求时出现错误。
处理 HTTP 请求
通过 read_once() 方法将客户端的 HTTP 请求读入读缓冲区后,各子线程通过 process() 函数对任务进行处理,调用 process_read() 函数和 process_write() 函数分别完成报文解析与报文响应两个任务。
void http_conn::process()
{
HTTP_CODE read_ret = process_read();
//NO_REQUEST,表示请求不完整,需要继续接收请求数据
if(read_ret == NO_REQUEST)
{
//注册并监听读事件
modfd(m_epollfd, m_sockfd, EPOLLIN);
return;
}
//调用process_write完成报文响应
bool write_ret = process_write(read_ret);
if(!write_ret)
{
close_conn();
}
//注册并监听写事件
modfd(m_epollfd, m_sockfd, EPOLLOUT);
}
}
通过调用 process_read() 函数将客户端的求报文进行解析,只有通过解析,才能知道用户请求的内容是什么,是请求图片,还是视频,或是其他请求,我们根据这些请求返回相应的HTML页面等。
项目中使用主从状态机的模式进行解析,从状态机(parse_line)负责读取报文的一行,主状态机(process_read)责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。每解析一部分都会将整个请求的 m_check_state 状态改变,状态机也就是根据这个状态来进行不同部分的解析跳转的:
- parse_request_line(text)
解析请求行,也就是GET中的GET /562f25980001b1b106000338.jpg HTTP/1.1这一行,或者POST中的POST / HTTP1.1这一行。通过请求行的解析我们可以判断该 HTTP 请求的类型(GET/POST),而请求行中最重要的部分就是 URL 部分,我们会将这部分保存下来用于后面的生成 HTTP 响应。
- parse_headers(text)
解析请求头部,GET和POST中空行以上,请求行以下的部分。
- parse_content(text)
解析请求数据,对于GET来说这部分是空的,因为这部分内容已经以明文的方式包含在了请求行中的URL部分了;只有POST的这部分是有数据的,项目中的这部分数据为用户名和密码,我们会根据这部分内容做登录和校验,并涉及到与数据库的连接。
其流程图如下:

从状态机详解
从状态机负责读取 buffer 中的数据,将每行数据末尾的 \r\n 置为 \0\0,并更新从状态机在buffer中读取的位置 m_checked_idx,以此来驱动主状态机解析。
从状态机从 m_read_buf 中逐字节读取,读取的当前字节有以下三种情况:
- 当前字节为 \r 时
- 如果下一个字符是 \n,则将 \r\n 修改成 \0\0,将 m_checked_idx 指向下一行的开头,返回 LINE_OK
- 如果已经达到了 buffer 末尾,表示 buffer 还需要继续接收,返回 LINE_OPEN
- 如果不是以上两种情况,表示语法错误,返回 LINE_BAD
- 当前字节是\n时(一般是上次读取到\r就到了buffer末尾,没有接收完整,再次接收时会出现这种情况)
- 如果前一个字符是 \r,则将 \r\n 修改成 \0\0,将 m_checked_idx 指向下一行的开头,返回LINE_OK
- 当前字节既不是 \r,也不是 \n
- 表示接收不完整,需要继续接收,返回 LINE_OPEN
从状态机的三种状态,标识解析一行的读取状态。
- LINE_OK,完整读取一行
- LINE_BAD,报文语法有误
- LINE_OPEN,读取的行不完整
从状态机函数:parse_line()
// 从状态机函数,用于分析一行内容
http_conn::LINE_STATUS http_conn::parse_line()
{
// temp 为将要分析的字节
char temp;
// m_read_idx 指向缓冲区 m_read_buf 的数据末尾的下一个字节
// m_checked_idx 指向从状态机当前正在分析的字节
for (; m_checked_idx < m_read_idx; ++m_checked_idx)
{
temp = m_read_buf[m_checked_idx];
if (temp == '\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';
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')
{
m_read_buf[m_checked_idx - 1] = '\0';
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
}
return LINE_OPEN;
}
主状态机详解
主状态机初始状态是 CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾 \r\n 符号改为 \0\0,以便于主状态机直接取出对应字符串进行处理。
HTTP_CODE含义
表示 HTTP 请求的处理结果,在头文件中初始化了八种情形,在报文解析时只涉及到四种。
- NO_REQUEST
请求不完整,需要继续读取请求报文数据 - GET_REQUEST
获得了完整的HTTP请求 - BAD_REQUEST
HTTP请求报文有语法错误 - INTERNAL_ERROR
服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发
主状态机函数:process_read()
http_conn::HTTP_CODE http_conn::process_read()
{
// 初始化 从状态机状态和HTTP请求解析结果
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char *text = 0;
// 判断条件将在后面讲解,parse_line() 函数为从状态机
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
{
// get_line()获取行在 buffer 中的起始位置,parse_line 已将\r\n改为\0\0
text = get_line();
// m_start_line 是每一个数据行在 m_read_buf 中的起始位置
// m_checked_idx 表示从状态机在 m_read_buf 中读取的位置
m_start_line = m_checked_idx;
LOG_INFO("%s", text);
Log::get_instance()->flush();
//主状态机的三种状态转移逻辑
switch (m_check_state)
{
case CHECK_STATE_REQUESTLINE:
{
// 调用 parse_request_line(text) 解析请求行
ret = parse_request_line(text);
if (ret == BAD_REQUEST)
return BAD_REQUEST;
break;
}
case CHECK_STATE_HEADER:
{
// 调用 parse_headers(text) 解析请求头
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:
{
// 调用 parse_content(text) 解析请求数据,如果是 GET 则没有请求数据
ret = parse_content(text);
//完整解析POST请求后,跳转到报文响应函数
if (ret == GET_REQUEST)
return do_request();
//解析完消息体即完成报文解析,避免再次进入循环,更新line_status
line_status = LINE_OPEN;
break;
}
default:
return INTERNAL_ERROR;
}
}
return NO_REQUEST;
}
解析请求行函数
// 解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
//在 HTTP 报文中,请求行中的各个部分之间通过 \t 或 空格 分隔。
// 返回请求行中最先含有 空格 和 \t 任一字符的位置
m_url = strpbrk(text, " \t");
// 如果返回值为 NULL 则 表示报文格式错误
if (!m_url)
{
return BAD_REQUEST;
}
// 将该位置改为\0,用于将前面数据取出
*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此时跳过了第一个空格或\t字符,但不知道之后是否还有
通过 strspn 函数继续跳过空格和\t字符,让 m_url 指向请求资源的第一个字符
*/
m_url += strspn(m_url, " \t");
m_version = strpbrk(m_url, " \t");
if (!m_version)
return BAD_REQUEST;
*m_version++ = '\0';
m_version += strspn(m_version, " \t");
// 仅支持HTTP/1.1
if (strcasecmp(m_version, "HTTP/1.1") != 0)
return BAD_REQUEST;
// 对url的前7个字符进行判断,因为有些报文的请求资源中会带有http://,需要对这种情况进行单独处理
if (strncasecmp(m_url, "http://", 7) == 0)
{
m_url += 7;
m_url = strchr(m_url, '/');
}
//同样增加 HTTPS 的情况
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)
strcat(m_url, "judge.html");
//请求行处理完毕,将主状态机转移处理请求头
m_check_state = CHECK_STATE_HEADER;
return NO_REQUEST;
}
解析请求头函数
//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
/*
在报文中,请求头和空行的处理使用的同一个函数
这里通过判断当前的text首位是不是\0字符
若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。
*/
if (text[0] == '\0')
{
//判断是GET还是POST请求
if (m_content_length != 0)
{
//POST需要跳转到消息体处理状态
m_check_state = CHECK_STATE_CONTENT;
return NO_REQUEST;
}
return GET_REQUEST;
}
//解析请求头部连接字段
else if (strncasecmp(text, "Connection:", 11) == 0)
{
text += 11;
//跳过空格和\t字符
text += strspn(text, " \t");
if (strcasecmp(text, "keep-alive") == 0)
{
//如果是长连接,则将linger标志设置为true
m_linger = true;
}
}
//解析请求头部内容长度字段
else if (strncasecmp(text, "Content-length:", 15) == 0)
{
text += 15;
text += strspn(text, " \t");
m_content_length = atol(text);
}
//解析请求头部HOST字段
else if (strncasecmp(text, "Host:", 5) == 0)
{
text += 5;
text += strspn(text, " \t");
m_host = text;
}
else
{
//printf("oop!unknow header: %s\n",text);
LOG_INFO("oop!unknow header: %s", text);
Log::get_instance()->flush();
}
return NO_REQUEST;
}
如果仅仅是GET请求,那么主状态机只设置之前的两个状态足矣。因为GET请求没有消息体,当解析完空行之后,便完成了报文的解析。
但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装。
为此,我们需要在解析报文的部分添加解析消息体的模块。
那么,主状态机中循环的判断条件为什么要写成这样呢?
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
在GET请求报文中,每一行都是 \r\n 作为结束,所以对报文进行拆解时,仅用从状态机的状态(line_status=parse_line()) == LINE_OK 语句即可。
但,在POST请求报文中,消息体的末尾没有任何字符,所以不能使用从状态机的状态,这里转而使用主状态机的状态作为循环入口条件。
那后面的 && line_status == LINE_OK又是为什么?
解析完消息体后,报文的完整解析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT,符合循环入口条件,还会再次进入循环,这并不是我们所希望的。
为此,增加了该语句,并在完成消息体解析后,将 line_status 变量更改为 LINE_OPEN,此时可以跳出循环,完成报文解析任务。
解析请求数据函数
//仅用于 POST 请求时,解析请求数据
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{
//判断buffer中是否读取了消息体
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;
}
响应请求报文
基础API介绍
stat
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:映射区的长度
- prot:期望的内存保护标志,不能与文件的打开模式冲突
- PROT_READ 表示映射区域可以被读取
- PROT_WRITE 映射区域可被写入
- PROT_NONE 映射区域不能存取
- PROT_EXEC 映射区域可被执行
- flags:影响映射区域的各种特性
- MAP_PRIVATE,对映射区域的写入操作会产生一个映射文件的复制,即私人的"写入时复制" 对此区域作的任何修改都不会写回原来的文件内容。
- MAP_FIXED,如果参数 start 所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
- MAP_SHARED,对应射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
- MAP_ANONYMOUS,建立匿名映射,此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
- MAP_DENYWRITE,只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
- MAP_LOCKED,将映射区域锁定住,这表示该区域不会被置换(swap)。
- fd:有效的文件描述符,一般是由open()函数返回
- off_toffset:被映射对象内容的起点
iovec结构体
定义了一个向量元素,通常,这个结构用作一个多元素的数组。
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
writev
writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
- filedes表示文件描述符
- iov为前述io向量机制结构体iovec
- iovcnt为结构体的个数
若成功则返回已写的字节数,若出错则返回-1。writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。
注意:循环调用 writev 时,需要重新处理 iovec 中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是 iovec 数组,计量单位是 iovcnt,而不是字节数,我们仍然需要通过遍历 iovec 来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界 iovec 的 io_base 和 io_len。
流程图
服务器处理完请求报文后调用 process_write 函数完成响应报文的编写,再通过 http_conn::write 函数完成数据的读取与发送。

HTTP_CODE 在头文件中初始化了八种情形,在报文解析与响应中只用到了七种。
- 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 下,一般不会触发
代码分析
do_request()
process_read 函数的返回值是对请求的文件分析后的结果,其返回值有两种情况:
- 是语法错误导致的 BAD_REQUEST
- 是 do_request 的返回结果
do_request 函数将网站的根目录和url文件拼接,然后通过stat判断该文件的属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。
m_url 为请求报文中解析出的请求资源,以 / 开头,项目中解析后的 m_url 有8种情况。
- /
- GET请求,跳转到judge.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,即关注页面
http_conn::HTTP_CODE http_conn::do_request()
{
//将初始化的m_real_file赋值为网站根目录
strcpy(m_real_file, doc_root);
int len = strlen(doc_root);
//找到m_url中 / 的位置
const char *p = strrchr(m_url, '/');
//处理cgi,在本系列最后的文章中会进行详细讲解
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
{
//根据标志判断是登录检测还是注册检测
//同步线程登录校验
//CGI多进程登录校验
}
// 如果请求资源为/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));
free(m_url_real);
}
// 如果请求资源为 /1,表示跳转登录界面
else if (*(p + 1) == '1')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/log.html");
//将网站目录和/log.html进行拼接,更新到m_real_file中
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
// 如果请求资源为 /5,跳转到 picture.html
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);
}
// 如果请求资源为 /6,跳转到 video.html
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);
}
// 如果请求资源为 /7,跳转到 fans.html
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
//如果以上均不符合,即不是登录和注册,直接将url与网站目录拼接
//这里的情况是welcome界面,请求服务器上的一个图片
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;
}
process_write()
根据 do_request() 的返回状态,服务器子线程调用 process_write 向 m_write_buf 中写入响应报文。
bool http_conn::process_write(HTTP_CODE ret)
{
switch (ret)
{
//内部错误,500
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;
}
//报文语法有误,404
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;
}
//资源没有访问权限,403
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;
}
//文件存在,200
case FILE_REQUEST:
{
add_status_line(200, ok_200_title);
//如果请求的资源存在
if (m_file_stat.st_size != 0)
{
add_headers(m_file_stat.st_size);
//第一个 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;
//发送的全部数据为响应报文头部信息和文件大小
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
else
{
//如果请求的资源大小为0,则返回空白html文件
const char *ok_string = "<html><body></body></html>";
add_headers(strlen(ok_string));
if (!add_content(ok_string))
return false;
}
}
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;
}
process_write()调用以下 6 个函数完成响应报文的编写:
- add_status_line函数,添加状态行:http/1.1 状态码 状态消息
- add_headers函数添加消息报头,内部调用add_content_length和add_linger函数
- content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据
- connection记录连接状态,用于告诉浏览器端保持长连接
- add_blank_line添加空行
- add_content添加响应正文
上述函数均是内部调用 add_response 函数更新 m_write_idx 指针和缓冲区 m_write_buf 中的内容。
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_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(arg_list);
return false;
}
//更新m_write_idx位置
m_write_idx += len;
va_end(arg_list);
LOG_INFO("request:%s", m_write_buf);
Log::get_instance()->flush();
return true;
}
//添加状态行
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)
{
add_content_length(content_len);
add_linger();
add_blank_line();
}
//添加Content-Length,表示响应报文的长度
bool http_conn::add_content_length(int content_len)
{
return add_response("Content-Length:%d\r\n", content_len);
}
//添加文本类型,这里是html
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");
}
//添加文本content
bool http_conn::add_content(const char *content)
{
return add_response("%s", content);
}
http_conn::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),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性。
书中原代码的write函数不严谨,这里对其中的Bug进行了修复,可以正常传输大文件。
书中源代码未正确更新iovec结构体的指针,忽略了成功发送数据,但是数据未全部发送完的情况。
bool http_conn::write()
{
int temp = 0;
// 要发送的数据长度为 0,表示响应报文为空,一般不会出现这种情况
if (bytes_to_send == 0)
{
modfd(m_epollfd, m_sockfd, EPOLLIN);
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);
return true;
}
unmap();
return false;
}
// 更新 已经发送 和 还需发送 的字节数
bytes_have_send += temp;
bytes_to_send -= temp;
// 成功发送一部分数据
// 判断成功发送的数据长度是否大于 m_iv[0].iov_len
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;
}
}
}
}

浙公网安备 33010602011771号