说到底成功落到实处了3个依据epoll的回顾的HTTP服务器,Zaver的框架会在代码量尽量少的情形下接近工业水平

近日3个月的业余时间在写2个私人项目,目标是在Linux下写3个高质量Web服务器,名字叫Zaver。主体框架和基本功用已做到,还有一部分尖端功用日后会渐渐增多,代码放在了github。Zaver的框架会在代码量尽量少的情事下接近工业水平,而不像有些课本上的toy
server为了教原理而放任了好多原本server应该某些东西。在本篇作品中,小编将一步步地讲明Zaver的设计方案和支付进程中相遇遭受的紧巴巴以及对应的缓解办法。

在上次的FreeBSD和linux的nginx静态文件品质比较测试
后,作者萌发了协调出手做三个简易的Web
Server来搞精通nginx高质量背后的规律的想法。最终成功落到实处了3个依照epoll的简要的HTTP服务器,完成了200,404,400,304响应,并且品质比nginx高了一点点。本文首要介绍那一个HTTP服务器的原理和安插性进度。

何以要双重造轮子

差不离每种人每日都要或多或少和Web服务器打交道,相比较盛名的Web
Server有Apache
Httpd、Nginx、IIS。这几个软件跑在许多台机器上为大家提供稳定的服务,当你打开浏览器输入网址,Web服务器就会把消息传给浏览器然后展今后用户日前。那既然有那么多现成的、成熟稳定的web服务器,为啥还要再一次造轮子,小编觉着理由有如下几点:

  • 抓实基础。一个出色的开发者必须有踏实的基础,造轮子是3个很好的路线。学编译器?边看教科书变写多少个。学操作系统?写三个原型出来。编程那个小圈子唯有协调动手完成了才敢说实在会了。以后正值学互连网编程,所以就打算写二个Server。

  • 已毕新功能。成熟的软件大概为了适应斯Leica的急需导致不会太考虑你1个人的出格须要,于是只可以自身入手落成这么些奇异要求。关于那或多或少Nginx做得十一分得好了,它提供了让用户自定义的模块来定制自身要求的功效。

  • 帮扶初学者简单地控制成熟软件的架构。比如Nginx,即使代码写得很美丽,可是想完全看懂它的架构,以及它自定义的一些数据结构,得查极度多的材质和参照书籍,而那么些架构和数据结构是为了加强软件的可伸缩性和频率所布署的,无关高并发server的真面目部分,初学者会眩晕。而Zaver用最少的代码体现了一个高并发server应有的榜样,固然没有Nginx质量高,得到的好处是绝非Nginx那么复杂,server架构完全暴光在用户面前。

读书了有的文章后,小编整理出了以下要点:

教材上的server

学互联网编程,第二个例子只怕会是Tcp
echo服务器。差不离思路是server会listen在有个别端口,调用accept等待客户的connect,等客户连接上时会重回二个fd(file
descriptor),从fd里read,之后write同样的数量到那个fd,然后再度accept,在网上找到3个尤其好的代码达成,宗旨代码是如此的:

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
server万分简单,因为HTTP是确立在TCP之上的,无非多一些协议的解析。client在确立TCP连接之后发的是HTTP协议头和(可选的)数据,server接受到多少后先解析HTTP协议头,依照商事头里的信息发回相应的多寡,浏览器把音讯显示给用户,两回呼吁就形成了。

其一艺术是有的图书教互连网编程的正规化例程,比如《长远精晓计算机连串》(CSAPP)在讲网络编程的时候就用那么些思路完成了三个最简单易行的server,代码实将来这里,不短,值得一读,尤其是以此server即落成了静态内容又完毕了动态内容,即使效能不高,但已达标教学的目标。之后那本书用事件驱动优化了那几个server,关于事件驱动会在前面讲。

虽说那些顺序能寻常干活,但它完全无法投入到工业使用,原因是server在处理多少个客户请求的时候不能承受其余客户,约等于说,那些顺序不可以同时满意五个想得到echo服务的用户,那是无能为力忍受的,试想一下你在用微信,然后告诉你有人在用,你不可以不等分外人走了后来才能用。

