终极成功促成了一个依照epoll的简易的HTTP服务器,目标是在Linux下写一个高性能Web服务器

近来五个月的业余时间在写一个亲信项目,目标是在Linux下写一个高性能Web服务器,名字叫Zaver。主体框架和基本功能已做到,还有局部尖端功能日后会逐渐增多,代码放在了github。Zaver的框架会在代码量尽量少的意况下接近工业水平,而不像一些讲义上的toy
server为了教原理而吐弃了不少本来server应该有的东西。在本篇作品中,我将一步步地申明Zaver的设计方案和开发进程中遇见遭逢的坚苦以及相应的缓解办法。

在上次的FreeBSD和linux的nginx静态文件性能比较测试皇冠现金app,
后,我萌发了投机出手做一个粗略的Web
Server来搞精通nginx高性能背后的法则的想法。最后成功促成了一个基于epoll的概括的HTTP服务器,实现了200,404,400,304响应,并且性能比nginx高了一点点。本文重要介绍这些HTTP服务器的原理和规划过程。

缘何要双重造轮子

几乎各种人每一日都要或多或少和Web服务器打交道,比较出名的Web
Server有Apache
Httpd、Nginx、IIS。这多少个软件跑在不少台机械上为我们提供稳定的服务,当您打开浏览器输入网址,Web服务器就会把音信传给浏览器然后展现在用户面前。这既然有那么多现成的、成熟稳定的web服务器,为何还要再一次造轮子,我认为理由有如下几点:

  • 夯实基础。一个优异的开发者必须有实在的基础,造轮子是一个很好的不二法门。学编译器?边看教科书变写一个。学操作系统?写一个原型出来。编程这几个圈子唯有和睦出手实现了才敢说真的会了。现在正值学网络编程,所以就打算写一个Server。

  • 兑现新功效。成熟的软件恐怕为了适应三菱的要求导致不会太考虑你一个人的出格要求,于是只好协调出手实现那一个分外需求。关于这或多或少Nginx做得万分得好了,它提供了让用户自定义的模块来定制自己索要的功力。

  • 扶植初学者容易地左右成熟软件的架构。比如Nginx,即使代码写得很美观,然则想完全看懂它的架构,以及它自定义的有些数据结构,得查万分多的素材和参照书籍,而那一个架构和数据结构是为了增进软件的可伸缩性和频率所计划的,无关高并发server的真面目部分,初学者会头昏。而Zaver用最少的代码显示了一个高并发server应有的指南,尽管没有Nginx性能高,得到的便宜是未曾Nginx那么复杂,server架构完全透露在用户眼前。

读书了部分篇章后,我整理出了以下要点:

读本上的server

学网络编程,第一个例证可能会是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
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。然则其中有一个复制父进程页表的操作,这也是为什么在Linux下创设过程比创造线程开销大的原由,而拥无线程都共享一个页表(关于为啥地址空间是COW但页表不是COW的因由,可以考虑一下)。

  • 过程调度器压力太大。当并发量上来了,系统里有这多少个进程,非常多的时刻将花在控制哪些进程是下一个周转过程以及上下文切换,这是异常不值得的。

  • 在heavy
    load下五个过程消耗太多的内存,在经过下,每一个接连都对应一个独自的地方空间;尽管在线程下,每一个连连也会占据独立。另外父子进程之间需要发出IPC,高并发下IPC带来的overhead不可忽略。

换用线程尽管能迎刃而解fork开销的题目,可是调度器和内存的问题要么不可能缓解。所以经过和线程在精神上是一样的,被称之为process-per-connection
model。因为不能处理高并发而不被业界使用。

一个十鲜显然的立异是用线程池,线程数量稳定,就没地点提到的题材了。基本架构是有一个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服务器有这么多少个形式:

事件驱动(伊芙(Eve)nt-driven)

假设有这么一个函数,在某个fd可以读的时候告诉我,而不是屡屡地去调用read,上边的题目不就化解了?这种情势叫做事件驱动,在linux下可以用select/poll/epoll这个I/O复用的函数来兑现(man
7
epoll),因为要不停知道什么样fd是可读的,所以要把这多少个函数放到一个loop里,那么些就叫事件循环(event
loop)。一个演示代码如下:

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

组合地点的研讨,我们得出了一个事件循环+ non-blocking I/O +
线程池的缓解方案,这也是Zaver的主题架构(同步的事件循环+non-blocking
I/O又被叫作Reactor模型)。
事件循环用作事件通报,假若listenfd上可读,则调用accept,把新建的fd插足epoll中;是平时的连续fd,将其投入到一个劳动者-消费者队列之中,等工作线程来拿。
线程池用来做总结,从一个劳动者-消费者队列里拿一个fd作为总结输入,直到读到EAGAIN结束,保存现在的处理状态(状态机),等待事件循环对这一个fd读写事件的下两回通报。

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

支付中相遇的题目

Zaver的周转架构在上文介绍完毕,下边将总括一下本身在开发时境遇的一部分不便以及一些解决方案。把开发中相见的困顿记录下来是个相当好的习惯,假如赶上问题查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在事变循环里会重回一个可读事件,然后就被分配给了某个线程,该线程read会重返0,代表对方已关门这么些fd,于是server端也调用close即可。

  • 既然把socket的fd设置为non-blocking,那么只要有一些数码包晚到了,这时候read就会回来-1,errno设置为EAGAIN,等待下次读取。这是就碰见了一个blocking
    read不曾境遇的题材,我们必须将已读到的数额保存下来,并保障一个场所,以代表是否还亟需多少,比如读到HTTP
    Request Header, GET /index.html HTT就得了了,在blocking
    I/O里假诺继续read就足以,但在nonblocking
    I/O,我们必须维护这个状态,下一回必须读到’P’,否则HTTP协议分析错误。

答:解决方案是保障一个状态机,在解析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,一个结构简单,帮助高产出的http服务器。基本架构是事件循环

  • non-blocking I/O +
    线程池。Zaver的代码风格参考了Nginx的品格,所以在可读性上非凡高。此外,Zaver提供了配备文件和命令行参数解析,以及完善的Makefile和源代码结构,也足以扶持任何一个C初学者入门一个品类是怎么构建的。如今自家的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)

皇冠现金app 1

 

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

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

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

 

皇冠现金app 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)。前者是用经过意况表示有事件爆发,后者通过情景变化表示事件发生。打个比方来说,使用情状触发的时候,只要缓冲区有数据,你就能检测到事件的留存。而采用边缘触发,你必须把缓冲区的数目总体读完事后,才能开展下五回事件的检测,否则,因为状态平素处在可读状态,没有爆发变化,你将永久收不到这么些事件。分明,后者对编写程序的严格性要求更高。

select和poll属于前者,epoll同时帮助这两种格局。值得一提的是,我要好测试了刹那间,发现就是在20000并发的情状下,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];
};

为了省事,我一直用一个大局数组装所有的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 );
    }
}

皇冠现金app 3