写一个简单版本的WebServer-Ver1.0-单进程

rio:

rio是一个结构体,rio结构体的作用是提供一个自己设置的IO缓冲区,从sockfd表示的文件中的接收缓冲区中读取数据到rio中,rio相当于一个中转站,获得对端写到sockfd中的数据时,首先将数据读取到rio维护的缓冲区中,然后再从rio维护的缓冲区读取到用户空间指定的区域中。

这个过程中有三个存放数据的区域:

1.sockfd维护的缓冲区:就是read读取数据的来源。这个接收缓冲区接收从对端发送的数据。

2.rio结构体维护的缓冲区:相当于一个中转站,sockfd中的数据首先转移到这里来,再从这转移到用户指定的区域中。

3.用户指定的区域:用户用于处理数据的区域,由自己指定。

rio维护四个成员变量:

1.int rio_fd.这个变量指向要读取的文件的文件描述符,通过这个将一个rio和某个文件绑定在一起。

2.int rio_cnt.这个变量表示rio中尚未转移到用户指定区域的字节数。因为rio从rio_fd指定的文件中读取数据到rio中,rio中存放的数据都是需要转移到用户指定区域的,用rio_cnt表示存在在rio中但尚未转移到用户指定区域的字节数。

3.char* rio_bufptr.这个指针用于指向rio缓冲区中未转移到用户指定区域的数据的起始处。因为rio中的缓冲区是自己设置的,不像sockfd会自己维护一个属于本文件的指针(read每次调用都会调整sockfd中的该指针),因此这个缓冲区需要自己维护,每次读取n个字节后该指针都要后移,保持在未读区域的第一个字节。

4.char rio_buf[MAXLINE].rio维护的缓冲区,用于存放从rio_fd绑定的文件中读取的数据。

rio_read:

read的包裹函数,简易的封装了下read函数。

为什么封装read为rio_read:

封装后,每次rio维护的缓冲区没有数据时,都会一下读入MAXLINE个数据到rio_buf中。然后如果要读取n个字节到用户指定区域,则采用memcpy直接在rio_buf与用户空间间进行拷贝。实际上调用read只是在每次rio_buf无数据,也就是未读数据rio_cnt为0时。

如果不封装,则每次读n个字节都要调用一次read,这样要频繁在用户态和内核态间切换。

read_requestthdrs:

这个函数用于处理头部信息,但因为很简陋,对头部信息没有什么处理的地方。

注意,每次Rio_readlineb(rp, buf, MAXLINE);实际上是使用memcpy拷贝的,而memcpy是直接覆盖掉,比如buf之前是"i'm a sb",而Rio_readlineb调用之后,直接从buf头开始覆盖,即使这次覆盖的字符比较少,如"aa",则拷成了"aa a sb"。

这样只要最后一个请求头" "出现,即使buf拷贝后为" a sb",strcmp也会判定与" "相等,因为到实际上就比对结束了。

2019.7.13:http的包内是不含有的,因此如果要以strcmp的方式来与' '作对比,在每次rio_readlineb后,必须在 后面补一个,这样读到 后会补上,才能变为“ a sb”.

注意下面图的&bufp=0就是在 后补。

整个流程实际上是:

首先处理请求行:请求行有method uri version三个信息。method为qingqiu方法,这个版本只能处理GET方法。uri为要请求的资源。version为请求的方法的版本。

然后处理请求头部:包含了客户端的一些信息,如支持的编码,是否保持连接,期望的语言类型。。。

为请求的结尾。读取到 说明这一段的请求解析到此。计网Page271:CRLF作为报文的请求行或head的结尾.CR表示回车,LF表示换行,即 .

parse_uri:

解析URL:
目前只考虑静态文档,因此cgiargs(CGI args)是不必要的(是这样的吗???),因此stcpy给cgiargs一个"",即一个。

然后将工作目录名传递给文件名filename,因为URI中传递的是相对URL(省略了主机的域名,见计网Page272),因此需要首先加入工作目录名才能定位到请求的文件。

然后将uri中的相对URL加入到filename中,这样就能定位到具体的文件了。

下面的这个if判断的作用是对于URL:http://www.hao123.com/

www.hao123.com为域名,真正传递的相对URL只有"/",除了这种情况,其他任何情况下uri[strlen[uri]-1]都不可能是'/'。这种情况应该跳转到主页,所以在filename后面加入的是home_page。

动态文档先跳过。

serve_static:

用于处理静态页面的函数。

