zoukankan      html  css  js  c++  java
  • 写一个简单版本的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。

  • 相关阅读:
    call()与apply()的作用与区别
    Tomcat8/9的catalina.out中文乱码问题解决
    怎样查看Jenkins的版本
    每日日报2020.8.18
    528. Random Pick with Weight
    875. Koko Eating Bananas
    721. Accounts Merge
    515. Find Largest Value in Each Tree Row
    286. Walls and Gates (Solution 1)
    408. Valid Word Abbreviation
  • 原文地址:https://www.cnblogs.com/lxy-xf/p/11087170.html
Copyright © 2011-2022 走看看