myhttp

     HTTP 是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。 

实现一个服务器程序,支持HTTP协议的服务器,浏览器进行访问

 

从请求角度上,支持GET和POST方法;从相应角度上,支持静态页面也支持动态页面

   静态页面:GET方法且query_string为空。

                    根据url_path获取到文件的真实路径,打开文件,根据文件内容构造HTTP响应报文。首行+header+空行+文件内容

                    此处使用sendfile,更高效的把文件中的内容传回给客户端。

   动态页面:基于CGI,创建一对匿名管道,fork出子进程。

                     父进程流程:

                                         a)如果方法是POST,就把body中的数据写到管道中

                                          b) 父进程构造HTTP响应报文。

                                          c) 父进程尝试从管道中读取子进程的计算结果作为HTTP相应中的body

                                          d)父进程进行进程等待。waitpid。。。现在有多个进程,每个进程又创建了子进程,waitpid明确等待回收自己创建的进程

                       (1)GET方法且query_string为空

                         (2)POST方法。

                  子进程流程:a)设置环境变量。REQUENCE_METHOD 、QUERY_STRING 、CONTENT_LENGTH

                                        b)重定向。标准输入输出重定向到管道上

                                        c)根据url_path获取到CGI程序真实路径

                                        d)进程程序替换

                                        e)错误处理

 

          CGI:获取到请求的参数,对参数进行处理

                         a)从环境变量之中获取到方法。如果是GET方法,从环境变量中获取到query_string。如果是POST方法,需要从环境变量中获取到content_length,再从标准输入中读入body数据

                          b)从参数中解析出核心数据

                          c)根据输入参数进行计算,和具体的业务相关

                          d)根据计算结果生成动态页面

 

HTTP协议的主要特点可概括如下:

1.支持客户/服务器模式。

2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。

3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。

4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

 

 工作流程

     (1) 服务器启动,在指定端口或随机选取端口绑定 httpd 服务。

     (2)收到一个 HTTP 请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。

     (3)取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。

     (4) 格式化 url 到 path 数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页。

     (5)如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到(10)。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。

    (6)读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200  状态码写到套接字。

    (7) 建立两个管道,cgi_input 和 cgi_output, 并 fork 一个进程。

    (8) 在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序。

    (9) 在父进程中,关闭 cgi_input 的读取端 和 cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。

(10) 关闭与浏览器的连接,完成了一次 HTTP 请求与回应,因为 HTTP 是无连接的。

 关键函数的作用:

ReadLine:按行读取数据, 把回车换行等情况都统一为换行符结束 

如果当前字符是\r,读取下一个字符,如果下一个字符是\n,即\r\n的组合,就将它们变为\n

                                                           如果下一个字符不是\n,即只有一个\r,那么把它变成\n

如果当前字符是\n,退出函数

如果以上都不是,那就把该字符放入buf中

 int ReadLine(int sock,char buf[],size_t max_size){
   char c='\0';
    ssize_t i=0;
    while(i < max_size){
      ssize_t read_size = recv(sock,&c,1,0);
      if(read_size <= 0){
        return -1;
      }
  
     if(c=='\r'){
        recv(sock,&c,1,MSG_PEEK);
        if(c == '\n'){
          recv(sock,&c,1,0);
        }else{
          c = '\n';
        }
      }
     buf[i++] = c;
     if(c=='\n'){
        break;
      }
    }
   buf[i] = '\0';
   return i;
  }

 HandlerCGI:

创建一对匿名管道,

 int HandlerCGI(int new_sock,const HttpRequest* req)
 {
   int err_code = 200;
   int fd1[2],fd2[2];
   pipe(fd1);
   pipe(fd2);
   int father_read = fd1[0];
   int child_write = fd1[1];
   int father_write = fd2[1];
   int child_read = fd2[0];
 
   pid_t ret = fork();
   if(ret > 0){   /father
     close(child_read);
     close(child_write);
     HandlerCGIFather(new_sock,ret,father_read,father_write,req);
   }else if(ret ==0){   //child
     close(father_read);
     close(father_write);
     HandlerCGIChild(child_read,child_write,req);
   }else{
     perror("fork");
     err_code = 404;
     goto END;
   }
 END:
   if(err_code != 200){
     printf("Handler 404\n");
     Handler404(new_sock);
   }
 }

     HandlerCGIFather:

