目录
Webserver 网页服务器
服务器通过HTTP协议与客户端(通常是浏览器)进行通信,来接收、存储、处理来自客户端的HTTP请求,并对其请求做出HTTP响应,返回给客户端其请求的内容(文件、网页)或返回的错误信息。
用户如何与你的web服务器进行通信
用户使用web浏览器与相应服务器进行通信。在浏览器中键入“域名”或“IP地址 :端口号”,浏览器则先将你的域名解析成相应的IP地址或者直接根据你的IP地址向对应的Web服务器发送一个HTTP请求。这一过程首先要通过TCP协议的三次握手建立与目标的连接,然后HTTP协议生成针对目标web服务器的HTTP请求报文,通过TCP、IP等协议发送到目标Web服务器上。
Web服务器如何接收客户端发来的HTTP请求报文
web服务器通过socket监听来自用户的请求,远端很多用户尝试connect这个webserver上正在listen的port,而监听到的这些连接会排队等待被accept。由于用户连接请求是随机到达的异步事件,每当socket listen到新的客户连接并且放入监听队列,我们都要告诉我们的web服务器有链接进来了 accept这个连接并分配一个逻辑单元处理这个用户请求。
而且我们在处理这个请求的同时,还需要继续监听其他客户的请求并分配其另一个逻辑单元来处理(这就要求要同时处理多个事件了,需要并发处理,这里我们使用的是线程池逻辑并发实现)
本项目中,服务器通过epoll这种IO复用计数来实现对监听socket和连接socket的同时监听。 当同时监听到多个文件描述符就绪时,如果不采取措施他将依次按照顺序处理其中就绪的每一个文件描述符,为提高效率,这部分我们通过线程池来实现并发,为每一个逻辑就绪的文件描述符分配逻辑单元。
reactor模式:主线程只负责监听文件描述符上是否有事件发生,若有立即通知工作线程(逻辑单元),将socket可读可写事件放入请求队列,交给工作线程处理。
proactor模式:所有的IO操作都交给主线程和内核来处理(读写)工作线程负责逻辑
web服务器如何处理以及响应接收到的HTTP请求报文
使用线程池(半同步/半反应堆模式)并发处理用户请求,主线程负责读写,工作线程(线程池中的线程)负责处理逻辑(HTTP请求报文解析)。
将listenfd上到达的connection通过accept接收,并返回一个新的connfd用于和用户通信,并对用户请求返回响应,同时将这个connfd注册到内核事件表中,等用户发来请求报文。
这个过程就是epoll_wait发现connfd上有数据可读了(epollin),主线程就将这个http请求报文读进这个连接socket的读缓存中users[sockfd].read(), 然后将该任务对象(指针)插入到线程池的请求队列中,线程池需要依靠锁机制以及信号量机制来实现线程同步 ,保证操作的原子性。
Epoll
epoll是一种高效的I/O事件通知机制,可用于监视多个文件描述符上的事件。它是Linux内核提供的一种事件通知机制,与select和poll这两种机制相比,具有更高的性能和可扩展性。
在epoll中,用户可以通过epoll_create函数创建一个epoll对象,然后将需要监视的文件描述符添加到epoll对象中。当文件描述符上发生事件时,epoll会通知用户,并返回相应的事件类型。用户可以使用epoll_wait函数来等待事件发生并进行处理。
- 注册事件的时候是异步的:应用程序可以通过epoll_ctl函数将感兴趣的IO事件注册到epoll对象中,而不需要等待事件发生才注册。
- 内核通知就绪事件是异步的:当有IO事件就绪时,内核会将就绪的事件添加到就绪队列中,并唤醒等待的应用程序,这个过程是异步的。
- 处理就绪事件是异步的:应用程序可以通过遍历就绪队列来处理就绪的IO事件,这个过程也是异步的。
epoll_create函数
int epoll_create(int size)
创建一个新的epoll实例,以便于监视多个文件描述符上的IO事件,该描述符将用作其他epoll系统调用的第一个参数,size可设置为任意正整数。
epoll_ctl函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除
应用程序可以通过epoll_ctl函数将感兴趣的IO事件注册到epoll对象中,而不需要等待事件发生才注册,这个过程是异步的。
epfd:为epoll_creat的句柄
op:表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);
event:告诉内核需要监听的事件
上述event是epoll_event结构体指针类型,表示内核所监听的事件,具体定义如下:
struct epoll_event{ _uint32_t events; epoll_data_t data; };
events描述事件类型,其中epoll事件类型有以下几种
EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
data用于保存事件相关的用户数据
typedef union epoll_data { void *ptr; // 指针类型 int fd; // 文件描述符类型 uint32_t u32; uint64_t u64; } epoll_data_t;
epoll_wait函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数.
events:用来存内核得到事件的集合,
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size.
timeout:是超时时间
-1:阻塞
0:立即返回,非阻塞
>0:指定毫秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
当应用程序调用epoll_wait函数等待就绪的IO事件时,它会阻塞等待,直到有就绪的事件到达或超时。这个过程是同步的,因为应用程序需要等待IO事件的就绪。
ET、LT、EPOLLONESHOT
LT水平触发模式:
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。
当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理
ET边缘触发模式:
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件
必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
EPOLLONESHOT:
一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读(epollin再次被触发),此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件
select/poll/epoll区别
1. select/poll委托内核帮我们检测,会告知有几个事件触发了,但是不会告知是哪几个;
epoll不仅会告知有几个事件触发了,还会告知是哪几个;
2. 调用函数:select和poll都是一个函数,epoll是一组函数;
3. 文件描述符数量:
select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐;
poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目无限制;
epoll通过红黑树描述,最大可以打开文件的数目无限制,可以通过命令ulimit -n number修改,仅对当前终端有效。
4. 将文件描述符从用户传给内核:
select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要将fd数据从用户空间拷贝到内核空间;
epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上。
5. 内核判断就绪的文件描述符:
select和poll通过遍历文件描述符集合,判断哪个文件描述符上有事件发生
epoll_create时,内核除了帮我们在epoll文件系统里建立红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可;
epoll是根据每个fd上面的回调函数判断,只有发生了事件的socket才会主动的去调用callback函数,若是就绪事件,插入list
6. 应用程序索引就绪文件描述符:
select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历
epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可
7. 工作模式:
select和poll都只能工作在相对低效的LT模式下
epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。
8.应用场景:
当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll
当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll
当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能
Epoll的IO模型是同步的还是异步的
从IO层面来看,epoll绝对是同步的;
从消息处理层面来看,epoll是异步的.
当应用程序调用epoll_wait函数等待就绪的IO事件时,它会阻塞等待,直到有就绪的事件到达或超时。这个过程是同步的,因为应用程序需要等待IO事件的就绪。
- 注册事件的时候是异步的:应用程序可以通过epoll_ctl函数将感兴趣的IO事件注册到epoll对象中,而不需要等待事件发生才注册。
- 内核通知就绪事件是异步的:当有IO事件就绪时,内核会将就绪的事件添加到就绪队列中,并唤醒等待的应用程序,这个过程是异步的。
- 处理就绪事件是异步的:应用程序可以通过遍历就绪队列来处理就绪的IO事件,这个过程也是异步的。
结合代码分析服务器接收http请求
浏览器端发出http请求,主线程创建http对象接收请求并将所有数据读入对应buffer中,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
//1. 创建MAX_FD个http类对象
http_conn *users = new http_conn[MAX_FD];
//2 创建内核事件表
epoll_event events[MAX_EVENT_NUMBER];
epollfd = epoll_create(5); //创建一个能够监听5个文件描述符的 epoll句柄
assert(epollfd != -1);
//3 将listenfd放到epoll对象中
addfd(epollfd, listenfd, false);
//4 将上述epollfd赋值给http类对象的m_epollfd属性
http_conn::m_epollfd = epollfd;
while (!stop_server)
{
//等待所监控文件描述符上有事件的产生
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
LOG_ERROR("%s", "epoll failure");
break;
}
//对所有的就绪事件进行处理
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
//处理新到的客户连接
if (sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
//LT水平触发
#ifdef listenfdLT
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
LOG_ERROR("%s:errno is:%d", "accept error", errno);
continue;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
continue;
}
users[connfd].init(connfd, client_address);
#endif
//ET非阻塞边缘触发
#ifdef listenfdET
//需要循环接收数据
while (1)
{
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
LOG_ERROR("%s:errno is:%d", "accept error", errno);
break;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
break;
}
users[connfd].init(connfd, client_address);
#endif
}
//处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接
}
//处理信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
}
//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
//读入对应缓冲区
if (users[sockfd].read_once())
{
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);
}
else
{
//服务器关闭连接
}
}
}
HTTP报文格式
请求报文:浏览器端 =》 服务器发送
HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。其中,请求分为两种,GET和POST,具体的:
1.请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。
2.请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
HOST,给出请求资源所在服务器的域名。
User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
Accept,说明用户代理可处理的媒体类型。
Accept-Encoding,说明用户代理支持的内容编码。
Accept-Language,说明用户代理能够处理的自然语言集。
Content-Type,说明实现主体的媒体类型。
Content-Length,说明实现主体的大小。
Connection,连接管理,可以是Keep-Alive或close
3. 空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
4. 请求数据,也叫主体,可以添加任意的其他数据。
响应报文:服务器端 =》浏览器端
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。空行,消息报头后面的空行是必须的。
响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。
get和post区别是什么
1. get请求的参数会附加在URL上,POST请求的参数放在消息体中
2. GET请求参数会被完整保留在浏览器历史记录中, Post中的参数不会被保留(GET可以缓存,POST不可缓存)
3. GET请求对数据长度有限制,POST请求没有长度限制。
4. 对参数的数据类型,GET只接受ASCII字符,而POST没有限制;
- GET请求适合用于获取数据,对于幂等操作(不会对服务器状态造成影响)比较合适。
- POST请求适合用于提交数据,对于会对服务器状态产生影响的操作(如新增、修改)比较合适。
- GET请求的参数会附加在URL上,POST请求的参数放在请求消息体中。
- GET请求对数据长度有限制,POST请求没有长度限制。
- GET请求在浏览器历史记录中可见,POST请求不可见。
- GET请求可以被缓存,POST请求不会被浏览器主动缓存。
HTTP状态码
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:服务器在执行请求时出现错误。
为什么使用有限状态机
一种抽象的理论模型,能够把有限个变量描述的状态变化过程,以可构造可验证的方式呈现出来。
封装逻辑:一种逻辑单元内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清晰易懂。
状态机流程图
主状态机负责解析get或者post请求的部分(请求行、请求头、请求消息体)
从状态机负责判断读取行是否完整、是否有语法错误
主状态机内部调用从状态机,从状态机驱动主状态机
http报文处理流程
1. 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务中取出一个任务进行处理。
2. 工作线程取出任务后通过process函数对任务进行处理,调用process_read函数,通过主、从状态机对请求报文进行解析。
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);
}
3. 解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。
请求报文整体流程
process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。
从状态机逻辑
HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成成单独的行进行解析。
从状态机负责读取buffer中的数据,将每行数据末尾的\r\n置为\0\0,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析。
主状态机逻辑
主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。
请求行-》请求头-》空行-》请求消息体
-
CHECK_STATE_REQUESTLINE
-
主状态机的初始状态,调用parse_request_line函数解析请求行
-
解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
-
解析完成后主状态机的状态变为CHECK_STATE_HEADER
-
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
//在HTTP请求报文中,请求行用来说明请求类型,要访问的资源以及所使用的http版本,
//其中各个部分用\t 或者空格分开
//请求行中最先含有空格和\t任一字符的位置并返回
m_url = strpbrk(text, " \t");
//如果没有 空格或者\t 报文格式错误
if (!m_url)
{
return BAD_REQUEST;
}
//将该位置改为\0,用于将前面数据取出
*m_url++ = '\0';
//取出数据,并通过与GET和POST比较,以确定请求方式
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字符,但不知道之后是否还有
//将m_url向后偏移,通过查找,继续跳过空格和\t字符,指向请求资源的第一个字符
m_url += strspn(m_url, " \t");
//使用与判断请求方式的相同逻辑,判断HTTP版本号
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;
//对请求资源前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;
}
解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。
-
CHECK_STATE_HEADER
-
调用parse_headers函数解析请求头部信息
-
判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。
-
若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。
-
connection字段判断是keep-alive还是close,决定是长连接还是短连接
-
content-length字段,这里用于读取post请求的消息体长度
-
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
//text为空 判断请求是否为post请求
if (text[0] == '\0')
{
//判断是空行还是请求头
if (m_content_length != 0)//不为空就是post请求啊
{
//判断是GET还是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;
}
后续的登陆和注册功能,为了避免将用户名和密码直接暴露在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=parse_line())==LINE_OK))是因为解析完消息体后,报文的完整解析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT,也就是说,符合循环入口条件,还会再次进入循环,这并不是我们所希望的。
为此,增加了该语句,并在完成消息体解析后,将line_status变量更改为LINE_OPEN,此时可以跳出循环,完成报文解析任务。
-
CHECK_STATE_CONTENT
-
仅用于解析POST请求,调用parse_content函数解析消息体
-
用于保存post请求消息体,为后面的登录和注册做准备
-
//判断http请求是否被完整读入
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;
}
响应报文流程
浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read
对其进行解析,根据解析结果HTTP_CODE
,进入相应的逻辑和模块。
其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_once
和http_conn::write
完成数据的读取与发送。
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下,一般不会触发
-
do_request
通过process_read返回的HTTP_CODE判断将要进行什么操作以及do_request返回什么响应报文
process_read函数返回请求的文件分析后的结果,返回一个HTTP_CODE值,其中一部分是语法错误导致的BAD_REQUEST
,一部分是do_request
的返回结果.
process_write
根据do_request的返回状态,服务器子线程调用process_write向m_write_buf中写入响应报文。
-
add_status_line函数,添加状态行:http/1.1 状态码 状态消息
-
add_headers函数添加消息报头,内部调用add_content_length和add_linger函数
-
content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据
-
connection记录连接状态,用于告诉浏览器端保持长连接
-
-
add_blank_line添加空行
均是内部调用
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); }
响应报文存在两种状态:
一种是响应存在通过io
向量机制iovec
,声明两个iovec
,第一个指向m_write_buf
,第二个指向mmap
的地址m_file_address;
一种是请求出错,这时候只申请一个iovec
,指向m_write_buf
。
iovec
定义了一个向量元素,通常,这个结构用作一个多元素的数组。
struct iovec { void *iov_base; /* starting address of buffer */ size_t iov_len; /* size of buffer */ };
iov_base指向数据的地址
iov_len表示数据的长度
process_write函数
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; } //报文语法有误,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); //如果请求的资源存在 /* 一种是请求文件的存在,通过io向量机制iovec, 声明两个iovec,第一个指向m_write_buf, 第二个指向mmap的地址m_file_address; */ 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; } /* 一种是请求出错,这时候只申请一个iovec,指向m_write_buf。 */ 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::write
服务器子线程调用process_write
完成响应报文,随后注册epollout
事件。服务器主线程检测写事件,并调用http_conn::write
函数将响应报文发送给浏览器端。
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;
//第一个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;
}
//继续发送第一个iovec头部信息的数据
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();
//在epoll树上重置EPOLLONESHOT事件
modfd(m_epollfd, m_sockfd, EPOLLIN);
//浏览器的请求为长连接
if (m_linger)
{
//重新初始化HTTP对象
init();
return true;
}
else
{
return false;
}
}
}
}