然后一个更上一层楼的消除方案被提议来了: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。不过其中有2个复制父进程页表的操作,那也是为何在Linux下开创进度比创立线程成本大的原故,而具备线程都共享一个页表(关于为什么地点址空间是COW但页表不是COW的原由,可以考虑一下)。

  • 过程调度器压力太大。当并发量上来了,系统里有很多进度,万分多的时刻将花在支配哪些过程是下1个周转进度以及上下文切换,那是尤其不值得的。

  • 在heavy
    load下七个进度消耗太多的内存,在经过下,每1个老是都对应七个独自的地址空间;即便在线程下,每二个接连也会占用独立。别的父子进度之间须求发出IPC,高并发下IPC带来的overhead不可忽略。

换用线程尽管能化解fork费用的标题,然则调度器和内存的标题还是不大概缓解。所以经过和线程在精神上是一样的,被叫做process-per-connection
model。因为不可以处理高并发而不被业界使用。

三个至极通晓的创新是用线程池,线程数量稳定,就没地点提到的难题了。基本架构是有贰个loop用来accept连接,之后把那些连续分配给线程池中的某些线程,处理完了未来那一个线程又可以处理其他连接。看起来是个相当好的方案,但在骨子里情形中,很多连接都以长连接(在二个TCP连接上开展数十次通讯),多个线程在收取职责之后,处理完第2批来的数据,此时会重新调用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哪一天来多少又能够读了?那里要引出1个要害的定义,事件驱动/事件循环。

落到实处多产出的socket服务器有那般多少个格局:

事件驱动(伊芙nt-driven)

即使有这么壹个函数,在有些fd可以读的时候告诉自个儿,而不是累累地去调用read,上面的题材不就化解了?那种格局叫做事件驱动,在linux下可以用select/poll/epoll那么些I/O复用的函数来兑现(man
7
epoll),因为要不停知道怎么着fd是可读的,所以要把那么些函数放到二个loop里,那些就叫事件循环(event
loop)。1个演示代码如下:

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. 多进度共享多个监听端口

Zaver

组成方面的切磋,我们得出了3个事变循环+ non-blocking I/O +
线程池的缓解方案,那也是Zaver的核心架构(同步的风云循环+non-blocking
I/O又被叫作Reactor模型)。
事件循环用作事件通报,尽管listenfd上可读,则调用accept,把新建的fd参与epoll中;是一般的连年fd,将其投入到二个劳动者-消费者队列之中,等工作线程来拿。
线程池用来做总括,从三个劳动者-消费者队列里拿3个fd作为计量输入,直到读到EAGAIN甘休,保存以后的拍卖景况(状态机),等待事件循环对那几个fd读写事件的下几回通报。

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

支出中碰着的难点

Zaver的运行架构在上文介绍已毕,上边将统计一下自身在付出时相遇的一对困难以及一些缓解方案。把开发中碰着的紧巴巴记录下来是个13分好的习惯,借使碰着难题查google找到个缓解方案平昔照搬过去,不做任何笔录,也从不思考,那么下次你遇上相同的题目,依然会再也五次搜索的经过。有时大家要做代码的创立者,不是代码的“搬运工”。做笔录定期回看遇到的标题会使本身成长更快。

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

