热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

实现了一个比Nginx速度更快的HTTP服务器

在上次的FreeBSD和linux的nginx静态文件性能对比测试后,我萌发了自己动手做一个简单的WebServer来搞清楚nginx高性能背后的原理的想法。最后成功实现了一个基于epoll的简单的HTTP服务器,实现了200,404,400,304响应,并且性能比nginx高了一点点。本文主
在上次的FreeBSD和linux的nginx静态文件性能对比测试 后,我萌发了自己动手做一个简单的Web Server来搞清楚nginx高性能背后的原理的想法。最后成功实现了一个基于epoll的简单的HTTP服务器,实现了200,404,400,304响应,并且性能比nginx高了一点点。本文主要介绍这个HTTP服务器的原理和设计过程。

阅读了一些文章后,我整理出了以下要点:

实现多并发的socket服务器有这样几个方法:

1. 多进程共享一个监听端口

bind之后使用fork()创建一份当前进程的拷贝,并启动子进程。子进程采用阻塞式accept、read、write,即这些操作会阻塞线程,直到操作完成才继续执行。缺点是进程之间通信速度慢,每个进程占用很多内存,所以并发数一般受限于进程数。

2. 多线程

类似多进程,只不过用线程代替了进程。主线程负责accept,为每个请求建立一个线程(或者使用线程池复用线程)。比多进程速度快,占用更少的内存,稳定性不及多进程。因为每个线程都有自己的堆栈空间,其占用的内存还是无法免除的,所以并发数一般受限于线程数。

一个阻塞式IO程序的流程示例图:

QQ截图20110923131031

 

3. 事件驱动的非阻塞IO(nonblocking I/O)

单线程,将socket设置为非阻塞模式(accept、read、write会立即返回。如果已经accept完了所有的连接,或读光了缓冲区的数据,或者写满了缓冲区,会返回-1,而不是进入阻塞状态)。使用select或epoll等机制,同时监听多个IO操作有无事件发生。当其中的一个或多个处于Ready状态(即:监听的socket可以accept,tcp连接可以read等)后,立即处理相应的事件,处理完后立即回到监听状态(注意这里的监听是监听IO事件,不是监听端口)。相当于阻塞式IO编程中任意一处都可能回到主循环中继续等待,并能从等待中直接回到原处继续执行;而accept、读、写都不再阻塞,阻塞全部移动到了一个多事件监听操作中。

一个非阻塞式IO程序的流程示例图:

 

QQ截图20110923131039

举例来说,如果在A连接的Read request的过程中,缓冲区数据读完了,而请求还没有结束,直接返回到主循环中监听其它事件。而这时如果发现另一个Send了一半的Response连接B变为了可写状态,则直接处理B连接Send Response事件,从上次B连接写了一半的地方开始,继续写入数据。这样一来,虽然是单线程,但A和B同时进行,互不干扰。

因为流程更加复杂,无法依靠线程的堆栈保存每个连接处理过程中的各种状态信息,我们需要自己维护它们,这种编程方式需要更高的技巧。比方说,原先我们可以在send_response函数中用局部变量保存发送数据的进度,而现在我们只能找一块其它的地方,为每一个连接单独保存这个值了。

nginx即使用事件驱动的非阻塞IO模式工作。

nginx支持多种事件机制:跨平台的select,Linux的poll和epoll,FreeBSD的kqueue,Solaris的/dev/poll等。在高并发的情况下,在Linux上使用epoll性能最好,或者说select的性能太差了。

事件机制分为水平触发,或译状态触发(level-triggered)和边缘触发(edge-triggered)。前者是用通过状态表示有事件发生,后者通过状态变化表示事件发生。打个比方来说,使用状态触发的时候,只要缓冲区有数据,你就能检测到事件的存在。而使用边缘触发,你必须把缓冲区的数据全部读完之后,才能进行下一次事件的检测,否则,因为状态一直处于可读状态,没有发生变化,你将永远收不到这个事件。显然,后者对编写程序的严谨性要求更高。

select和poll属于前者,epoll同时支持这两种模式。值得一提的是,我自己测试了一下,发现即使在20000并发的情况下,epoll使用这两种模式之前性能差异仍可以忽略不计。

另外需要注意的是,对于常规文件设置非阻塞是不起作用的。

4. 此外还有异步IO,一般在Windows上使用,这里就不谈了。

另外nginx使用了Linux的sendfile函数。和传统的用户程序自己read和write不同,sendfile接收两个文件描述符,直接在内核中实现复制操作,相比read和write,可以减少内核态和用户态的切换次数,以及数据拷贝的次数。

接下来正式开始设计。我选择了非阻塞IO,epoll的边缘触发模式。先找了个比较完整的使用epoll的一个socket server例子作为参考,然后在它的基础上边修改边做实验。

这个例子比较简单,而且也没有体现出非阻塞IO编程。不过通过它我了解到了epoll的基本使用方法。

为了实现并发通信,我们需要把程序“摊平”。

