指标是在Linux下写一个高质量Web服务器,最终成功促成了一个基于epoll的简约的HTTP服务器

近年来四个月的业余时间在写八个亲信项目,目标是在Linux下写三个高品质Web服务器,名字叫Zaver。主体框架和基本成效已形成,还有部分高档成效日后会日益扩张,代码放在了github。Zaver的框架会在代码量尽量少的景观下接近工业水平,而不像一些讲义上的toy
server为了教原理而扬弃了成都百货上千本来server应该有的东西。在本篇小说中,笔者将一步步地阐明Zaver的设计方案和开发进度中遇见碰到的不方便以及相应的消除方法。

在上次的FreeBSD和linux的nginx静态文件质量相比测试
后,作者萌发了团结入手做多个回顾的Web
Server来搞掌握nginx高质量背后的原理的想法。最终成功完毕了二个依照epoll的差不离的HTTP服务器,实现了200,404,400,304响应,并且性能比nginx高了一丝丝。本文主要介绍这些HTTP服务器的规律和设计进度。

缘何要重新造轮子

差不多每个人每一日都要或多或少和Web服务器打交道,相比较著名的Web
Server有Apache
Httpd、Nginx、IIS。那个软件跑在广大台机器上为大家提供稳定的劳动,当您打开浏览器输入网址,Web服务器就会把音讯传给浏览器然后呈今后用户前边。那既然有那么多现成的、成熟稳定的web服务器,为何还要再度造轮子,作者认为理由有如下几点:

  • 抓实基础。叁个卓越的开发者必须有扎实的根基,造轮子是贰个很好的门径。学编写翻译器?边看教科书变写一个。学操作系统?写一个原型出来。编制程序这些领域只有本身动手达成了才敢说实在会了。未来正在学网络编制程序,所以就打算写七个Server。

  • 落实新职能。成熟的软件大概为了适应Subaru的供给导致不会太考虑你一位的不一致平常须求,于是只可以协调入手达成那个奇特供给。关于那一点Nginx做得特出得好了,它提供了让用户自定义的模块来定制本人必要的机能。

  • 支援初学者不难地操纵成熟软件的架构。比如Nginx,就算代码写得很漂亮,可是想全盘看懂它的架构,以及它自定义的片段数据结构,得查相当多的资料和参照书籍,而那几个架构和数据结构是为了升高软件的可伸缩性和频率所安顿的,毫无干系高并发server的精神部分,初学者会头晕。而Zaver用最少的代码展现了三个高并发server应有的旗帜,就算并未Nginx品质高,获得的好处是不曾Nginx那么复杂,server架构完全暴光在用户眼前。

阅读了有个别篇章后,作者收拾出了以下要点:

读本上的server

学网络编制程序,第3个例证恐怕会是Tcp
echo服务器。大约思路是server会listen在有个别端口,调用accept等待客户的connect,等客户连接上时会重回二个fd(file
descriptor),从fd里read,之后write同样的数据到那几个fd,然后重新accept,在网上找到一个十分好的代码实现,大旨代码是那样的:

while ( 1 ) {

    /*  Wait for a connection, then accept() it  */

    if ( (conn_s = accept(list_s, NULL, NULL) ) < 0 ) {
        fprintf(stderr, "ECHOSERV: Error calling accept()\n");
        exit(EXIT_FAILURE);
    }


    /*  Retrieve an input line from the connected socket
        then simply write it back to the same socket.     */

    Readline(conn_s, buffer, MAX_LINE-1);
    Writeline(conn_s, buffer, strlen(buffer));


    /*  Close the connected socket  */

    if ( close(conn_s) < 0 ) {
        fprintf(stderr, "ECHOSERV: Error calling close()\n");
        exit(EXIT_FAILURE);
    }
}

