https://blog.youkuaiyun.com/lf_2016/article/details/53511648
https://blog.youkuaiyun.com/yhhwatl/article/details/52538843
https://blog.youkuaiyun.com/hujian_/article/details/51063935
https://blog.youkuaiyun.com/chenjiayi_yun/article/details/26178809
https://blog.youkuaiyun.com/effective_coder/article/details/8991980
http://www.voidcn.com/article/p-zoldlivm-bcq.html
https://blog.youkuaiyun.com/hujian_/article/details/51100463
https://blog.youkuaiyun.com/bit_clearoff/article/details/53507613
https://blog.youkuaiyun.com/bit_clearoff/article/details/53503846
https://www.cnblogs.com/lang5230/p/5556611.html
https://blog.youkuaiyun.com/qq_26768741/article/details/70477766
https://blog.youkuaiyun.com/qq_33951180/article/details/70239745
https://blog.youkuaiyun.com/virgofarm/article/details/81116681
https://blog.youkuaiyun.com/SmartBrave/article/details/50909179
https://blog.youkuaiyun.com/watson2016/article/details/52415429
https://blog.youkuaiyun.com/watson2016/article/details/52415382
https://github.com/wallmamami/http
https://blog.youkuaiyun.com/Watson2016/article/details/52415487
https://blog.youkuaiyun.com/qq_36110777/article/details/73556838
project 1:模拟空间配置器
https://blog.youkuaiyun.com/lf_2016/article/details/53511648
https://blog.youkuaiyun.com/qq_42606051/article/details/82111108
项目背景:
1)小块内存带来的内存碎片问题
单从分配的角度来看。由于频繁分配、释放小块内存容易在堆中造成外碎片(极端情况下就是堆中空闲的内存总量满足一个请求,但是这些空闲的块都不连续,导致任何一个单独的空闲的块都无法满足这个请求)。
2)小块内存频繁申请释放带来的性能问题。
开辟空间的时候,分配器会去找一块空闲块给用户,找空闲块也是需要时间的,尤其是在外碎片比较多的情况下。如果分配器其找不到,就要考虑处理假碎片现象(释放的小块空间没有合并),这时候就要将这些已经释放的的空闲块进行合并,这也是需要时间的。
3)小块空间太多会造成空间的浪费
每一次malloc 函数开辟出一块堆空间 在返回的指针前几个字节保存了开辟空间的大小。 这样free()的时候才知道传进去的指针到底意味着的多大的空间。 所以多开辟的空间叫配置空间。小空间向系统申请过多,这些记录空间大小的内存就太多造成内存浪费。
4)malloc new 出来的空间要 free delete 释放 如果忘记不释放会发生内存泄漏。
剖析一下STL里面的空间配置器(SGI版)。
STL里面的空间配置主要分为两级,一级空间配置器(__malloc_alloc_template)和二级空间配置器(__default_alloc_template)。在STL中默认如果要分配的内存大于128个字节的话就是大块内存,调用一级空间配置器直接向系统申请,如果小于等于128个字节的话则认为是小内存,则就去内存池中申请。一级空间配置器很简单,直接封装了malloc和free处理,增加_malloc_alloc_oom_handle处理机制。二级空间配置器才是STL的精华,二级空间配置器主要由memoryPool+freelist构成。
项目实现的功能:
1)内存分配通过一级配置器和二级配置器合作完成。其中一级配置器负责分配大块内存,二级配置器负责分配小块内存(此处以128字节为界限)。
2)一级配置器直接调用库函数malloc,二级配置器模拟实现一个内存池,通过指针数组管理特定大小的内存链表,指针数组的成员链接从内存池申请空间,再将空间分配给客户端。
3)当内存池大小小于需求量,并且指针数组所管理的链表中无大块内存供给时,调用一级配置器重新为内存池分配空间。
4)对象构造和析构运用了类型萃取技术,该技术同时也运用于对大块内存空间进行拷贝和填充的过程中。
项目的逻辑步骤:
二级空间配置器的逻辑步骤:
假如现在申请n个字节:
1、判断n是否大于128,如果大于128则直接调用一级空间配置器。如果不大于,则将n上调至8的倍数处,然后再去自由链表中相应的结点下面找,如果该结点下面挂有未使用的内存,则摘下来直接返回这块空间的地址。否则的话我们就要调用refill(size_t n)函数去内存池中申请。
2、向内存池申请的时候可以多申请几个,STL默认一次申请nobjs=20个,将多余的挂在自由链表上,这样能够提高效率。
进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申请,如果申请成功的话,再回到refill函数。
这时候就有两种情况,如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。否则就表示nobjs大于1,则将多余的内存块挂到自由链表上。
如果chunk_alloc失败的话,在他内部有处理机制。
3、进入chunk_alloc(size_t n,size_t& nobjs )向内存池申请空间的话有三种情况:
3.1、内存池剩余的空间足够nobjsn这么大的空间,则直接分配好返回就可以了。
3.2、内存池剩余的空间leftAlloc的范围是n<=leftAlloc<nobjsn,则这时候就分配nobjs=(leftAlloc)/n这么多个的空间返回。
3.3、内存池中剩余的空间连一个n都不够了,这时候就要向heap申请内存,不过在申请之前先要将内存池中剩余的内存挂到自由链表上,之后再向heap申请。
3.3.1、如果申请成功的话,则就再调一次chunk_alloc重新分配。
3.3.2、如果不成功的话,这时候再去自由链表中看看有没有比n大的空间,如果有就将这块空间还给内存池,然后再调一次chunk_alloc重新分配。
3.3.3、如果没有的话,则就调用一级空间配置器分配,看看内存不足处理机制能否处理。
整体架构:
项目的难点:
总结:
空间配置器的其他问题:
1、在空间配置器中所有的函数和变量都是静态的,所以他们在程序结束的时候才会被释放发。二级空间配置器中没有将申请的内存还给操作系统,只是将他们挂在自由链表上。所以说只有当你的程序结束了之后才会将开辟的内存还给操作系统。
2、由于它没有将内存还给操作系统,所以就会出现二种极端的情况。
2.1、假如我不断的开辟小块内存,最后将整个heap上的内存都挂在了自由链表上,但是都没有用这些空间,再想要开辟一个大块内存的话会开辟失败。
2.2、再比如我不断的开辟char,最后将整个heap内存全挂在了自由链表的第一个结点的后面,这时候我再想开辟一个16个字节的内存,也会失败。
或许我比较杞人忧天吧,总的来说上面的情况只是小概率情况。如果非得想办法解决的话,我想的是:针对2.1我们可以引入释放二级空间配置器的方法,但是这个释放比较麻烦。针对2.2我们可以合并自由链表上的连续的小的内存块。
3、二级空间配置器会造成内碎片问题,极端的情况下一直申请char,则就会浪费7/8的空间。但是整体来说,空间配置器的性能还是蛮高的。
project 2:基于HTTP协议的web服务器
https://blog.youkuaiyun.com/qq_26768741/article/details/70477766
项目目的:
整个的项目采用B/S模式,通过浏览器发送HTTP的get方法和post方法,然后server进行响应,这样最终通过html看到我们所显示的最终的效果。为了支持并发,采用了多线程的结构。
项目实现的功能:
1)简单实现多线程版http服务器,支持GET方法和POST方法,支持cgi机制。
2)服务器将客户请求的资源以html页面的形似呈现,并能够进行差错处理(如:客户请求的资源不存在时,服务器能够返回一个404的页面)。
3)服务器能进行简单的cgi运行。比如当客户在表单中输入数据后,服务器能够将运行结果返回个客户。
4)能够通过页面对mysql数据库进行增删查改等操作。
项目的实现技术:
1、进行创建监听套接字
和其他socket编程的模式是一样的,我们这里的第一步依然是首先创建监听套接字,创建的过程依然是,
socket–>bind—>listen。
这里在bind的时候采用sockaddr_in方式,另外为了实现端口的复用,我们采用了setsockopt的SU_REUSEADDR方式。
listen的时候,我们这里经常需要关注backlog,这个是已完成队列和未完成队列和的大小。
2、进行accpet多线程的建立
进行accept接收客户端的connect请求。这个过程的实质是对backlog队列的一个操作,在accept前,内核接受到connect请求首先把socket放入未完成队列,然后accept的时候,需要把socket放入已完成队列当中去,然后accept成功以后从已完成队列当中取出。这就是accept的整个过程。
accept成功以后,我们使用pthread_create来进行创建线程,把socket托付给线程来进行操作。在线程处理的过程中,有一个问题就是线程等待,我们为了解决这个问题,我们使用线程分离,这样使得线程可以作为孤儿进程的形式托管给1号进程,当执行完毕以后,由1号进程来进行资源的回收。
3、线程处理
在整个线程处理函数内部,我们对HTTP的请求进行分析,通过对其中的路径参数等信息,我们调用根目录下的index.html进行处理。
3.1 进行获取HTTP信息
线程处理中,少不了的就是对HTTP报文信息的处理,从这些中提取出来有用的信息,我们采取的读取方式是按行读取。
例如对HTTP 方法的第一行进行读取,然后这一行的三个字段又是按照空格隔开,我们利用这个特性,把HTTP请求的方法(GET或POST,目前只进行处理这两个),然后可以得到资源路径(url),最后一个字段是HTTP 的版本信息。
得到这一部分资源以后,我们现在就需要进行处理,接下来就是考虑参数。HTTP请求经常会带一些参数,通过这些参数浏览器请求资源,GET方法的资源是在url当中,POST方法的资源是在消息正文当中,这样我们也就能得到资源了。
参数的形式一般为:
?xx=400&bb=22## 采用?开始,采用&连接
所以这里需要说的是,其实我们的多线程HTTP服务器总共会有三种情况:
非cgi : 此时不带参数,我们需要返回主页给浏览器
GET cgi :此时参数在url后,并且按照一定的规律连接,我们需要把参数提出来交给cgi程序去进行运算
POST cgi :此时参数在消息正文当中,我们需要结合消息报头,提取参数
3.2 非cgi模式
我们这里先来看下非cgi方式,这种情况下,首先明白此时我们可以得到资源路径,这个资源路径其实就是根目录下的路径,默认我们去寻找根目录下的主页。
所以我们需要给资源加上index.html
然后我们把整个index.html的信息发送给scoket。
我们在这里采用的方式是sendfile的操作,sendfile主要是实现零拷贝发送文件,实现一个高效的数据传输,并且对其进行验证。这样socket接受到主页信息,就可以显示出来网页了,当然这个过程我们也是按照HTTPPOST响应发送过去的。
3.3 cgi模式
cgi模式是我们操作的一种方式,这种方式下我们处理带参数的HTTP请求,我们把这些参数都取出来,然后把这些参数使得cgi程序能够获取到,获取到以后就可以进行计算或者数据处理等。
这里我们的处理方式就是对这两组管道需要进行一下重定向,对于fork以后的子进程,我们把管道重定向,利用dup2系统调用,然后达到的效果就是子进程最终可以从stdin中得到父进程给的信息,而父进程也就是服务器又可以从socket得到HTTP请求的内容。然后子进程数据计算以后把数据写到stdout中,server从管道中取回数据,发送给socket。这样socket端也就是浏览器那边就可以显示出最后的结果。
在这个里面重要的还有一个点就是HTTP的参数如何传递到cgi程序当中。下面我们分开来说一下。这里我们使用的是环境变量的方式,cgi程序在子进程当中运行,可以获取到环境变量,所以就可以得到所需要的参数,具体看下面的介绍。
3.4 GET cgi模式
get方法的时候,这个时候cgi所需要的参数是放在url当中的,所以这个时候我们就去在HTTP GET请求行的第二个内容资源路径当中进行字符串的处理,我们找‘?’,当找到以后,我们让一个指针指向这里,叫做query_string,我们把这个作为一个环境变量传递给子进程就可以了。对于GET的cgi模式,最重要的也就是method(方法)和query_string(包含参数)
3.5 POST cgi模式
但是当我们使用POST方法的cgi模式的时候,这个时候就会有另外的问题,我们的参数在正文当中,所以我们需要在正文当中寻找,另外需要知道正文中的字节数。这个时候POST的消息报头就起作用了,它在其中阻止了name:value形式的content_length:xx这样的内容,然后获取到这个长度以后,我们就可以知道向socket读取多少长度的内容了,然后读取完以后我们就可以获得到参数,同样是按照“?”和“&”的形式进行组织的,我们取出这个内容,然后进行数据运算操作。
3.6 父进程后续操作
我们需要说一下父进程的后续操作,父进程进行处理的时候首先需要重定向管道,这样才好进行后续的操作,然后我们进行查看方法,如果是POST方法,我们需要把HTTP的请求正文全部获取到放入和cgi程序打交道的管道当中。这样才能让cgi获取到正文信息,其他情况下,我们都需要从cgi返回到管道的结果当中进行获取返回的信息,把这个信息发送给socket。最后,当然别忘了使用waitpid等待子进程。
4 、 cgi的编写方式
cgi的编写方式我们可以叫做cgi网关协议,我们所有的cgi程序都可以套用这一套来进行操作,我们采用的传递参数方式是环境变量,其实还可以用管道进行传递,传递进管道,cgi程序从管道当中读取出来。
然后我们进行字符串处理。
因为参数的组织形式是”?data1=100&data2=200”这种形式的,所以我们要找的关键符号就是“=”和“&”,这样我们就可以取到参数,然后把参数进行运算,得到结果输出到标准输出就好了。
5、cgi操作
cgi操作形式其实是可以多种多样的,可以提供math计算操作,也可以提供关于对数据库mysql的操作,另外,我们也可以去操作我们使用python爬虫爬下来的数据来进行操作,后续比如也可以在html上面跑一个日期类,显示下天气等信息。
整体架构:
1 、关于HTTP协议
即超文本传输协议,是互联网上应用最广泛的网络协议。它是应用层的协议,底层是基于TCP通信的。HTTP协议的工作过程:客户通过浏览器向服务器发送文档请求,浏览器将请求的资源回应给浏览器,然后关闭连接。即:连接->请求->响应->关闭连接。
2、关于URL
即统一资源定位符,每个网页都对应一个URL地址(俗称网址),具有全球唯一性。它包含的信息指出文件的位置以及浏览器应该怎么处理它。 一个完整的URL包括协议类型、主机类型、路径和文件名。
http协议的URL格式: http: //host[:port][abs_path] ,http表示使用http协议来进行资源定位;host是主机域名;port是端口号,一般有默认的;abs_path代表资源的路径。
这里我主要介绍项目中涉及的URL的两种格式—URL带参数和不带参数的。
不带参数的URL仅仅代表一份资源的路径;
带参的URL中?作为资源路径与参数的分隔符,?之前是资源路径,?之后是参数。
3、关于HTTP的请求与响应格式
响应报头中的状态码和状态码描述,例如:当请求的资源不存在时,会收到“404 NotFound”的页面,404就是状态码,“NotFound”就是状态码描述,即请求的文件不存在。
项目的实现思路:
1、http协议是基于TCP通信的协议,因此,实现web服务器的第一步至少要能实现两个主机不同进程之间的TCP通信。
2、接下来的部分就是比较主要的处理逻辑了,当服务器收到请求后,首先应该分析请求方法(因为web服务器是要支持cgi的,但请求方法不同处理cgi也不同,这里我们只处理GET和POST方法)。
3、当方法确定后,应该拿到请求的URL,这一步是为了我们后边能处理GET和POST方法的cgi(GET和POST的参数位置不同,GET的参数在URL中,POST的参数在请求正文中)
4、判断资源是否存在,如果存在,判断这个资源是一个目录、普通文件还是一个可执行程序。之前几步我们已经提取到URL以及参数。GET方法:如果没有参数,就直接将请求的资源返回(即进入非cgi模式运行);否则,进入cgi模式内部运行;只要是POST方法就需要支持cgi:直接进入cgi函数内部运行。
非cgi模式:
进入非cgi模式时一定是GET方法且没有参数,此时进入echo_www()函数内部即可,该函数会将所请求的资源以html的格式返回给浏览器。
cgi模式:
运行cgi时的过程,首先服务器要从浏览器上读取参数,然后需要fork出一个子进程进行cgi部分的处理,父进程通过环境变量的方式将参数转交给子进程,子进程运行完成后,将结果交给父进程,父进程再将数据输出给浏览器。在这个过程中可以将父进程看作一个所谓的中间量,只进行了参数的转交,因此可以将子进程的输入输出文件描述符进行重定向,即子进程直接与浏览器“联系”。
父进程工作:
创建两个管道,并关闭相应的文件描述符;
Post方法:继续读取数据,直到读完Post的参数部分;
Get方法:直接从子进程读取结果;
将数据与方法全部交给子进程后等待子进程结果。
子进程工作:
关闭管道适当的文件描述符;
对标准输入输出进行重定向;
通过环境变量传递参数;
进行exec程序替换;
121 int exe_cgi(int sock,char* method,char* path,char* query_string)
122 {
123 char line[MAX];
124 char method_env[MAX/10];
125 char query_string_env[MAX];
126 char content_length_env[MAX/10];
127 int content_length=-1;
128 if(strcasecmp(method,"GET")==0)
129 {
130 clear_header(sock);
131 }
132 else
133 {//POST
134 do{
135 get_line(sock,line,sizeof(line));
136 if(strncmp(line,"Content-length: ",16)==0)
137 {
138 content_length=atoi(line+16);
139 }
140 }while(strcmp(line,"\n"));
141 if(content_length==-1)
142 {
143 return 400;
144 }
145 }
146
147 int input[2];//child read,parent write
148 int output[2]; //child write,parent read
149
150 pipe(input);
151 pipe(output);
152 //path
153 pid_t id=fork();
154 if(id<0)
155 {
156 return 503;
157 }
158 else if(id==0)
159 {//child
160 close(sock);
161 close(input[1]);
162 close(output[0]);
163
164 sprintf(method_env,"METHOD=%s",method);
165 putenv(method_env);
166 if(strcasecmp(method,"GET")==0)
167 {
168 sprintf(query_string_env,"QUERY_STRING=%s",query_string);
169 putenv(query_string_env);
170 }
171 else
172 {
173 sprintf(content_length_env,"CONTENT_LENGTH_ENV=%d",content_length);
174 putenv(content_length_env);
175 }
176
177 dup2(input[0],0);
178 dup2(output[1],1);
179
180 execl(path,path,NULL);
181 exit(1);
182 }
183 else
184 {
185 close(input[0]);
186 close(output[1]);
187
188 char c;
189 if(strcasecmp(method,"POST")==0)
190 {
191 int i=0;
192 for(;i<content_length;i++)
193 {
194 recv(sock,&c,1,0);
195 write(input[1],&c,1);
196 }
197
198 }
199 sprintf(line,"HTTP/1.0 200 OK\r\n");
200 send(sock,line,strlen(line),0);//strlen而非sizeof,因为只取一行
201 sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");
202 send(sock,line,strlen(line),0);//strlen而非sizeof,因为只取一行
203 sprintf(line,"\r\n");
204 send(sock,line,strlen(line),0);//strlen而非sizeof,因为只取一行
205
206 while(read(output[0],&c,1)>0)
207 {
208 send(sock,&c,1,0);
209 }
210 waitpid(id,NULL,0);
211 }
212 return 200;
213 }
遇到的问题:
1、本地环回测试ok,Linux下的浏览器测试也可以,但不能接外部的浏览器访问(没有设置桥接模式)嗯~要是在外部浏览器测试的话千万别忘记关闭防火墙
2、服务器应答时,没有将html格式的页面发送,而是将底层的实现代码展示在浏览器,并且在调试时将本来要打印的调试信息会打印到网页上(在回应空行时将send期望发送的数值写的太大,本来只需要发送两个字节的内容)
解决:先检查代码,思路正确,在容易出现问题的地方加入调试信息,最后将问题定位在echo_www()函数内
3、不能显示图片(这个问题是没有将所有发送的情况考虑完全,只考虑到目录、可执行程序,但没有考虑到如果请求的是一个路径明确的普通文件)
解决:测试请求一个路径明确的test.html文件,加入调试信息 ,将问题定位在:如果请求的资源存在,应该如何处理。对于普通文件,找到后并回显给浏览器;如果是目录,应答的是默认页面;如果是可执行程序,执行后返回结果
4、能显示图片后,但显示的不完整(原因:echo_www中,期望读取一行信息的line值太小,不能存下一张图片)
5、运行cgi模式时,每次提交数据并进行submit后都会自动出现提醒下载的页面
原因:在响应报头中,将Content-Type中的”text”写成”test”。而浏览器对于不能识别或解析的实体,都会提醒用户下载。