a)对于post方法,把body中的数据写到管道中

     从socket中读出数据,写到管道中,此处无法用sendfile,因为sendfile只能把数据写到socket之中,直接一个字节一个字节的从socket中读出来,再写到管道中去

b)父进程需要构建一个完整的HTTP协议数据。

     对于HTTP协议要求我们按照指定的格式返回数据。CGI程序返回的结果只是body部分。 header,首行等部分需要父进程自己构造

c)从管道中尝试读数据,写回到socket中。判断语句中为0表示读到了 文件结束标志EOF。子进程结束(子进程停止write),父进程就会读到文件尾

     father_read对应的是child_write,对于父进程来说,child_write已经关闭了;对于子进程来说,如果CGI程序处理完进程也就退出了。

     进程退出的同时会关闭child_write,此时就意味着管道的所有写端都关闭,再尝试读,read返回0

d)进程等待,否则会造成僵尸进程

    不能是wait,因为服务器会给每个客户端都创建一个线程,每个线程又有可能创建子进程,此时如果用wait,任何一个子进程结束都可能导致 wait返回,这样的话子进程就不是对应的线程来回收

void HandlerCGIFather(int new_sock,int child_pid,int father_read,\
                       int father_write,const HttpRequest* req)
 {  
    // a
    char c = '\0';
    if(strcasecmp(req->method,"POST")==0){
      //read data from socket,write in pipe
      //can't use sendfile,because sendfile only write data to socket
      //one byte,one byte read from socket and write to pipe
     
     ssize_t i=0;
     for(;i<req->content_length;++i){
       read(new_sock,&c,1);
       write(father_write,&c,1);
     }
   }
   
  //b
  const char* first_line = "HTTP/1.1 200 OK\n" ;
  const char* blank_line = "\n";
  send(new_sock,first_line,strlen(first_line),0);
  send(new_sock,blank_line,strlen(blank_line),0);
 
  //c
  while(read(father_read,&c,1)>0){
     write(new_sock,&c,1);
   }
     //d
     waitpid(child_pid,NULL,0);
 }

 

HandlerCGIChild:

a)exec替换后,子进程里的内容都没有了,创建环境变量。保存CGI想要传递下去的信息。 

      REQUEST_METHODD  ,   QUERY_STRING  ,   CONTENT_LENGTH

      字符串拼接结果相当于是REQUEST_METHOD = GET

b)把子进程的标准输入和标准输出重定向到管道。目的是读取父进程交给的数据,再写回到父进程的管道中

c)进程的程序替换,通过url_path拼装成一个完整的路径,把对应的文件进行替换   

     之所以采用execl,原因一:此处CGI程序不需要指定命令行参数,l/v不影响

                                  原因二:此处CGI程序对应的全路径中通过putenv的方式 进行设置 的

                                原因三:此处CGI程序环境变量在子进程中通过putenv的方式进行设置的,如果使用execle的话,就不必使用putenv了,此处的两种方式均可

d)exec执行失败,需要进行错误处理。

      如果此处不进行退出,就会出现子进程和父进程监听相同的端口号情况,而此时我们只是希望子进程去调用 CGI程序处理客户端链接这样的事情只应该由父进程来完成

      execl之后就把 动态页面生成的逻辑交给了CGI程序