首先,分析我们的HTTP服务器通信过程用到的变量:

状态

Wait for reading

Wait for writing

次数

变量类型

非本地变量

备注

Accept

Y

N

n

local

   

Read request

Y

N

n

nonlocal

Read buf

 

Open file

N

N

n

nonlocal

文件名

 

Send response header

N

Y

n

nonlocal

Response header buf

 

Read file -> Send response content

N

Y

n*n

nonlocal

Read&write buf

Write pos

fd

Sock

读满read buf或读到EOF,再发

发送时将read buf

Close file

N

N

n

 

fd

 

Close socket

N

N

n

 

sock

 

然后,定义一个结构用于保存这些变量:

struct process {
int sock;
int status;
int response_code;
int fd;
int read_pos;
int write_pos;
int total_length;
char buf[BUF_SIZE];
};

为了简便,我直接用一个全局数组装所有的process:

static struct process processes[MAX_PORCESS];

另外定义每个连接通信过程中的三个状态:

#define STATUS_READ_REQUEST_HEADER    0
#define STATUS_SEND_RESPONSE_HEADER 1
#define STATUS_SEND_RESPONSE 2

之后,就是按部就班地实现主循环、读取request,解析header,判断文件是否存在、检查文件修改时间,发送相应的header和content了。

下面只把程序中跟epoll有关的关键部分贴出来:

main()函数:

使用epoll_create()创建一个epoll fd,注意,这里的listen_sock已经设置为nonblocking(我使用setNonblocking函数)了:

    efd = epoll_create1 ( 0 );
if ( efd == -1 )
{
...
}

event.data.fd = listen_sock;
event.events = EPOLLIN | EPOLLET;
s = epoll_ctl ( efd, EPOLL_CTL_ADD, listen_sock, &event );
if ( s == -1 )
{
...
}

/* Buffer where events are returned */
events = calloc ( MAXEVENTS, sizeof event );

这里的EPOLLIN表示监听“可读”事件。

