小型web服务器

                                   小型web服务器

一、项目平台: centos 6.5

二、实现功能 :网站的后台程序

三、基本要求:

1.基于HTTP/1.0版本的web服务器,客户端可以通过GET、POST方法进行资源请求

2.服务器将客户请求的资源以html页面的形式呈现,并且能够进行差错处理。

3.服务器能运行简单的cgi

四、项目的背景知识

1.了解HTTP协议

    是超文本传输,是应用层的协议,他是基于TCP协议的。他的工作过程:客户端通过浏览器向服务器发送请求,浏览器将请求的资源在传给浏览器,在关闭连接。

2.了解url

  是统一资源定位符,也是我们俗称的网址。

http://www.example.jp:80/dir/index.html?uid=1

如上一个url包括协议方案名,服务器的地址和端口号,请求资源的路径,查询字符串。

在这里的查询字符串是根据请求的方法来确定有还是没有。若为GET方法就会有查询字符串。若为POST方法就没有,他是通过http请求报文中的body发送的。

3.http请求及响应的格式

 五、项目的基本思路

  1.通过socket来建立通信

     a) 创建socket

     b)  绑定地址端口

     c) 监听

     d) 进入事件循环

  2.服务器接收浏览器的请求并且进行解析

        a)解析请求的首行,获取方法和url(在这里只考虑GET、POST的请求方法)

        b)再去解析出url获取path和query_string

       c)  读取并解析header(这里只获取到content-length)其余信息丢弃

       d)body暂时不进行解析,根据后面的情况再去判断是否需要解析

  3.根据解析好的HTTP请求来进行计算

     a ) 静态页面:服务器上的某个固定位置的html文件,文件内容若没有人去修改,就会一直不变,浏览器的页面就一样。

            也就是进入到一个非cgi模式

           (1)拼接目录

           /index.html---->不是一个绝对路径,只是和服务器上某个路径匹配的

           是一个相对于HTTP的根目录--->允许对外访问的文件集中到某个目录下面就是HTTP的根目录

            若url_path是一个文件,直接进行拼接

           若url_path是一个目录,就会默认构造一个文件路径,尝试取目录下的index.html文件

           (2)打开文件

               读取文件中的内容,根据内容构造http响应,文件中的内容就是响应中body的部分

     b)动态页面:服务器会根据用户输入参数来决定生成什么样的页面。

            进入到一个cgi模式

      (1)HTTP服务器需要创建子进程

      (2)子进程进行程序替换,替换成磁盘上某个可执行程序

父进程执行父进程的相关逻辑:

    1.将body写入管道

    2.父进程尝试读取子进程构造的结果

    3.父进程构造HTTP响应,写回客户端

 子进程传递给父进程的信息

     1.设置环境变量(方法、query_string、content_length)

     2.重定向

     3.根据url_path构造路径

     4.进行替换

六、测试

  用一个简单版的计算器来测试CGI

        1.基于CGI协议获取到需要的参数

        2.根据业务逻辑(计算器相关的逻辑),进行计算

        3.把结果构造成HTML写回到标准输出中

代码实现:

1.创建socket连接,采用多线程来进行处理清楚

#define SIZE (1024*10)
 17 typedef struct HttpRequest
 18 {
 19     char first_line[SIZE];
 20     char *method;
 21     char *url;
 22     char *url_path;
 23     char *query_string;
 24     int content_length;
 25 }HttpRequest;
 