其中调用了get_filetype。get_filetype写来用于获得发送过去的文件的文件类型,通过get_filetype获取得到返回文件的文件类型,将它写入filetype中,filetype是用于响应报文的首部行中。

serve_static实际上是构造回复的头部,打开文件与发送文件。

sprintf(buf, "HTTP/1.0 200 OK ");用于在buf中写入状态行。

//--------------------------------------

get_filetype(filename, filetype);

sprintf(buf, "%sServer: Tiny Web Server ", buf);//这里的用法:Server:....的内容相当于附加到原来的buf上,很巧妙。

sprintf(buf, "%sContent-length: %d ", buf, filesize);

sprintf(buf, "%sContent-type: %s ", buf, filetype);

这四行用于构造响应报文的首部行,并将首部行写入buf中。

//--------------------------------------

Rio_writen(fd, buf, strlen(buf));

将构造的状态行和首部行写入文件fd

//--------------------------------------

srcfd = Open(filename, O_RDONLY, 0);

打开要发送过去的文件,srcfd绑定在该文件上。

srcp = (char *)Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);

用mmap的方式在srcfd与fd间进行数据的传输。

mmap的用法:

APUE Page 422

Mmap为mmap的简单封装。

mmap原型:void* mmap(void* addr,size_t len,int prot,int flag,int fd,off_t off);

返回值void*表示返回一个指针,该指针指向从fd所映射到的内存区域的起始地址。

addr一般设置为0,这个参数用于选择映射区域的起始地址,设为0由系统选取。

len为要映射文件内容的大小。

prot为映射区域的保护性要求:

PROT_READ 映射区可读

PROT_WRITE 映射区可写

PROT_EXEC 映射区可执行

PROT_NONE 映射区不可访问

flag为映射区属性:设置为MAP_SHARED代表修改映射区也会修改fd指向的文件;设置为MAP_PRIVATE则映射区为一个副本,不用担心对文件造成影响。

off为要映射的内容在文件的起始偏移(如果从开开始则设为0就可以)。

如果从srcfd中读取内容,在写入dstfd中的话,首先要从srcfd中read到内存,然后再从内存write到dstfd。

而引入mmap后,不需要read和write,srcfd与dstfd直接通过内存相互联系。

//----------------------------------------

Rio_writen(fd, srcp, filesize);

将映射到内存srcp处的文件内容写入fd.

//----------------------------------------

Munmap(srcp, filesize);

Munmap为munmap的简单封装。

munmap与mmap成套使用,map<-------------->unmap.

munmap用于解除srcp处的映射,解除后再访问则会发生段错误。

serve_dynamic:

处理动态页面的函数,因为目前只处理静态页面,因此在函数中,在构造完响应报文的状态行和首部行后,直接return而不处理要发送过去的数据内容。

另外这里面一个地方:

Rio_writen(fd,(void*)" ",2);

这里Rio_writen将第二个参数指向的地址中,写入第三个参数指定的字节数到第一个参数指定的文件中,即从写入 到fd绑定改的文件中。" "实际上是位于常量区的,用void*取得它的地址,然后由第三个参数知道要写2字节到fd中。

另外c++中,cout对<<进行了重载,在很多情况下,如char* p="i'm a sb",cout<<p因为对char*进行了重载的缘故而输出的是char*指向区域的内容,如果要看p指向区域的地址,则强制转换成void*就好了,如cout<<(void*)p。 

return之后:

其实这部分内容无意义。

首先Fork()创建子进程。

首先是子进程:

子进程用setenv设置环境变量"QUERY_STRING"为CGI参数cgiargs。

然后用Dup2将fd(这个fd为套接字的文件描述符)复制到标准输出栏。这样的目的,因为原本子进程是用于execve一个新程序,这样将标准输出替换为fd后,则新程序中,原本要输出到标准输出的数据,现在全部往fd,也就是套接字中输出了。

然后子进程用Execve开始执行新程序。

关于Execve:

原型为:int execve(const char *filename, char *const argv[ ], char *const envp[ ]);

filename为新程序文件所处的位置,根据filename来定位并执行它。

因为execve相当于替换原子进程,即从新执行一个程序,从int main开始执行,第二个参数argv[]就是相当于这个新程序的main所需要的参数,这里argv是个数组,数组内存放着char* const型的常指针(不可更改指向)。

第三个参数为传递给新程序的环境变量数组。

对于父进程:

等待子进程终止。wait()的参数int* status指针用于存放子进程的终止状态,如果不关心则设置为NULL。

原文地址:https://www.cnblogs.com/lxy-xf/p/11087170.html