一体化兑以往这里
纵然您还不太懂这个程序,能够把它下载到本地编写翻译运转一下,用telnet测试,你会意识在telnet里输入什么,马上就会显得怎么。假诺你在此以前还不曾接触过互联网编制程序,恐怕会冷不丁精晓到,那和浏览器访问有个别网址然后音讯呈现在显示器上,整个原理是一模一样的!学会了这些echo服务器是怎么着工作的以后,在此基础上拓展成四个web
server11分简单,因为HTTP是起家在TCP之上的,无非多一些商议的剖析。client在建立TCP连接之后发的是HTTP协议头和(可选的)数据,server接受到多少后先解析HTTP协议头,依据商业事务头里的新闻发回相应的数额,浏览器把新闻彰显给用户,二回呼吁就做到了。

以此艺术是有个别书本教网络编制程序的专业例程,比如《深入领悟总结机种类》(CSAPP)在讲互联网编制程序的时候就用这么些思路实现了三个最简便易行的server,代码实现在这里,相当短,值得一读,特别是以此server即实现了静态内容又实现了动态内容,就算功效不高,但已落得教学的指标。之后那本书用事件驱动优化了那么些server,关于事件驱动会在背后讲。

固然如此那么些顺序能健康工作,但它完全无法投入到工业使用,原因是server在处理一个客户请求的时候不能够接受别的客户,约等于说,那些顺序不可能同时满意多个想取得echo服务的用户,那是十分小概忍受的,试想一下您在用微信,然后告诉你有人在用,你不能够不等10分人走精晓后才能用。

然后叁个革新的缓解方案被提议来了:accept以往fork,父进度继续accept,子进度来拍卖这么些fd。这么些也是一些讲义上的专业示例,代码大致长这么:

/* Main loop */
    while (1) {
        struct sockaddr_in their_addr;
        size_t size = sizeof(struct sockaddr_in);
        int newsock = accept(listenfd, (struct sockaddr*)&their_addr, &size);
        int pid;

        if (newsock == -1) {
            perror("accept");
            return 0;
        }

        pid = fork();
        if (pid == 0) {
            /* In child process */
            close(listenfd);
            handle(newsock);
            return 0;
        }
        else {
            /* Parent process */
            if (pid == -1) {
                perror("fork");
                return 1;
            }
            else {
                close(newsock);
            }
        }
    }

全体代码在
这里。表面上,那几个顺序化解了前方只好处理单客户的难题,但依据以下几点首要缘由,依旧不能投入工业的高并发使用。

  • 老是来二个接连都fork,花费太大。任何讲Operating
    System的书都会写,线程能够知道为轻量级的进度,那进度到底重在什么地点?《Linux
    Kernel
    Development》有一节(Chapter3)专门讲了调用fork时,系统实际做了什么样。地址空间是copy
    on
    write的,所以不造成overhead。但是中间有二个复制父进度页表的操作,那也是为啥在Linux下创设进度比创制线程费用大的原委,而拥有线程都共享三个页表(关于为什么地方址空间是COW但页表不是COW的缘由,能够考虑一下)。

  • 进程调度器压力太大。当并发量上来了,系统里有不少进度,非凡多的日子将花在支配哪些进度是下3个周转进度以及上下文切换,那是不行不值得的。

  • 在heavy
    load下四个进程消耗太多的内存,在经过下,每多少个三番五次都对应二个单身的地点空间;即使在线程下,每3个老是也会占用独立。其余父子进度之间需求发出IPC,高并发下IPC带来的overhead不可忽略。

换用线程即使能缓解fork花费的题材,不过调度器和内部存款和储蓄器的标题照旧不能消除。所以经过和线程在真相上是一致的,被叫做process-per-connection
model。因为不能处理高并发而不被产业界使用。

二个要命鲜明的修正是用线程池,线程数量稳定,就没地点提到的难点了。基本架构是有2个loop用来accept连接,之后把那几个一连分配给线程池中的某些线程,处理完了后头这几个线程又有什么不可处理别的连接。看起来是个要命好的方案,但在其实际景况况中,很多连接都以长连接(在三个TCP连接上实行反复通讯),二个线程在接受职务之后,处理完第②批来的多少,此时会另行调用read,天知道对方怎么时候发来新的数据,于是那几个线程就被那一个read给阻塞住了(因为默认意况下fd是blocking的,即只要那个fd上未曾数量,调用read会阻塞住进度),什么都不可能干,固然有n个线程,第(n+1)个长连接来了,依旧不恐怕处理。