//线程的入口函数
 void* ThreadEntry(void* arg)
 {
     int32_t new_sock=(int32_t)arg;
     HandlerRequest(new_sock);
     return NULL;
 }
 typedef struct sockaddr_in sockaddr_in;
 typedef struct sockaddr sockaddr;
 //服务器的入口函数
 void HttpServerStart(const char* ip,short port)
 {
     //0.忽略信号
     signal(SIGCHLD,SIG_IGN);
     //1.创建socket
    int listen_sock=socket(AF_INET,SOCK_STREAM,0);
     //失败原因:文件描述符达到上限
    if(listen_sock<0)
    {
         perror("socket");
        return;
     }
     //2.绑定地址端口
     sockaddr_in addr;
     addr.sin_family=AF_INET;
     addr.sin_port=htons(port);
     addr.sin_addr.s_addr=inet_addr(ip);
     int ret=bind(listen_sock,(sockaddr*)&addr,sizeof(addr));
     if(ret<0)
         //失败的原因:该端口可能被别的进程绑定
     {
         perror("bind");
         return;
     }
     //3.监听
    ret=listen(listen_sock,5);
     if(ret<0)
     {
         perror("listen");
         return;
     }
 printf("HttpServerStart ok!\n");
     //4.进入事件循环
     while(1)
     {
         //printf("进入事件循环\n");
         sockaddr_in peer;
         socklen_t len = sizeof(peer);
        int32_t new_sock= accept(listen_sock,(sockaddr*)&peer,&len);
         if(new_sock<0)
        {
            perror("accept");
             continue;
         }
         printf("accept\n");
         //6.创建线程,由新线程完成具体的HTTP服务器后续的操作
         //线程创建块,且占用资源少,切换块
        //1.不可以传递指针,是因为此时可能new_sock生命周期结束,才去调用ThreadEntry,变成野指针
        //2.那么试着加上static,也是不可以的,因为用static修饰的只有一份。
  //3.那么每产生一个new_fd都申请一块空间,在函数调用完成之后再去释放,这样可以是可以,但是不好。
         int32_t new_sock= accept(listen_sock,(sockaddr*)&peer,&len);
        if(new_sock<0)
         {
            perror("accept");
             continue;
         }
         printf("accept\n");
         //6.创建线程,由新线程完成具体的HTTP服务器后续的操作
         //线程创建块,且占用资源少,切换块
       //1.不可以传递指针,是因为此时可能new_sock生命周期结束,才去调用ThreadEntry,变成野指针
        //2.那么试着加上static,也是不可以的,因为用static修饰的只有一份。
         //3.那么每产生一个new_fd都申请一块空间,在函数调用完成之后再去释放,这样可以是可以,但是不好。
         //4.因此采用传值的方式,进入函数,就会进行拷贝,在栈上保存一份属于自己的。函数一旦退出,会自动释放
         pthread_t tid;
         pthread_create(&tid,NULL,ThreadEntry,(void*)new_sock);
        //采用detach,不关注结果,这样才能保证accpet快速被调用
         pthread_detach(tid);
     }
 }
//通过命令行参数,把需要绑定的ip和port传进来
 int main(int argc,char* argv[])
 {
    if(argc!=3)
    {
         printf("Usage: ./http_server ip port\n");
         return -1;
     }
     printf("输入正确\n");
     //http服务器启动入口函数
     HttpServerStart(argv[1],atoi(argv[2]));
     return 0;
 }

2. 基本的处理流程

