linux http服务 卡顿,实现了一个比Nginx速度更快的HTTP服务器

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

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

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

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

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

2. 多线程

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

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

1cc02f6cc4bf48910aadb1960253edf0.png

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

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

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

1fedcfc7d2b8838938c76dc73abc7083.png

举例来说,如果在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 < n; 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 );

}

}0b1331709591d260c1c78e86d0c51c18.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值