Web Server 和 HTTP协议(转)
转自:http://www.kuqin.com/shuoit/20150809/347488.html
一直在找实习,有点什么东西直接就在evernote里面记了,也没时间来更新到这里。找实习真是个蛋疼的事,一直找的是困难模式的C++的后台开发这种职位,主要是因为其他的更不会了。虽然找的是C++的职位,但是我的简历有俩项目都是php的,因为老赵的项目就是用php做网站。最近越来越感觉这样的简历不靠谱,想换个C++的和网络有关的多线程的项目吧。所以最近准备点几个网络和多线程的技能点。于是我看了tinyhttpd、LightCgiServer和吴导的husky。基本上对着吴导的husky抄了个paekdusan,但是也不能纯粹的抄一遍啊,所以还是改了一些小东西,大的框架没变。主要的改变包括以下几方面:
- 在线程池部分中,使用C++11的thread替代了pthread,从而实现跨平台的目标
- 在支持并发的队列中,使用C++11的mutex和lock替代了pthread的mutex和lock,从而实现跨平台的目标
- 在socket部分,使用了预编译宏的方式,从而实现跨平台的目标
- 接收数据部分更健壮,以面对不能一次性读完一个HTTP头部的情况;发送也一样
- 实现了一个具有简易的KeepAlive策略的HTTP服务器
- 实现了一个静态文件的HTTP服务器
tinyhttpd和LightCgiServer
首先,还是先介绍一下tinyhttpd吧。网上的评价还是很高的,能让人仅从500-600行的代码中了解HTTP Server的本质。 贴一张tinyhttpd的流程图吧:
关于tinyhttpd更详细的信息,大家还是直接去看代码吧,因为真的很易读、易懂。tinyhttpd的代码给人的感觉就是,怎么易读、易懂怎么来,例如服务器回复一个501 Method Not Implemented
的response是这么写的,看到我就惊呆了,只能怪我以前看过的代码太少,我第一反应就是先sprintf到一个长的buff里面,然后一起send,但是它这样的写法确实更加易懂、易读。
void unimplemented(int client) { char buf[1024]; sprintf(buf, "HTTP/1.0 501 Method Not Implementedrn"); send(client, buf, strlen(buf), 0); sprintf(buf, SERVER_STRING); send(client, buf, strlen(buf), 0); sprintf(buf, "Content-Type: text/htmlrn"); send(client, buf, strlen(buf), 0); sprintf(buf, "rn"); send(client, buf, strlen(buf), 0); sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implementedrn"); send(client, buf, strlen(buf), 0); sprintf(buf, "</TITLE></HEAD>rn"); send(client, buf, strlen(buf), 0); sprintf(buf, "<BODY><P>HTTP request method not supported.rn"); send(client, buf, strlen(buf), 0); sprintf(buf, "</BODY></HTML>rn"); send(client, buf, strlen(buf), 0); }
除此之外,值得一提的是tinyhttpd实现的是一个CGI Server的功能,但是在CGI的功能上实现得比较简陋,LightCgiServer实现得更完整一些,关于CGI Server更详细的情况请看CGI Server
husky和paekdusan
正如本文开头所说,大的程序结构上,paekdusan基本是对着husky抄的,只是做了一些小的改变。程序在大的结构上,可以看作是一个生产者消费者模型
。
先看一个不伦不类的流程图:
从上图可以看出,主线程是生产者
,线程池中的线程们是消费者
,它们之间通过task队列来通信。主线程作为生产者
,accept成功返回之后,将处理该client的task添加到task队列中,然后继续accept等待client的到来;线程池的线程们作为消费者
,不断的从task队列中取出task,调用task的run接口。
值得注意的是,task队列是一个BoundedBlockingQueue
,也就是说,task队列是一个有容量限制,并且阻塞的队列。当消费者试图从task队列中取task时,如果task队列是空的,则消费者会被阻塞,直到生产者往task队列中放入task,将消费者唤醒。同样的,当生产者试图向task队列中放入task时,如果task队列是满的,则生产者会被阻塞,直到消费者从task队列中取出task,将生产者唤醒。
再看一个不伦不类的时序图:
这里要求task实现了run接口,task队列的设计可以认为是command模式
的实践。看上去有很多类,但是其实是因为每个类的功能比较单一,程序只是把一些功能单一的类组合在一起了而已,其实类之间的耦合性比较低。
具体实现的代码见这里paekdusan
问题记录
HTTP协议的基本格式
HTTP的request的第一部分是request line,以空格分割得到的三部分依次是method,URI和version
HTTP的request的第二部分是header,header以rn结尾,header中的每一行也以rn结尾,也就是说,当header是空时,以一个rn结尾;当header不空时,一定是以两个连续的rn结尾的。heder中的每一行格式是 key : value,其中value可以是空,所以简单的说,header是一个map,键和值之间用:分隔,键值对之间用rn分隔,在map的最后还有一个rn。值得注意的是,cookie是在header里面的。
HTTP的request的第三部分是body,协议规定body后面不能再有其他字符,所以body不能靠去找rn来结束,要靠header里面的content-length来指明,content-length就是body的字节数。
HTTP的response的第一部分是response line,以空格分割得到的三部分依次是version,status code和Reason Phrase
HTTP的response的第二部分是header,格式和request类似
HTTP的response的第三部分是body,格式和request类似
另外,还有一点,我不知道request里面有没有可能出现,反正在response里面是会出现的,那就是如果header中指明了transfer-coding是chunked,那么body将会是一串chunked的块。在HTTP协议的rfc2616中是这么定义chunk-body的格式的:
Chunked-Body = *chunk last-chunk trailer CRLF chunk = chunk-size [ chunk-extension ] CRLF chunk-data CRLF chunk-size = 1*HEX last-chunk = 1*("0") [ chunk-extension ] CRLF chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) chunk-ext-name = token chunk-ext-val = token | quoted-string chunk-data = chunk-size(OCTET) trailer = *(entity-header CRLF)
也就是说chunk由四部分组成,首先是若干个chunk块(每个chunk块由chunk-size,可选的chunk-extension,rn, chunk-data 和 rn组成),接着是last-chunk块(chunk-size是0,没有chunk-data的特殊chunk块),然后是trailer(若干和header一样格式的数据组成),最后是一个rn。其实不看中间“可选的chunk-extension”还是比较简单的。
KeepAlive的实现
在之前husky的代码中,server端发送了response之后,就close socket了,即关闭该socket,如果client需要再次发送http request需要再次建立一个新的tcp连接。而打开一个常见的网页,通常有很多http request从client发送到server,那么就需要很多次tcp的建立和断开,比较低效。之所以用KeepAlive就是为了避免多次请求需要重复的建立TCP连接,也就是说server端发送完response之后,不关闭连接,而是在该连接上继续等待数据。KeepAlive在HTTP1.1是默认开启的,如果要关闭,需要在header中声明connection: close。
但是朴素的KeepAlive会引起一些问题,例如client一直不断开连接,那么和client的连接一直保持,client多了的时候,新来的client无法获得server的资源,所以需要一些其他的折衷。例如如果接下来的5s内都没有收到数据则断开连接,或者是接下来的5s内服务器接收了100个客户端请求就断开连接。
由于“5s内服务器接收了100个客户端请求就断开连接”这需要在根据一个线程外部的信息控制线程的运行,使得线程运行过程中对于外部的以来过多,故而paekdusan没有这么实现,而是在同一个连接上接收了50个http request之后断开连接。另外paekdusan还实现了“一个连接的持续时间超过5s就断开连接”,具体来说是这样的,recv超时时间1s,每次recv完数据之后判断距离第一次recv的时间是否超过5s,超过则断开连接。详见如下代码:
//简单起见 删除了一些处理不完整http请求的代码,并且简化了now 和 startTime的设置 //详见https://github.com/aholic/paekdusan/blob/master/KeepAliveWorker.hpp while ((now - startTime <= 5) && requestCount > 0) { recvLen = recv(sockfd, recvBuff, RECV_BUFFER_SIZE, 0); if (recvLen <= 0 && getLastErrorNo() == ERROR_TIMEOUT) { LogInfo("recv returns %d", recvLen); continue; } if (recvLen <= 0) { LogError("recv failed: %d", getLastErrorNo()); break; } //do with recvBuff, get a http request requestCount--; }
但是由于使用的是阻塞的recv,所以实现的不是非常合理,存在一些问题。 例如此时恰好距离第一次recv只有4.99秒,所以(now - startTime <= 5)满足,继续进入while循环,然后阻塞在recv上,recv设置的超时是1秒,那么其实最后跳出while循环的时间距离第一次recv已经过去了5.99秒。暂时没想到什么好办法,因为把recv设置成理解返回的话,while循环的次数太多,效率也不高。所以最好是要有一种通知的机制。
CGI Server
CGI Server一般是要fork一个进程来执行http request的URI中指定的CGI Script的,并且通过环境变量,向CGI Script传递本次请求的信息,具体怎么做可以看看这篇文章。但是注意到paekdusan是一个多线程的服务器,所以这里涉及到多线程和多进程,这是个很蛋疼的情况。多线程和多进程的混合会有很多问题,这篇文章有详细的介绍,好吧,你可能会发现它被墙了,那我还是简单的介绍一下会有什么问题吧。
首先需要说明的是,在一个子线程中调用fork会发生什么:产生的子进程中只会有一个线程,也就是调用fork的这个线程。
那么问题来了,假如父进程中的其他线程获取了一个锁,正在改线程间共享的数据,这时共享数据处于半修改状态。但是在子进程中,其他线程都消失了,那这些共享数据的修改怎么办?并且,锁的状态也得未定义了。另外,即使你的代码是线程安全的,你也不能保证你用的Lib的实现是线程安全的。
所以唯一合理的在多线程的环境下使用多进程的情况,只有fork之后立即exec,也就是马上讲子进程替换成一个新的程序,这样的话,子进程中所有的数据都变得不重要,都抛弃了。所以,其实多线程的CGI Server也算是合理,但是需要注意安全性问题。因为打开的子进程默认是继承了父进程的文件描述符的,也就是说,子进程可以有父进程对文件的读写权限。
我大概知道的就这么多,更详细的还是翻墙去读原文吧。
C++11的thread
C++11的thread用起来感觉比以前的linux上的pthread或者是windows上的beginthread都好用太多来,来一段简短的代码展示一下基本用法吧。
void sayWord(const string& word) { for (int i = 0; i < 1000; i++) { cout << word << endl; } } void saySentence(const string& sentence) { for (int i = 0; i < 1000; i++) { cout << sentence << endl; } } int main() { string word = "hello"; string sentence = "this is an example from cstdlib.com"; thread t1(std::bind(sayWord, ref(word))); thread t2(saySentence, ref(sentence)); t1.join(); t2.join(); return 0; }
运行以上代码,会发现交替输出“hello”和“this is an example from cstdlib.com”。注意到t1和t2的构造参数看起来怪怪的,“std::bind(sayWord, ref(word))”和“saySentence, ref(sentence)”,主要线程函数的参数是引用,有个模版里面的bind在这,我也很难解释清楚,感觉模版叼叼的。另外,以非静态类成员函数创建线程时,需要在参数中带上this或者是ref一个对象的实例,不然无法调用。
C++11的mutex和lock
注意到上面thread的代码其实是有问题的,两个线程交替输出的东西可能会混合,所以要加锁。C++11的mutex和lock也很好用。
代码里面用的是lock_guard,lock_guard就是在构造函数里面调用mutex的lock方法,析构函数里面调用mutex的unlock方法,用起来比较方便。unique_lock和lock_guard类似,但是多了一些其他的成员函数来配合其他的mutex类。关于mutex和lock的用法,[这篇博客](http://www.cnblogs.com/haippy/p/3237213.html)说的比较详细。主要和pthread里面的lock的区别就是,在pthread中,重复获取一个已经获得的lock不会报错,而在C++11中会报错。
C++11的condition_variable
在paekdusan的task队列是
BoundedBlockingQueue
,也就是说有阻塞和唤醒的操作。所以涉及到condition_variable。condition_variable主要用两个函数:wait(unique_lock<mutex>& lck)
和notify_one()
。线程被wait阻塞时,会先调用lck.unlock()函数释放锁;线程被notify_one唤醒时,会调用lck.lock()获取锁,以回复当时wait前的样子。关于condition_variable的用法,这篇博客说的比较详细windows和linux上socket的API的不同点
- windows上需包含winsock.h;linux上需包含cerrno,sys/socket.h,sys/types.h,arpa/inet.h和unistd.h
- windows上调用socket之前要调用WSAStartup,并且用#pragma comment(lib,”Ws2_32”)链接Ws2_32.lib
- windows上关闭socket的函数叫做closesocket;linux上叫做close
- windows上获取错误码用GetLastError();linux上查看全局变量errno,错误码的意义也不一样
- windows上设置SO_RCVTIMEO和SO_SNDTIMEO选项时,单位是毫秒;linux是秒
- windows上accept的原型是accept(SOCKET, struct sockaddr, int);linux上accept的原型是accept(int, struct sockaddr, socklen_t)
我遇到的就这些,估计还有很多,只是我还没遇到。
Web Server 和 HTTP协议(转)的更多相关文章
- Web Server 和 HTTP 协议
https://toutiao.io/posts/xm2fr/preview 一直在找实习,有点什么东西直接就在evernote里面记了,也没时间来更新到这里.找实习真是个蛋疼的事,一直找的是困难模式 ...
- Jexus Web Server 完全傻瓜化图文配置教程(基于Ubuntu 12.04.3 64位)[内含Hyper-v 2012虚拟机镜像下载地址]
1. 前言 近日有感许多新朋友想尝试使用Jexus,不过绝大多数都困惑徘徊在Linux如何安装啊,如何编译Mono啊,如何配置Jexus啊...等等基础问题,于是昨日向宇内流云兄提议,不如搞几个配置好 ...
- C#中自己动手创建一个Web Server(非Socket实现)
目录 介绍 Web Server在Web架构系统中的作用 Web Server与Web网站程序的交互 HTTPListener与Socket两种方式的差异 附带Demo源码概述 Demo效果截图 总结 ...
- 【转】推荐介绍几款小巧的Web Server程序
原博地址:http://blog.csdn.net/heiyeshuwu/article/details/1753900 偶然看到几个小巧有趣的Web Server程序,觉得有必要拿来分享一下,让大家 ...
- web server && web framework角色区分
问题 web framework是否包括webserver? 是否可以包括? webserver 和 framework的关系是? https://www.quora.com/What-is-the- ...
- Server Develop (九) Simple Web Server
Simple Web Server web服务器hello world!-----简单的socket通信实现. HTTP HTTP是Web浏览器与Web服务器之间通信的标准协议,HTTP指明了客户端如 ...
- Chapter 1: A Simple Web Server
这算是一篇读书笔记,留着以后复习看看. Web Server又称为Http Server,因为它使用HTTP协议和客户端(一般是各种各样的浏览器)进行通信. 什么是HTTP协议呢? HTTP协议是基于 ...
- 如何写一个简单的Web Server(一)
在本篇博文中我将介绍如何写一个Web Server.博文中大部分资料我是参考的这篇文章(http://www.linuxhowtos.org/C_C++/socket.htm),英文不错的同学可以 ...
- web server服务器
使用最多的 web server服务器软件有两个:微软的信息服务器(iis),和Apache. 通俗的讲,Web服务器传送(serves)页面使浏览器可以浏览,然而应用程序服务器提供的是客户端应用程序 ...
随机推荐
- 怎样把function中的arguments变成普通数组
当我们在写一个具有处理可变长度参数的函数时,需要对arguments做一些操作.但是arguments它并不是一个数组,没有数组的各种操作,而且,JS的严格模式中不允许更改它的值. 这时我们需要将它的 ...
- 《HTML5与CSS3基础教程》学习笔记 ——Four Day
第十六章 1. 输入和元素 电子邮件框 <input type="email"> 搜索框 <input type="search"> ...
- hdu 1963 Investment 多重背包
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1963 //多重背包 #include <cstdio> #include <cstr ...
- eclipse优化(部分)
1. 增强Eclipse(MyEclipse)输入代码提示功能 一般设置: (1). 打开Eclipse,选择打开" Window -- Preferences". (2). 在目 ...
- Ubuntu下第一个C程序的成功运行
对于每个新手来说,进入Ubuntu最想做的事莫过于在终端(Terminal)里运行自己的第一个C/C++程序"hello.c/hello.cpp"了. 很多语言书籍都是默认搭载好运 ...
- centos6.5安装fpm打包工具
FPM功能简单说就是将一种类型的包转换成另一种类型.FPM的github:https://github.com/jordansissel/fpm 1.支持的源类型包: dir: 将目录打包成所需要的类 ...
- C语言的sizeof
今天帮同学想用C实现数组的折半查找,本来算法挺简单的,可是折腾了好几个小时才发现问题在哪,这个sizeof坑人不浅啊. #include<stdio.h> void m(int []); ...
- Java集合的小抄
在尽可能短的篇幅里,将所有集合与并发集合的特征.实现方式.性能捋一遍.适合所有"精通Java",其实还不那么自信的人阅读. [转自:花钱的年华] 期望能不止用于面试时,平时选择数据 ...
- poll实现
struct pollfd { int fd; //当前描述符 short events; //进程关心的该描述符的事件 short revents; //返回 ...
- cocos2dx中的坐标体系
1.UI坐标系和GL坐标系 2.本地坐标与世界坐标 本地坐标是一个相对坐标,是相对于父节点或者你指明的某个节点的相对位置来说的,本地坐标的原点在参考节点的左下角 世界坐标是一个绝对的坐标,是以屏幕的左 ...