//请求的处理
 void HandlerRequest(int new_sock)
 {
     printf("Rev Request\n");
     int err_code=200;
     HttpRequest req;
     memset(&req,0,sizeof(req));
     //1.解析请求(按照http协议的格式进行解析)
     // a)按行读socket,读出HTTP请求的首行
    printf("解析首行之前\n");
     if(ReadLine(new_sock,req.first_line)<0)
    {
         printf("ReadLine first_line failed\n");
         err_code=404;
         //构造404响应的代码
         goto END;
     }
     printf("first_line=%s\n",req.first_line);
     //b)解析首行,获取到方法和url
     if(ParseFirstLine(req.first_line,&req.method,&req.url)<0)
     {
         printf("ParseFirstLine failed\n");
         err_code=404;
         //此时对错误作统一处理使用404这个错误码
        //构造404响应的代码
         goto END;
     }
     printf("method=%s url=%s\n",req.method,req.url);
    // c)解析url,获取url_path和query_string
    if(ParseUrl(req.url,&req.url_path,&req.query_string)<0)
     {
         printf("ParseUrl failed\n");
         err_code=404;
         //构造404响应的代码
         goto END;
     }
     printf("method=%s,url_path=%s,query_string=%s\n",req.method,req.url_path,req.query_string);
     // d)读取并解析header部分,这里只保留Content_Lenght,
     // 其他的header内容直接丢弃
     if(ParseHeadler(new_sock,&req.content_length)<0)
    {
         printf("ParseHeadler failed\n");
         err_code=404;
        //构造404响应的代码
         goto END;
     }
     //body暂时不解析,交给后面的逻辑根据方法来决定是否需要解析body
     //2.根据收到的请求进行计算,生成响应,把响应写回客户端
     if(strcmp(req.method,"GET")==0 && req.query_string==NULL)
     {
        // a)静态页面:如果是GET方法且没有query_string
         err_code=HandlerStaticFile(&req,new_sock);
     }
     // b)动态页面:若方法为GET,有query_string
     else if (strcmp(req.method,"GET")==0 && req.query_string!=NULL)
     {
         // b)动态页面:若方法为GET,有query_string
         err_code=HandlerCGI(&req,new_sock);
     }
     else if(strcmp(req.method,"POST")==0 )
     {
        //  c)动态页面: 方法为POST
        }
     else
    {
         printf("method not suport! method=%s\n",req.method);
        err_code=404;
         goto END;
    }
 END:
     if(err_code!=200)
     {
         Handler404(new_sock);
     }
     //在这里响应写完之后,服务器主动断开连接,进入到一个TIME_WAIT的状态。
     //由于服务器可能在短时间内接受的大量的连接,服务器出现了大量的TIME_WAIT
     //可能会导致下一次连接不上。因此需要设置setsocketopt REUSEADDR重用TIME_WAIT状态的连接
 
     close(new_sock);
 }

3.按行读取

//从socket中读取一行
  int ReadLine(int new_sock,char line[])
  {
     //按行读取数据,浏览器发送数据中的行分割符可能不一样
     //\r  \n    \r\n
     //将不同的行分割符都转化为\n
     //1.从socket中读取字符,一次读一个
    char c='\0';
     int output_index=0;//描述当前读到字符应该放到缓冲区的哪个下标上
      while(1)
     {
          ssize_t read_size=recv(new_sock,&c,1,0);//从sock中读数据
         if(read_size<=0)
         {
             return -1;
         }
         //2.判断当前读到的字符是不是 \r
          if(c=='\r')
          {
             //3.如果c是\r,尝试读取下一个
            //MSG_PEEK只是看一下里面的内容,并不从缓冲区中删除
             recv(new_sock,&c,1,MSG_PEEK);
              if(c=='\n'){
                  //4.如果下一个字符是\n,说明行分隔符就是\r\n 
                  //就把行分隔符改为\n
                  //此处在进行一次recv,是为了把缓冲区中的\n拿出来
                  recv(new_sock,&c,1,0);
             }else
              {
                 // 5.若下一个字符是其他的字符,说明行分隔符就是\r
                  // 就把\r修改为\n
                  c='\n';
              }
          }
          //经过上面的if,不论行分隔符是\r还是\r\n,c都已经变成了\n
          //6.如果是其他的字符,就直接放到缓冲区中
          line[output_index++]=c;
          //7.再来判断当前字符是不是\n,如果当前字符是\n
          //说明这一行读完了,就退出循环,
         if(c=='\n')  //将这个if写到这里,读出的行后面就包含\n
          {
              break;
          }
      }
      //这次读了多少个字节返回去了
      return output_index;
  }