答:那里提到到了epoll的三种工作格局,一种叫边缘触发(Edge
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和浏览器保持着多少个长连接的时候,浏览器突然被关门了,那么server端怎么处理那么些socket?

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

  • 既然把socket的fd设置为non-blocking,那么只要有部分数码包晚到了,那时候read就会回去-1,errno设置为EAGAIN,等待下次读取。那是就遇上了二个blocking
    read不曾碰着的标题,咱们务必将已读到的多寡保存下来,并爱戴3个状态,以表示是还是不是还索要多少,比如读到HTTP
    Request Header, GET /index.html HTT就终止了,在blocking
    I/O里假如继续read就足以,但在nonblocking
    I/O,我们务必保障这一个情形,下三遍必须读到’P’,否则HTTP协议分析错误。

答:消除方案是珍重1个状态机,在解析Request
Header的时候对应五个状态机,解析Header
Body的时候也爱护三个状态机,Zaver状态机的时候参考了Nginx在解析header时的贯彻,小编做了有个别简洁和安插性上的更改。

  • 怎么较好的贯彻header的剖析

答:HTTP
header有不少,必然有成百上千个解析函数,比如解析If-modified-since头和剖析Connection头是分别调用八个不相同的函数,所以那边的规划必须是一种模块化的、易拓展的统筹,可以使开发者很不难地修改和概念针对不一样header的解析。Zaver的落成方式参考了Nginx的做法,定义了三个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往二个已被对方关闭的socket里写多少,系统发送了SIGPIPE。

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

  1. 多线程

不足

当前Zaver还有很多立异的地点,比如:

  • 明日新分配内存都以因而malloc的法门,之后会改成内存池的章程
  • 还不协助动态内容,前期开首考虑扩充php的协理
  • HTTP/1.1较复杂,方今只兑现了多少个关键的(keep-alive, browser
    cache)的header解析
  • 不移步总是的超时过期还从未做

恍如多进度,只但是用线程代替了经过。主线程负责accept,为每种请求建立七个线程(恐怕使用线程池复用线程)。比多进程速度快,占用更少的内存,稳定性不及多进度。因为各种线程都有和好的仓库空间,其占用的内存如故不可以清除的,所以并发数一般受限于线程数。

总结

正文介绍了Zaver,3个布局简单,支持高产出的http服务器。基本架构是事件循环

  • non-blocking I/O +
    线程池。Zaver的代码风格参考了Nginx的品格,所以在可读性上13分高。别的,Zaver提供了安插文件和命令行参数解析,以及周密的Makefile和源代码结构,也得以匡助其余一个C初学者入门3个品种是怎么创设的。近期自小编的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操作有无事件暴发。当其中的二个或多少个处于Ready状态(即:监听的socket可以accept,tcp连接可以read等)后,立时处理相应的事件,处理完后当即回到监听状态(注意那里的监听是监听IO事件,不是监听端口)。相当于阻塞式IO编程中自由一处都恐怕回到主循环中继承伺机,并能从等待中向来回到原处继续执行;而accept、读、写都不再阻塞,阻塞全体移动到了贰个多事件监听操作中。

2个非阻塞式IO程序的流水线示例图:

 

图片 2

比方来说,若是在A连接的Read
request的经过中,缓冲区数码读完了,而请求还尚无为止,直接重返到主循环中监听其余事件。而此刻假诺发现另一个Send了二分之一的Response连接B变为了可写状态,则向来处理B连接Send
Response事件,从上次B连接写了3/6的地点伊始,继续写入数据。那样一来,纵然是单线程,但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使用这二种方式以前质量差别仍可以够忽略不计。

除此以外索要专注的是,对徐婧常文件设置非阻塞是不起功用的。

  1. 除此以外还有异步IO,一般在Windows上利用,这里就不谈了。

除此以外nginx使用了Linux的sendfile函数。和观念的用户程序自身read和write差异,sendfile接收八个文本描述符,直接在基本中贯彻复制操作,相比read和write,能够裁减内核态和用户态的切换次数,以及数额拷贝的次数。

接下去正式开班筹划。笔者选取了非阻塞IO,epoll的边缘触发格局。先找了个相比完好的使用epoll的贰个socket
server例子作为参照,然后在它的底蕴上面修改边做试验。

其一例子相比较简单,而且也一贯不显示出非阻塞IO编程。然而通过它自身打听到了epoll的核心采取办法。

为了促成产出通讯,大家须求把程序“摊平”。

第1、,分析大家的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

 

接下来,定义1个社团用于保存这个变量:

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有关的第3部分贴出来:

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 );
    }
}

图片 3

相关文章