怎么做?大家发现标题是出在read阻塞住了线程,所以化解方案是把blocking
I/O换到non-blocking
I/O,那时候read的做法是一旦有多少则赶回数据,假设没有可读数据就回来-1并把errno设置为EAGAIN,注解下次有数量了本人再来继续读(man
2 read)。

此间有个难点,进度怎么了解这么些fd哪一天来多少又有什么不可读了?那里要引出贰个重庆大学的概念,事件驱动/事件循环。

福寿双全多出现的socket服务器有那样多少个法子:

事件驱动(伊芙nt-driven)

一经有诸如此类叁个函数,在有些fd能够读的时候告诉本人,而不是反复地去调用read,上边的题材不就解决了?那种办法叫做事件驱动,在linux下能够用select/poll/epoll这几个I/O复用的函数来落实(man
7
epoll),因为要时时刻刻知道怎样fd是可读的,所以要把那一个函数放到三个loop里,这一个就叫事件循环(event
loop)。2个示范代码如下:

while (!done)
{
  int timeout_ms = max(1000, getNextTimedCallback());
  int retval = epoll_wait(epds, events, maxevents, timeout_ms);

  if (retval < 0) {
     处理错误
  } else {
    处理到期的 timers

    if (retval > 0) {
      处理 IO 事件
    }
  }
}

在这个while里,调用epoll_wait会将经过阻塞住,直到在epoll里的fd产生了当下登记的事件。这里有个要命好的例证来呈现epoll是怎么用的。要求注脚的是,select/poll不富有伸缩性,复杂度是O(n),而epoll的复杂度是O(1),在Linux下工业程序都以用epoll(别的平台有各自的API,比如在Freebsd/MacOS下用kqueue)来打招呼进度哪些fd产生了轩然大波,至于缘何epoll比前双方成效高,请参见这里

事件驱动是完成高品质服务器的主要性,像Nginx,lighttpd,Tornado,NodeJs都以依照事件驱动落成的。

  1. 多进度共享2个监听端口

Zaver

组合地方的商量,大家得出了3个风浪循环+ non-blocking I/O +
线程池的缓解方案,那也是Zaver的主旨架构(同步的事件循环+non-blocking
I/O又被誉为Reactor模型)。
事件循环用作事件通报,要是listenfd上可读,则调用accept,把新建的fd出席epoll中;是不乏先例的连日fd,将其投入到贰个劳动者-消费者队列之中,等工作线程来拿。
线程池用来做总括,从多个劳动者-消费者队列里拿2个fd作为计量输入,直到读到EAGAIN截止,保存以往的处理处境(状态机),等待事件循环对这几个fd读写事件的下壹次通报。

bind之后选用fork()创立一份当前进程的正片,并运营子进程。子进度采用阻塞式accept、read、write,即那个操作会阻塞线程,直到操作实现才继续执行。缺点是经过之间通讯速度慢,每一种进程占用很多内部存款和储蓄器,所以并发数一般受限于进度数。

支付中遇见的标题

Zaver的周转架构在上文介绍达成,上边将总计一下本身在开发时碰到的某个艰辛以及一些消除方案。把开发中遇见的艰巨记录下来是个可怜好的习惯,假设遇到标题查google找到个缓解方案一向照搬过去,不做任何记录,也从未考虑,那么下次你遇见同样的题材,依旧会再也一次搜索的长河。有时大家要做代码的创设者,不是代码的“搬运工”。做记录定期回看蒙受的标题会使本人成长更快。

  • 万一将fd放入生产者-消费者队列中后,得到那些职责的办事线程还从未读完这几个fd,因为没读完数据,所以这么些fd可读,那么下一回事件循环又赶回那几个fd,又分给别的线程,怎么处理?

答:那里提到到了epoll的两种工作方式,一种叫边缘触发(艾德ge
Triggered),另一种叫水平触发(Level
Triggered)。ET和LT的命名是万分形象的,ET是代表在气象改变时才公告(eg,在边缘上从低电平到高电平),而LT表示在这一个意况才通告(eg,只要处于低电平就公告),对应的,在epoll里,ET代表假若有新数据了就文告(状态的改动)和“只要有新数据”就间接会布告。