4.解析首行

  //字符串分割
  int Split(char first_line[],const char* split_char,char* output[])
  {
      char* tmp=NULL;
      int output_index=0;
      //不可以用strtok这个函数,因为这个函数存在线程不安全的问题。其内部是用static
      //进行了修饰,也就是在静态全局区中,变量只存了一份,存在竞争
      //因此用strtok_r这个函数进行替换。其内部没有用static进行修饰,因此需要手动传参
      //来记录上次切分的位置
      char* p=strtok_r(first_line,split_char,&tmp);
      while(p!=NULL)
      {
          output[output_index++]=p;
          p=strtok_r(NULL,split_char,&tmp);
      }
      return output_index;
  } 
  //解析首行 解析出需要的url和方法
  //当前只考虑不带域名的简单情况
  //GET /index.html?a=10 HTTP/1.1
 //按空格进行字符串切分,切分出三部分
  int ParseFirstLine(char first_line[],char** p_method,char** p_url)
  {
      char* tok[100]={NULL};
     //tok_size 描述字符串切分出的部分有几个
     int tok_size=Split(first_line," ",tok);
     if(tok_size!=3)
     {
         printf("first_line Split failed ! n=%d",tok_size);
         return -1;
     }
     //将切割的结果返回
     *p_method=tok[0];
    *p_url=tok[1];
     return 0;
 }

   5.解析url

//解析url
 //url形如
 // /index.html?a=10&b=20
 // /index.thml
 // 先不考虑带有域名的
 int ParseUrl(char url[],char** p_url_path,char**p_query_string)
 {
     *p_url_path=url;
     char*p=url;
     for(;*p!='\0';++p)
   {
        if(*p=='?')
        {
            //若找到?说明url中带有query_string
             //将?设为\0
             *p='\0';
             //p+1就是query_string的位置
             *p_query_string=p+1;
             return 0;
         }
     }
     //如果循环退出没有找?,那么此时url中不存在query_string
     //就让query_string指向null
     *p_query_string=NULL;
     return 0;
 }

6.解析头部

 //解析头部
 int ParseHeadler(int new_sock,int* content_length_str)
 {
     //由于header部分是按行组织的数据
     //循环尝试读取header,每次读一行
     while(1)
    {
        char line[SIZE]={0};
         int read_size=ReadLine(new_sock,line);
        if(read_size<0)
         {
             printf("ReadLine failed!\n");
             return -1;
         }
        //此处需要考虑比较 Readline的实现细节
         //若Readline返回结果中不包含 \n
        //此代码就需要和空串对比
         if(strcmp(line,"\n")==0)
         {
             //读到了空行,说明header结束
             return 0;
         }
         //针对当前读到的行进行判断,看这一行是不是Conten_Length:
         //例如读到了一行形如:
         ///content_length:10\n
         const char* Content_length_str="Content_Length: ";
         if(strncmp(line,Content_length_str,strlen(Content_length_str)==0))
         {
             *content_length_str=atoi(line+strlen(Content_length_str));
            //此处不可以直接返回,因为要将缓冲区的所有数据都拿出来。防止黏包
        }
     }
 }

7.错误处理

 void Handler404(int new_sock)
 {
     printf("Handler404~~~\n");
     //构造404错误页面 严格按照HTTP响应格式
     const char* first_line="HTTP/1.1 404 Not Fonud\n";
     const char* blank_line="\n";
     //body部分的内容就是HTML
     const char* body="<head> <meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">"
         "</head><body><h1>您的页面去偷偷去玩了!!</h1></body>";
     char header[SIZE]={0};
     sprintf(header,"Content-Length:%lu\n",strlen(body));
 
     send(new_sock,first_line,strlen(first_line),0);
     send(new_sock,header,strlen(header),0);
     send(new_sock,blank_line,strlen(blank_line),0);
     send(new_sock,body,strlen(body),0);
     return ;
 }

8.静态页面的处理

 int IsDir(const char* file_path)
 {
    struct stat st;
     int ret=stat(file_path,&st);//查看文件属性
    if(ret<0)
     {
         perror("stat");
         return -1;
    }
     if(S_ISDIR(st.st_mode))//此宏可以查看文件类型是否为目录文件
     {
         return 1;
     }
     return 0;
 }