void HandlerCGIChild(int child_read,int child_write,const HttpRequest* req)
 {
   //a
   char method_env[SIZE] = {0};
   sprintf(method_env,"REQUEST_METHOD=%s",req->method);
   putenv(method_env);
   if(strcasecmp(req->method,"GET") == 0){
     //set QUERY_STRING
     char query_string_env[SIZE] = {0};
     sprintf(query_string_env,"QUERY_STRING=%s",req->query_string);
     putenv(query_string_env);
   }else{
     //set CONTENT_LENGTH
     char content_length_env[SIZE] = {0};
     sprintf(content_length_env,"CONTENT_LENGTH=%d",req->content_length);
     putenv(content_length_env);
   }
   //b
   dup2(child_read,0);
   dup2(child_write,1);
 
   //c
   char file_path[SIZE] = {0};
   HanderFilePath(req->url_path,file_path);
   execl(file_path,file_path,NULL);
   //d
   exit(0);
 } 

 

 

HandlerRequest:

1.读取并分析请求

    a)从socket中取出HTTP,读取首行

    b)分析首行,获取method和url

    c)读取并分析头部,只关注content_length

 2.通过请求判断是动态还是静态页面

   a)GET方法 且 没有 query_string--------静态页面

   b)GET方法 且 有 query_string--------动态页面

   c)POST---------动态页面

 void HandlerRequest(int new_sock)
 {
  int err_code = 200;
  HttpRequest req;
  memset(&req,0,sizeof(req));
 
 if(ReadLine(new_sock,req.first_line,sizeof(req.first_line))<0){
    printf("ReadLine first_line failed\n");
    err_code = 404;
    goto END;
  }
 printf("first_line=%s\n",req.first_line);
 
 if(ParseFirstLine(req.first_line,&req.method,&req.url)<0){
    printf("ParseFirstLine failed!first line=%s\n",req.first_line);
    err_code = 404;
    goto END;
  }
 
 if(ParseQueryString(req.url,&req.url_path,&req.query_string)<0){
    printf("ParseQueryString failed!url=%s\n",req.url);
    err_code = 404;
    goto END;
  }
 
 if(HandlerHeader(new_sock,&req.content_length)<0){
    printf("HandlerHeader failed!");
    err_code = 404;
    goto END;
  }
 
 if(strcmp(req.method,"GET")==0 && req.query_string==NULL){
     HandlerStaticFile(new_sock,&req);
  }else if(strcmp(req.method,"GET")==0 && req.query_string != NULL){
     HandlerCGI(new_sock,&req);
  }else if(strcmp(req.method,"POST")==0){
     HandlerCGI(new_sock,&req);
  }else{
     printf("method not supposed!method=POST or GET\n");
     err_code = 404;
     goto  END;
  }
 END:
   if(err_code != 200){
     printf("Handler 404\n");
     Handler404(new_sock);
   }
  
  close(new_sock);
 }

 

HttpServerStart:

1.socket

2.bind
3.listen
4. 进入循环接收客户端请求
5. 创建线程处理更多连接

 void HttpServerStart(const char* ip,short port)
 {
  int listen_socket = socket(AF_INET,SOCK_STREAM,0);
  if(listen_socket < 0){
    perror("socket");
    return;
   }
  
 int opt=1;
 setsockopt(listen_socket,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

 sockaddr_in addr;
 addr.sin_family = AF_INET;
 addr.sin_addr.s_addr = inet_addr(ip);
 addr.sin_port = htons(port);
 int ret = bind(listen_socket,(sockaddr*)&addr,sizeof(addr));

 if(ret < 0){
     perror("bind");
     return;
   }
 
 ret = listen(listen_socket,5);
 
 if(ret < 0){
     perror("listen");
     return;
   }
 printf("HttpServerStart OK\n");
 
 while(1){
     sockaddr_in peer;
     socklen_t len = sizeof(peer);
     int new_sock = accept(listen_socket,(sockaddr*)&peer,&len);
     if(new_sock < 0){
       perror("accept");
       continue;
     }
    pthread_t tid;
    pthread_create(&tid,NULL,ThreadEntry,(void*)new_sock);
    pthread_detach(tid);
   }
 }

服务器 性能:

         http_load测QPS

         进程池-----FastCGI

         计算业务-------多线程进行计算

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值