C++什么写一个Web服务器

读本上的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曾几何时来数量又有什么不可读了?那里要引出一个根本的定义,事件驱动/事件循环。

为什么要再度造轮子

差点每个人每日都要或多或少和Web服务器打交道,相比较显赫的Web
Server有Apache
Httpd、Nginx、IIS。那么些软件跑在众多台机器上为大家提供稳定的劳务,当您打开浏览器输入网址,Web服务器就会把音讯传给浏览器然后突显在用户面前。这既然有那么多现成的、成熟稳定的web服务器,为啥还要再一次造轮子,我以为理由有如下几点:

  • 夯实基础。一个良好的开发者必须有踏实的功底,造轮子是一个很好的路径。学编译器?边看教科书变写一个。学操作系统?写一个原型出来。编程这么些小圈子唯有和谐出手落成了才敢说真的会了。现在正值学网络编程,所以就打算写一个Server。

  • 落到实处新成效。成熟的软件恐怕为了适应斯Leica的须要导致不会太考虑你一个人的异样要求,于是只好自己入手已毕那一个特殊必要。关于这或多或少Nginx做得一定得好了,它提供了让用户自定义的模块来定制自己索要的效果。

  • 接济初学者不难地控制成熟软件的架构。比如Nginx,固然代码写得很美丽,不过想完全看懂它的架构,以及它自定义的局地数据结构,得查卓殊多的素材和参照书籍,而这一个架构和数据结构是为了加强软件的可伸缩性和功能所布署的,毫不相关高并发server的本来面目部分,初学者会眩晕。而Zaver用最少的代码显示了一个高并发server应有的榜样,尽管没有Nginx性能高,得到的益处是未曾Nginx那么复杂,server架构完全揭发在用户眼前。

参考资料

[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)

Zaver

组合地点的研商,我们得出了一个风云循环+ non-blocking I/O +
线程池的缓解方案,那也是Zaver的焦点架构(同步的事件循环+non-blocking
I/O又被称之为Reactor模型)。
事件循环用作事件通报,要是listenfd上可读,则调用accept,把新建的fd参加epoll中;是惯常的连天fd,将其加盟到一个劳动者-消费者队列之中,等工作线程来拿。
线程池用来做总计,从一个劳动者-消费者队列里拿一个fd作为计量输入,直到读到EAGAIN截至,保存现在的拍卖情状(状态机),等待事件循环对那个fd读写事件的下两遍通报。

付出中相遇的题材

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挂了。此外因为背后提到的一个题目,我仔细看了下韦·布(We·bb)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,意思是忽视该信号即可。

总结

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

  • non-blocking I/O +
    线程池。Zaver的代码风格参考了Nginx的风骨,所以在可读性上卓殊高。此外,Zaver提供了安排文件和命令行参数解析,以及完善的Makefile和源代码结构,也足以帮助任何一个C初学者入门一个系列是怎么构建的。近日本人的wiki就用Zaver托管着。

近日多少个月的业余时间在写一个私人项目,目标是在Linux下写一个高性能Web服务器,名字叫Zaver。主体框架和基本成效已形成,还有一部分高档效能日后会逐步增加,代码放在了github。Zaver的框架会在代码量尽量少的境况下接近工业水平,而不像一些教材上的toy
server为了教原理而甩掉了许多原先server应该有些东西。在本篇小说中,我将一步步地评释Zaver的设计方案和开发进度中相见遭逢的困顿以及对应的化解方式。

事件驱动(伊夫(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都是基于事件驱动已毕的。

C++,不足

此时此刻Zaver还有很多改良的地点,比如:

  • 方今新分配内存都是经过malloc的方式,之后会改成内存池的方法
  • 还不协助动态内容,中期开端考虑增加php的支撑
  • HTTP/1.1较复杂,近日只兑现了多少个基本点的(keep-alive, browser
    cache)的header解析
  • 不运动总是的过期过期还没有做