//获取路径
 void GetFilePath( const char* url_path,char file_path[])
 {
     //根据HTTP服务器的根目录进行拼接
     sprintf(file_path,"./wwwroot%s",url_path);
     //如果用户传的url_path如下:/image
     //假设这个url_path是一个目录,那就要构造一个默认的文件路径,尝试取目录下的/index.html
     //如果用户传的url_path是:/image,实际就为:/image/index.html
     //因此需要判断file_path是普通文件还是目录文件  
     if(IsDir(file_path))
     {
       //file_path可能存在的情况:
        //1./image/
        //2./image
         if(file_path[strlen(file_path)-1]=='/'){
             strcat(file_path,"index.html");
         }else{
             strcat(file_path,"/index.html");
         }
     }
 }
 
 int GetFileSize(const char* file_path)
 {
     struct stat st;
     int ret=stat(file_path,&st);
     if(ret<0)
     {
         return 0;
     }
     return st.st_size;
 }
 int WriteStaticFile(const char* file_path,int new_sock)
 {
     //1.打开文件
     //2.读取文件内容
     //3.根据文件内容构造http响应
     //4.将响应内容写到sock中
     int fd=open(file_path,O_RDONLY);
     if(fd<0)
     {
         printf("file open failed! file_path=%s\n",file_path);
        return 404;
     }
     const char* first_line="HTTP/1.1 200 OK\n";
     int size=GetFileSize(file_path);
     char header[SIZE]={0};
     sprintf(header,"Content-Length: %d\n",size);
     const char* blank_line="\n";
     send(new_sock,first_line,strlen(first_line),0);
     send(new_sock,header,strlen(header),0);
     send(new_sock,blank_line,strlen(blank_line),0);
     sendfile(new_sock,fd,NULL,size);
     //关闭文件
     close(fd);
     return 200;
 }
 int HandlerStaticFile(const HttpRequest* req,int new_sock)
 {
     //1.拼接目录(根据url_path构造出当前文件系统上的真实目录)
     //2.打开文件,读取文件内容,根据文件内容构造HTTP响应
     //其中文件的内容就作为HTTP响应中body的内容
     char file_path[SIZE]={0};
     GetFilePath(req->url_path,file_path);
     int err_code=WriteStaticFile(file_path,new_sock);
     return err_code;
} 

9.动态页面的处理

 //3.父进程执行父进程的相关逻辑
 int  HandlerCGIFather(const HttpRequest* req,int new_sock,int father_read,int father_write)
 {
     printf("父进程逻辑\n");
     //a) 父进程把HTTP请求的body部分写到管道
         if(strcmp(req->method,"POST")==0){
         int content_length=req->content_length;
         int i=0;
         for(;i<content_length;++i)
         {
             char c='\0';
             recv(new_sock,&c,1,0);
             //printf("ccc\n");
            write(father_write,&c,1);
 
         }
     }
     //b)  父进程尝试取读取子进程构造的结果
     //c) 父进程构造HTTP响应,写回客户端
     const char* first_line="HTTP/1.1 200 OK\n";
     const char* header="Content-Type: text/html\n";
     const char* blank_line="\n";
     send(new_sock,first_line,strlen(first_line),0);
     send(new_sock,header,strlen(header),0);
     send(new_sock,blank_line,strlen(blank_line),0);
     char c='\0';
     while(read(father_read,&c,1)>0){
         //从管道中读数据,如果所有的写段关闭,read将读到EOF,从而返回0
         send(new_sock,&c,1,0);
     }
     //d) 父进程回收子进程
     //进程等待可以,但更简单的是忽略SIGCHLD信号
     return 0; 
 } 
 //子进程执行子进程的相关逻辑