在主循环中epoll_wait():

    while ( 1 )
{
int n, i;

n = epoll_wait ( efd, events, MAXEVENTS, -1 );
if ( n == -1 )
{
perror ( "epoll_wait" );
}
for ( i = 0; i {
if ( ( events[i].events & EPOLLERR ) ||
( events[i].events & EPOLLHUP ) )
{
fprintf ( stderr, "epoll error\n" );
close ( events[i].data.fd );
continue;
}

handle_request ( events[i].data.fd );

}
}

epoll_wait()会在发生事件后停止阻塞,继续执行,并把发生了事件的event的file descriptor放入events中,返回数组大小。注意的是,这里要循环处理所有的fd。


接下来是关键部分:

void handle_request ( int sock )
{
if ( sock == listen_sock )
{
accept_sock ( sock );
}
else
{
struct process* process = find_process_by_sock ( sock );
if ( process != 0 )
{
switch ( process->status )
{
case STATUS_READ_REQUEST_HEADER:
read_request ( process );
break;
case STATUS_SEND_RESPONSE_HEADER:
send_response_header ( process );
break;
case STATUS_SEND_RESPONSE:
send_response ( process );
break;
default:
break;
}
}
}
}

根据epoll返回的fd,做不同处理:如果是监听的socket,则accept();否则,根据sock的fd查找相应的process结构体,从中取回状态信息,返回到之前的处理状态中。这样就能实现信春哥,死后原地复活的状态恢复机制了。

在accept中,将accept出来的连接也设置为非阻塞,然后在process数组中找一个还没使用的空位,初始化,然后把这个socket存到process结构体中:

struct process* accept_sock ( int listen_sock )
{
int s;
// 在ET模式下必须循环accept到返回-1为止
while ( 1 )
{
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
if ( current_total_processes >= MAX_PORCESS )
{
// 请求已满,accept之后直接挂断
infd = accept ( listen_sock, &in_addr, &in_len );
if ( infd == -1 )
{
if ( ( errno == EAGAIN ) ||
( errno == EWOULDBLOCK ) )
{
break;
}
else
{
perror ( "accept" );
break;
}
}
close ( infd );

return;
}

in_len = sizeof in_addr;
infd = accept ( listen_sock, &in_addr, &in_len );
if ( infd == -1 )
{
if ( ( errno == EAGAIN ) ||
( errno == EWOULDBLOCK ) )
{
break;
}
else
{
perror ( "accept" );
break;
}
}

getnameinfo ( &in_addr, in_len,
hbuf, sizeof hbuf,
sbuf, sizeof sbuf,
NI_NUMERICHOST | NI_NUMERICSERV );

//设置为非阻塞
s = setNonblocking ( infd );
if ( s == -1 )
abort ();
int on = 1;
setsockopt ( infd, SOL_TCP, TCP_CORK, &on, sizeof ( on ) );
//添加监视sock的读取状态
event.data.fd = infd;
event.events = EPOLLIN | EPOLLET;
s = epoll_ctl ( efd, EPOLL_CTL_ADD, infd, &event );
if ( s == -1 )
{
perror ( "epoll_ctl" );
abort ();
}
struct process* process = find_empty_process_for_sock ( infd );
current_total_processes++;
reset_process ( process );
process->sock = infd;
process->fd = NO_FILE;
process->status = STATUS_READ_REQUEST_HEADER;
}
}

三个不同状态对应三个不同函数进行处理,我就不全贴了,以read_request为例:

void read_request ( struct process* process )
{
int sock = process->sock, s;
char* buf=process->buf;
char read_complete = 0;

ssize_t count;

while ( 1 )
{
count = read ( sock, buf + process->read_pos, BUF_SIZE - process->read_pos );
if ( count == -1 )
{
if ( errno != EAGAIN )
{
handle_error ( process, "read request" );
return;
}
else
{
//errno == EAGAIN表示读取完毕
break;
}
}
else if ( count == 0 )
{
// 被客户端关闭连接
cleanup ( process );
return;
}
else if ( count > 0 )
{
process->read_pos += count;
}
}

int header_length = process->read_pos;
// determine whether the request is complete
if ( header_length > BUF_SIZE - 1 )
{
process->response_code = 400;
process->status = STATUS_SEND_RESPONSE_HEADER;
strcpy ( process->buf, header_400 );
send_response_header ( process );
handle_error ( processes, "bad request" );
return;
}
buf[header_length]=0;
read_complete = ( strstr ( buf, "\n\n" ) != 0 ) || ( strstr ( buf, "\r\n\r\n" ) != 0 );

if ( read_complete )
{
// ...

//解析之后,打开文件,把文件描述符存入process,然后进入发送header状态
process->status = STATUS_SEND_RESPONSE_HEADER;
//修改此sock的监听状态,改为监视写状态
event.data.fd = process->sock;
event.events = EPOLLOUT | EPOLLET;
s = epoll_ctl ( efd, EPOLL_CTL_MOD, process->sock, &event );
if ( s == -1 )
{
perror ( "epoll_ctl" );
abort ();
}
//发送header
send_response_header ( process );
}
}

推荐阅读
  • Linux一键安装web环境全攻略
    摘自阿里云服务器官网,此处一键安装包下载:点此下载安装须知1、此安装包可在阿里云所有Linux系统上部署安装,此安装包包含的软件及版本为& ... [详细]
  • svnWebUI:一款现代化的svn服务端管理软件
    svnWebUI是一款图形化管理服务端Subversion的配置工具,适用于非程序员使用。它解决了svn用户和权限配置繁琐且不便的问题,提供了现代化的web界面,让svn服务端管理变得轻松。演示地址:http://svn.nginxwebui.cn:6060。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 本文介绍了在Hibernate配置lazy=false时无法加载数据的问题,通过采用OpenSessionInView模式和修改数据库服务器版本解决了该问题。详细描述了问题的出现和解决过程,包括运行环境和数据库的配置信息。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 目录浏览漏洞与目录遍历漏洞的危害及修复方法
    本文讨论了目录浏览漏洞与目录遍历漏洞的危害,包括网站结构暴露、隐秘文件访问等。同时介绍了检测方法,如使用漏洞扫描器和搜索关键词。最后提供了针对常见中间件的修复方式,包括关闭目录浏览功能。对于保护网站安全具有一定的参考价值。 ... [详细]
  • Linux下部署Symfoy2对app/cache和app/logs目录的权限设置,symfoy2logs
    php教程|php手册xml文件php教程-php手册Linux下部署Symfoy2对appcache和applogs目录的权限设置,symfoy2logs黑色记事本源码,vsco ... [详细]
  • 现在比较流行使用静态网站生成器来搭建网站,博客产品着陆页微信转发页面等。但每次都需要对服务器进行配置,也是一个重复但繁琐的工作。使用DockerWeb,只需5分钟就能搭建一个基于D ... [详细]
  • Skywalking系列博客1安装单机版 Skywalking的快速安装方法
    本文介绍了如何快速安装单机版的Skywalking,包括下载、环境需求和端口检查等步骤。同时提供了百度盘下载地址和查询端口是否被占用的命令。 ... [详细]
  • LVS实现负载均衡的原理LVS负载均衡负载均衡集群是LoadBalance集群。是一种将网络上的访问流量分布于各个节点,以降低服务器压力,更好的向客户端 ... [详细]
  • 本文介绍了在无法联网的情况下,通过下载rpm包离线安装zip和unzip的方法。详细介绍了如何搜索并下载合适的rpm包,以及如何使用rpm命令进行安装。 ... [详细]
  • windows用什么nginx,iis不香么 ... [详细]
  • nginx+多个tomcat
    学习nginx的时候遇到的问题:nginx怎么部署两台tomcat?upstream在网上找的资源,我在nginx配置文件(nginx.conf)中添加了两个server。结果只显 ... [详细]
  • Nginx Buffer 机制引发的下载故障
    Nginx ... [详细]
author-avatar
祖朝伯
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有