举个有血有肉的事例:假诺某fd上有2kb的数量,应用程序只读了1kb,ET就不会在下一回epoll_wait的时候回来,读完事后又有新数据才回来。而LT每一遍都会回来那个fd,只要这么些fd有多少可读。所以在Zaver里我们供给用epoll的ET,用法的方式是固定的,把fd设为nonblocking,如果回到某fd可读,循环read直到EAGAIN(如若read再次来到0,则远端关闭了连年)。

  • 当server和浏览器保持着1个长连接的时候,浏览器突然被关闭了,那么server端怎么处理那几个socket?

答:此时该fd在事变循环里会重临一个可读事件,然后就被分配给了有些线程,该线程read会重回0,代表对方已关闭这么些fd,于是server端也调用close即可。

  • 既然如此把socket的fd设置为non-blocking,那么一旦有一对数目包晚到了,那时候read就会回去-1,errno设置为EAGAIN,等待下次读取。那是就碰见了三个blocking
    read不曾蒙受的难题,大家务必将已读到的数量保存下去,并保险1个情景,以象征是或不是还亟需多少,比如读到HTTP
    Request Header, GET /index.html HTT就甘休了,在blocking
    I/O里假如继续read就足以,但在nonblocking
    I/O,我们必须保证这些情景,下3回必须读到’P’,不然HTTP协议分析错误。

答:化解方案是敬服七个状态机,在解析Request
Header的时候对应一个状态机,解析Header
Body的时候也维护3个状态机,Zaver状态机的时候参考了Nginx在解析header时的落到实处,作者做了一部分不难和陈设上的改观。

  • 怎么较好的落到实处header的解析

答:HTTP
header有不少,必然有成都百货上千个解析函数,比如解析If-modified-since头和剖析Connection头是分别调用四个不等的函数,所以那边的安顿性必须是一种模块化的、易拓展的布置,能够使开发者很不难地修改和概念针对不一样header的剖析。Zaver的贯彻形式参考了Nginx的做法,定义了3个struct数组,个中每四个struct存的是key,和对应的函数指针hock,假使条分缕析到的headerKey
== key,就调hock。定义代码如下

zv_http_header_handle_t zv_http_headers_in[] = {
    {"Host", zv_http_process_ignore},
    {"Connection", zv_http_process_connection},
    {"If-Modified-Since", zv_http_process_if_modified_since},
    ...
    {"", zv_http_process_ignore}
};
  • 怎么样存款和储蓄header

答:Zaver将拥有header用链表连接了起来,链表的贯彻参考了Linux内核的双链表达成(list_head),它提供了一种通用的双链表数据结构,代码相当班值日得一读,笔者做了简化和改变,代码在这里

  • 压力测试

答:那些有很多再三考虑的方案了,比如http_load, webbench,
ab等等。笔者最终选项了webbench,理由是大约,用fork来模拟client,代码只有几百行,出题目能够马上依照webbench源码定位到底是哪位操作使Server挂了。其它因为后边提到的八个题材,我仔细看了下韦布ench的源码,并且越发推荐C初学者看一看,唯有几百行,不过涉及了命令行参数解析、fork子进程、父子进度用pipe通讯、信号handler的登记、创设HTTP协议头的技巧等片段编制程序上的技艺。

  • 用韦布ech测试,Server在测试甘休时挂了

答:百思不得其解,不管时间跑多长时间,并发量开多少,都以在结尾webbench停止的每一日,server挂了,所以小编猜想肯定是这一刻发生了何等“事情”。
早先调剂定位错误代码,我用的是打log的点子,前边的事实注明在此间那不是很好的主意,在二十三十二线程环境下要经过看log的法门固定错误是一件相比辛勤的事。最终log输出把错误定位在向socket里write对方伸手的文件,也正是系统调用挂了,write挂了难道不是回来-1的呢?于是唯一的诠释便是进度接受到了某signal,那么些signal使进度挂了。于是用strace重新开始展览测试,在strace的输出log里发现了难题,系统在write的时候接受到了SIGPIPE,默许的signal
handler是甘休进度。SIGPIPE发生的原因为,对方早已关门了那一个socket,但进度还往里面写。所以作者预计webbench在测试时间到了将来不会等待server数据的回到间接close掉全部的socket。抱着那样的存疑去看webbench的源码,果然是那样的,webbench设置了多少个定时器,在不奇怪测试时间会读取server重回的数据,并寻常close;而当测试时间一到就间接close掉全体socket,不会读server重回的数量,那就造成了zaver往1个已被对方关闭的socket里写多少,系统一发布送了SIGPIPE。