int  HandlerCGIChild(const HttpRequest* req,int child_read,int child_write){
     // a)设置环境变量 
     char request_method_env[SIZE]={0};
     sprintf(request_method_env,"REQUEST_METHOD=%s",req->method);
     putenv(request_method_env);
     if(strcmp(req->method,"GET")==0)
     {
         char query_string_env[SIZE]={0};
         sprintf(query_string_env,"QUERY_STRING=%s",req->query_string);
         putenv(query_string_env);
     }else {
         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)  根据url_path构造出CGI路径
     char file_path[SIZE]={0};
     GetFilePath(req->url_path,file_path);
     // d)  进行程序替换(也就进入到了CGI程序内部)
     //l:通过边长参数列表来传输参数  lp:从path中获取  le:手动构造环境变量 
     //v通过数组 vp ve
     execl(file_path,file_path,NULL);
     exit(0);
     return 0;
 }
 int HandlerCGI(const HttpRequest* req,int new_sock)
 {
    //1创建一对匿名管道
      int fd1[2];
      int fd2[2];
      pipe(fd1);
      pipe(fd2);
      int father_read=fd1[0];
      int child_write=fd1[1];
      int child_read =fd2[0];
      int father_write=fd2[1];
      printf("进入动态\n");
      //2创建子进程
      int ret=fork();
      if(ret>0){
          close(child_write);
          close(child_read);
          //3.父进程执行父进程的相关逻辑
          printf("父进程\n");
        HandlerCGIFather(req,new_sock,father_read,father_write);
            close(father_read);
            close(father_write);
      }
      else if(ret==0)
      {
          close(father_read);
          close(father_write);
          printf("子程序\n");
          //4.子进程执行子进程的相关逻辑
           HandlerCGIChild(req,child_read,child_write);
      }
      else{
      perror("fork");
      }
      return 200;
 }

当写完大体框架时我们先进行验证,此时将动态静态页面全部返回404

静态页面:

 <html>
   <head>
   <meta http-equiv="content-type" content="text/html;charset=utf-8">
   </head>
   <body>
       <h1>小喵的世界欢迎你~</h1>
       <img src="/image/1.jpg">
   </body>
   </html>

 

动态页面如下:

  <html>
   <form action="/calc/calc">
   a:<br>
   <input type="text" name="a">
   <br>
   b:<br>
   <input type="text" name="b">
   <br><br>
   <input type="submit" value="Submit">
  </form>
  <body background="/calc/image/1.jpg">
  </body>
  </html>
void GetQueryString(char output[])
   {
       //按照CGI的协议实现此处协议
      //1.先获取方法
      char* method=getenv("REQUEST_METHOD");
      if(method==NULL)
      {
          //没有获取到 环境变量
          fprintf(stderr,"REQUEST_METHOD failed\n");
          return;
      }
      if(strcmp(method,"GET")==0)
      {
         char* query_string=getenv("QUERY_STRING");
          if(query_string==0)
          {
              fprintf(stderr,"QUERY_STRING failde\n");
              return;
          }
          strcpy(output,query_string);
      }
      else{
          //POST
     //获取CONTENT_LENGTH
      char* content_length_env=getenv("CONTENT_LENGTH");
     if(content_length_env==NULL)
         {
         fprintf(stderr,"CONTENT_LENGTH filed\n");
         return;
     }
    //根据CONTENT_LENGTH读取body内容
     int content_length=atoi(content_length_env);
      int i=0;
     for(;i<content_length;++i)
     {
          char c='\0';
          read(0,&c,1);
        output[i]=c;
      }
     return;
  }
  }
  int main()
  {
    //1.基于CGI协议获取到需要的参数
    char query_string[SIZE]={0};
    GetQueryString(query_string);
    //2.根据业务逻辑(计算器相关的逻辑),进行计算
    //此时获取到的QUERY_STRING形如:
    //   a=10&b=20
    int a=0;
    int b=0;
    sscanf(query_string,"a=%d&b=%d\n",&a,&b);
    int sum=a+b;
    //3.把结果构造成HTML写回到标准输出中
    printf("<html><h1>sum=%d</h1></html>",sum);
    return 0;
  }

 

 

 

遇到的问题:

1.乱码问题   是因为编码格式不对

2. 在进行字符串切割的时候,不能使用strtok,线程不安全

3.在拼接目录要考虑到客户端请求的是目录还是文件

4.CGI要在程序替换之前进行重定向

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值