1. select/poll/epoll总结
-
调用函数:select和poll都是一个函数,epoll是一组函数
-
文件描述符数量:select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐。
poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目。
epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效。 -
将文件描述符从用户传给内核:select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝。
epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上。 -
内核判断就绪的文件描述符:
select和poll通过遍历文件描述符集合,判断哪个文件描述符上有事件发生。
epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后 epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list。 -
应用程序索引就绪文件描述符
select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历。
epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可。 -
工作模式
select和poll都只能工作在相对低效的LT模式下
epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。 -
应用场景
当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll。
当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll。
当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能。
2. ET、LT、EPOLLONESHOT
-
LT水平触发模式
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。
当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理。 -
ET边缘触发模式
epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件。
必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain。 -
EPOLLONESHOT
一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket。
我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件。
3. 主线程处理流程
- 主线程创建线程池并初始化,并且创建http_conn对象
- 建立socket连接,浏览器发送http请求
- 主线程使用epoll监控文件描述符事件的发生,如果epoll上存在新的事件,开始初始化用户数组user并存入user中。
- 如果epoll事件有读事件发生,一次性读取完客户数据,并将事件文件描述符添加到线程池的工作队列中。
// 创建线程池,初始化线程池
threadpool<http_conn> *pool = NULL;
try
{
pool = new threadpool<http_conn>;
}
catch (...)
{
return 1;
}
// 创建一个数组,用于保存所有的客户端信息
http_conn *users = new http_conn[MAX_FD];
// 创建监听套接字,使用IPv4(PF_INET)协议族,流式协议(SOCK_STREAM),第三个参数一般写0, 流式协议默认使用TCP,报式协议默认使用UDP
int listenfd = socket(PF_INET, SOCK_STREAM, 0); // 创建成功则返回文件描述符,失败则返回-1
// 设置端口复用,在服务器绑定端口之前设置(SOL_SOCKET是端口复用的级别,SO_REUSEADDR表示端口复用)
int reuse = 1; // 端口复用的值,1表示可以复用,0表示不可以复用
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
// 绑定(socket地址是一个结构体,封装了端口号和IP等信息)
struct sockaddr_in address; // 利用sockaddr_in创建socket地址(sockaddr_in用于IPv4,sockaddr_in6用于IPv6:)
address.sin_family = AF_INET; // 使用IPv4协议族
// h-host(主机字节序)、to(转换成什么)、n-network(网络字节序)、s-short unsigned short、l-long unsigned int
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port); // 转换端口号,将主机字节序转变为网络字节序
int ret = 0;
ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
assert(ret >= 0);
ret = listen(listenfd, 5); // 5表示未连接的和已经连接的和的最大值
assert(ret >= 0);
// 创建epoll对象,和事件数组,添加
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
// 添加到epoll对象中
addfd(epollfd, listenfd, false);
http_conn::m_epollfd = epollfd;
SIGALRM在经过TIMESLOT秒后发送给目前的进程
bool timeout = false; // 超时标志
bool stop_server = false; // 循环条件
client_data *user_timer = new client_data[MAX_FD]; // 创建用户的连接资源
while (!stop_server)
{
// epoll_wait等待所监控文件描述符上事件的产生,大于0返回就绪的文件描述符个数,等于0表示时间到,等于-1表示失败
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if ((number < 0) && (errno != EINTR))
{
LOG_ERROR("%s", "epoll failure\n");
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);
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
//水平触发模式
if (connfd < 0)
{
LOG_ERROR("%s:error is:%d", "accept error", errno);
continue;
}
if (http_conn::m_user_count >= MAX_FD)
{
// 目前连接已满,给客户端一个信息,显示服务器正忙
show_error(connfd, "Internal sever busy");
// 日志
LOG_ERROR("%s", "Internal sever busy");
continue;
}
// 将新的客户数据初始化放入用户数组中
users[connfd].init(connfd, client_address);
// 初始化新的客户的连接资源
user_timer[connfd].address = client_address;
user_timer[connfd].sockfd = connfd;
// 创建定时器,并设置连接资源、回调函数、超时时间
util_timer *timer = new util_timer;
timer->user_data = &user_timer[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
// 连接资源绑定定时器
user_timer[connfd].timer = timer;
// 将定时器添加到双向链表中
timer_lst.add_timer(timer);
}
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
// 对方异常断开或者出现错误等事件
// 移除链表上的定时器
util_timer *timer = user_timer[sockfd].timer;
timer->cb_func(&user_timer[sockfd]);
if (timer)
timer_lst.del_timer(timer);
users[sockfd].close_conn();
}
else if (events[i].events & EPOLLIN)
{
// 创建定时器临时变量,将该连接对应的定时器取出
util_timer *timer = user_timer[sockfd].timer;
if (users[sockfd].read_once())
{
LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
Log::get_instance()->flush();
// 一次性读取完所有数据
pool->append(users + sockfd);
}
}
}
}
3.1 process函数处理报文的请求与响应
判断请求队列上是否有数据,如果有,工作线程取出任务之后,调用process函数,分别调用process_read函数和process_write函数的解析和响应。
// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process()
{
// 解析HTTP请求
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);
}
3.2 process_read函数解析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为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。
请求头部:紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
HOST,给出请求资源所在服务器的域名。
User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
Accept,说明用户代理可处理的媒体类型。
Accept-Encoding,说明用户代理支持的内容编码。
Accept-Language,说明用户代理能够处理的自然语言集。
Content-Type,说明实现主体的媒体类型。
Content-Length,说明实现主体的大小。
Connection,连接管理,可以是Keep-Alive或close。
空行:请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
请求数据:也成为主体,可以添加任意的其他数据。
本项目只完成了GET请求报文的解析,POST请求后续改进。
- process_read函数通过主从状态机对请求报文进行处理,主状态机包括CHECK_STATE_REQUESTLINE(解析请求行)、CHECK_STATE_HEADER(解析请求头)、CHECK_STATE_CONTENT(解析请求体),从状态机包括LINE_OK(读取完整的行)、LINE_BAD(读取行出错)、LINE_OPEN(读取行数据不完整)。
- 初始化主状态机的初始状态为解析请求行,重点是进入分析数据的循环条件。
- switch判断属于那个主状态,若属于CHECK_STATE_REQUESTLINE,调用parse_request_line函数解析HTTP请求行,获得请求方法,目标URL,以及HTTP版本号,;若属于CHECK_STATE_HEADER,调用parse_headers函数解析HTTP请求的一个头部信息,如果返回一个GET_REQUEST(表示获得了一个完整的请求),跳转do_request函数生成响应报文;若属于
CHECK_STATE_CONTENT,调用parse_content函数判断是否完整的读入了,如果返回GET_REQUEST,跳转do_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_check_idx;
// printf("got 1 http line: %s\n", text);
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响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
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部分为响应正文。
3.3 process_write函数生成相应的响应报文
- 通过process_read函数解析后的报文,根据返回的HTTP_CODE,进入process_write函数中相应的逻辑模块。
- add_status_line函数,添加状态行:http/1.1 状态码 状态消息
- add_headers函数添加消息报头,内部调用add_content_length和add_linger函数。
content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据。
connection记录连接状态,用于告诉浏览器端保持长连接。 - add_blank_line添加空行。
- 如果FILE_REQUEST,向缓冲区中写入响应报文。
- http_conn::write_once()函数中writev分散写,将写缓冲区和请求的目标文件两块不连续的内存分别写出去,写缓冲区中包括状态行、消息报头等。
// 根据服务器处理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(400, error_400_title);
add_headers(strlen(error_400_form));
if (!add_content(error_400_form))
{
return false;
}
break;
case NO_RESOURCE:
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);
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;
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;
}