解决方案也万分简单,把SIGPIPE的信号handler设置为SIG_IGN,意思是忽视该信号即可。

  1. 多线程

不足

当下Zaver还有很多革新的地点,比如:

  • 今昔新分配内部存款和储蓄器都是经过malloc的法门,之后会改成内部存款和储蓄器池的章程
  • 还不协理动态内容,前期初始考虑扩充php的支撑
  • HTTP/1.1较复杂,如今只兑现了多少个重点的(keep-alive, browser
    cache)的header解析
  • 不运动总是的逾期过期还未曾做

类似多进度,只可是用线程代替了经过。主线程负责accept,为种种请求建立二个线程(大概使用线程池复用线程)。比多进度速度快,占用更少的内存,稳定性不及多进度。因为种种线程都有协调的库房空间,其占用的内部存储器依旧无法排除的,所以并发数一般受限于线程数。

总结

正文介绍了Zaver,一个组织不难,援助高产出的http服务器。基本架构是事件循环

  • non-blocking I/O +
    线程池。Zaver的代码风格参考了Nginx的作风,所以在可读性上那些高。其余,Zaver提供了配备文件和命令行参数解析,以及宏观的Makefile和源代码结构,也能够扶助任何多个C初学者入门1个类型是怎么构建的。最近本人的wiki就用Zaver托管着。

五个阻塞式IO程序的流水生产线示例图:

参考资料

[1]
https://github.com/zyearn/zaver

[2]
http://nginx.org/en/

[3] 《linux八线程服务端编制程序》

[4]
http://www.martinbroadhurst.com/server-examples.html

[5]
http://berb.github.io/diploma-thesis/original/index.html

[6] <a href=”http://tools.ietf.org/html/rfc2616
target=”_blank”>rfc2616</a>

[7]
https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/

[8] Unix Network Programming, Volume 1: The Sockets Networking API
(3rd Edition)

图片 1

 

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

单线程,将socket设置为非阻塞格局(accept、read、write会立刻回到。若是已经accept完了富有的连日,或读光了缓冲区的多寡,只怕写满了缓冲区,会回去-1,而不是跻身阻塞状态)。使用select或epoll等体制,同时监听多个IO操作有无事件时有爆发。当在那之中的3个或多少个处于Ready状态(即:监听的socket能够accept,tcp连接能够read等)后,立刻处理相应的风云,处理完后立马回到监听状态(注意那里的监听是监听IO事件,不是监听端口)。也就是阻塞式IO编制程序中私自一处都大概回到主循环中继续守候,并能从等待中直接再次来到原处继续执行;而accept、读、写都不再阻塞,阻塞全体运动到了二个多事件监听操作中。

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

 

图片 2

举例来说,要是在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)。前者是用经过情景表示有事件产生,后者通过意况变化意味着事件产生。打个比方来说,使用情状触发的时候,只要缓冲区有多少,你就能检查和测试到事件的留存。而接纳边缘触发,你必须把缓冲区的多少总体读完之后,才能拓展下1次事件的检查和测试,否则,因为状态从来处在可读状态,没有爆发变化,你将永久收不到这些事件。明显,后者对编写程序的严刻性要求更高。

select和poll属于前者,epoll同时帮助那两种形式。值得一提的是,作者要好测试了弹指间,发现即使在30000并发的场馆下,epoll使用那三种形式在此之前品质差别仍是可以够忽略不计。

别的部须要要注意的是,对于健康文件设置非阻塞是不起效能的。

  1. 别的还有异步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];
};

为了省事,作者平素用3个大局数组装全部的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数组中找1个还没利用的空位,伊始化,然后把这几个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 );
    }
}

图片 3

相关文章