基于CGI协议的HTTP服务器项目

HTTP服务器—支持CGI协议

一. 项目介绍

利用掌握的TCP/IP协议构建服务器,根据http协议格式解析从客户端接收的请求,处理请求,构造响应并发送给客户端,支持CGI协议,通过设置环境变量和读写标准输入输出的方式进行参数的传递。


二. 技术特点

  1. 使用 socket套接字完成构建TCP/IP服务器

  2. 使用到 epoll 模型,提高了并发的速度

  3. 支持CGI程序,通过设置环境变量和标准输入输出流的方式,进行数据获取

  4. 支持HTTP协议的GET和POST请求,且对这两个请求有不同的处理方法

  5. 対路径做基本处理:用户给的路径不完整时,可以补充为默认路径

  6. 涉及到的技术点:socket套接字,文件操作,管道,进程创建,进程替换,环境变量


三. HTTP协议

1. http协议介绍

  1. HTTP协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准

  2. HTTP协议基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)

  3. HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。

  4. HTTP是一个无状态的协议,对事务处理是没有记忆的和独立的

  5. HTTP默认的端口号为80,HTTPS的端口号为443。

2. 短链接和长链接

短链接:服务器每次连接只处理一个用户请求,并且客户端接受服务器应答后立刻断开连接,如果客户端没有发出请求,服务器不会专门等待,也不会在完成一个请求后还保持原来的请求,而是会立即断开这次连接

长链接: 服务器在发送响应后一段时间内仍然保持这条连接,允许在这条连接上进行多次请求与响应。

本项目的服务器采用的短链接方式,客户端发送请求,服务端进行请求处理,在服务端完成响应后,会主动断开连接,不会保持请求。

3. http请求与响应过程

从用户在一个浏览器中输入一个网址,到获取到整个页面,中间发生了什么?

  1. 域名解析

    1. 首先浏览器会查询本地 hosts 文件,找相应的域名解析
    2. 在hosts文件中没有找到,就会在浏览器的缓存中查找是否有该域名的缓存
    3. 没有找到的话,就会去DNS中获取该域名对应的ip地址
  2. 建立连接

    1. 得到ip地址后,客户端通过路由找到服务器端
    2. 客户端会先尝试通过TCP/IP协议来和服务器建立连接(三次握手)
  3. 获取数据

    1. 连接建立完成,客户端向服务器端发起请求
    2. 服务器得到请求,做出相应的业务处理,构造http响应并传回客户端
    3. 浏览器接收到数据后,根据http协议格式分析响应,并将结果展示到页面上
  4. 断开连接

    1. 服务器在发送数据后会主动断开连接(四次挥手)
    2. 客服端在接收到数据后会断开连接

    这样一次从浏览器到客户端的请求和相应也就完成了。


4. http协议的报文格式

  1. 请求报文格式

    1. 请求首行:方法,url,版本号,三部分中间用空格分离
    2. 请求头部:以键值对的方式构成,每一个键值对占一行,描述本次协议的属性
    3. 空行:用来分割body
    4. body: 请求的内容,GRT方法的请求没有body,POST请求有body
  2. 响应报文格式

    1. 响应首行:版本号,状态码,状态对应的内容,三部分中间用空格分离
    2. 响应头部:同请求格式一样
    3. 空行:分割body
    4. body: 响应的内容

5. get和post方法

http协议的方法有很多:

  • GET 获取资源
  • POST 向服务器端发送数据,传输实体主体
  • PUT 传输文件
  • HEAD 获取报文首部
  • OPTIONS 询问支持的方法
  • TRACE 追踪路径

比较常用的两个方法

  1. GET方法

    GET方法没有body,如果GET方法请求的是CGI程序,就要求客户端将参数以键值对的方式放到url中,并且以将参数与CGI程序的路径分割开来,如/cgi?a=1&b=2&c=3

    因此在url中可以看到的就是GET方法,但是GET是不安全的,因为参数在url中,因此用户也是可见的,在传输密码敏感信息是时不合适的。

    另外GET方式也不适用于传送大量数据,因为浏览器的地址栏一般最多只能识别1024个字符,因此当需要传递大量数据时,GET也不适用。

  2. POST方法

    POST方法从客户向服务器传送数据,允许客户端给服务器提供的数据更多,POST请求将数据封装到请求包体行中,数据之间用&分隔,POST方法可以传输大量数据,没有大小限制,而且不会在url中显示

    一般在网页中利用表单的方式,发送POST请求


6. url解析

url格式:

基于CGI协议的HTTP服务器项目_第1张图片

在GET方法中:参数以键值对的方式跟文件路径之后,且用?分割,每个键值对间用&分割

我们将?后面的参数集合成为:query_string,获取query_string 也是一个重点


四. CGI协议

CGI(Common Gateway Interface):是HTTP服务器与你的或其它机器上的程序进行“交谈”的一种工具,其程序须运行在网络服务器上。

CGI处理步骤:

  1. 通过Internet把用户请求送到服务器
  2. 服务器接收用户请求并交给CGI程序处理
  3. CGI程序把处理结果传送给服务器
  4. 服务器把结果送回到用户

CGI参数获取与结果传递

  1. 参数获取:在通过提前设置环境变量,或者使用管道的方式从标准输入中读取

  2. 结果传递:重定向标准输出到管道的输入端口,通过管道将数据发送给服务器


五. 项目具体实现

1. TCP服务器的搭建

    // 套接字初始化
    int SocketInit(char* ip, int port){
      //建立TCP客户端
      int sock = socket(AF_INET,SOCK_STREAM, 0);
      if( sock < 0 ){
        perror("socket");
        exit(1);
      }

      //设置端口可重用,解决time_wait时端口被占用的情况
      //要在绑定前设置
      int opt = 1;
      setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);

      //进行端口号绑定
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(ip);
      if(bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0 ){
        perror("bind");
        exit(1);
      }

      //监听文件描述符
      if(listen(sock, 5) < 0)
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(ip);
      if(bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0 ){
        perror("bind");
        exit(1);
      }

      //监听文件描述符
      if(listen(sock, 5) < 0){
        perror("listen");
        exit(1);
      }
      return sock;
    }

2. 构建epoll服务器

    //建立 epoll 文件描述符
    epoll_fd = epoll_create(10);
    //epoll添加文件描述符
    void epoll_add(int fd)
    {
      struct epoll_event event;
      event.events = EPOLLIN;
      event.data.fd = fd;
      epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);
    }

3. 处理客户端连接

    void ProcessCreate(int sock)
    {
      struct sockaddr_in addr;
      socklen_t len = sizeof addr;
      //客户端连接服务器端
      int conn_sock =  accept(sock, (struct sockaddr*)&addr, &len);
      if(conn_sock < 0)
      {
        perror("accept");
        return ;
      }
      //将新的文件描述符添加进 epoll 模型
      epoll_add(conn_sock);
    }

4. 处理客户端请求,http 协议解析

    //首行的获取:
unsigned get_line(int sock, char* line, size_t size)
{
  unsigned i = 0;
  char ch = 'a';  
  size_t s;
  while( i < size-1 && ch != '\n' ){
    s = recv(sock, &ch, 1, 0);
    if(s > 0){
      if(ch == '\r'){
        recv(sock, &ch, 1, MSG_PEEK);
        if(ch != '\n' )
          ch = '\n';
        else 
          recv(sock, &ch, 1, 0 );
      }
        line[i++] = ch;
    }
    else
      break;     
  }
  line[i] = '\0';
  return i;
}

我们要处理http请求,我们必须先处理首行,获取到首行,进行分析取出方法和url

  //2. 在首行中获取方法和url
  //2.1获取方法
  while(i < sizeof(method) -1 && j < sizeof(first_line) && !isspace(first_line[j]))
  {
      method[i++] = first_line[j++];
  }
  method[i] = '\0';
  //处理掉多余的空格
  while(j < sizeof(first_line) && isspace(first_line[j]))
  {
    j++;
  }
  i = 0;
  //2.2继续获取url
  while(i < sizeof(url) -1 && j < sizeof(first_line) && !isspace(first_line[j]))
  {
      url[i++] = first_line[j++];
  }

接下来判断方法:

如果是POST方法,就按照cgi方向处理

如果是GET方法,判断是否有query_string,如果没有query_string就按静态页面的方向处理,如果有就按照cgi方向处理

获取query_string的方法是,使用指针在url中查找?,将其改为'\0',使指针指向下一个字符,即为query_string的起始位置,而url字符串也就变成了文件路径path,我们处理文件路径,在该路径下加上我们本地文件的相对路径即可。

最后我们根据path来执行相应的cgi程序,或者显示相应的html文件

5. 静态页面的显示

void echo_www(int sock, char* path, int size, int* errCode)
{
  clear_header(sock);
  char line[MAX];

  int fd = open(path, O_RDONLY);
  if( fd < 0)
  {
    *errCode = 0; 
    return ;
  }

  //构造响应的首行
  sprintf(line, "HTTP/1.1 200 OK\r\n");
  send(sock, line, strlen(line), 0);

 //构造头部
  sprintf(line, "Content-Length: %d\r\n",size);
  send(sock, line, strlen(line), 0);

  //构造空行
  sprintf(line, "\r\n");
  send(sock, line, strlen(line), 0);

    //使用sendfile直接在文件描述符间传递数据,加快效率
  sendfile(sock, fd, NULL, size);

  close(fd);
}   

6. CGI程序的调用

CGI程序的调用是我们这个项目的重点,一般是通过创建进程使用进程替换来调用,当我们执行一个CGI程序前,要做的就是环境变量的处理,即传递参数,本项目中的具体步骤如下:

首先,判断方法,根据不同的方法,进行不同的处理

如果是GET方法:我们就不需要做什么特殊处理,因为query_string在之前已经获取好了,我们需要做的就是清空socket描述符中header数据,因为我们之前并没有对它做什么处理,且get方法不需要得到body,因此对于header数据也就没有什么需要。

如果是POST方法:我们需要从header中获取Content-Length的值,因为我们要获取body,从body中获取参数,而body的长度就在Content-Length中,并且这一步,顺表帮我们清空了socket中的header数据,避免对后面读取数据时的影响。

其次,我们构建http相应的首行,头部和空行,并将结果写入socket文件描述符,

最后,我们开始设置环境变量,设置之前我们要先创建两个管道,方便后续进行数据交互,因为管道是单向的因此需要我们两个,之后进行进程创建,未来子进程是要做程序替换,而父进程是来传递POST中的body,并接受处理结果的。

在子进程中先设置method环境变量,使用putenv()函数设置。如果是GET方法,就设置query_string环境变量,如果是POST方法,就设置content_length环境变量,表示body的长度。

在父进程中首先判断是否是POST,如果是,就读取body并将数据写到管道中,然后从另外一个管道进行结果读取,将结果写进socket文件描述符即可,当读取数据结束,完成CGI程序的调用。


总结

本项目基于http协议和cgi协议实现了一个简单版本的HTTP服务器,涉及的内容并不复杂,主要是注意细节的处理,和文件字符串的处理,在传输过程中可能会有编码错误,希望大家有所注意

项目的思路就如上所述,想看完整的项目代码请移步

github链接:简单的HTTP项目


最后,附上几张已经成型的页面:

在浏览器输入 http://192.168.183.130:9090/index.html (本地地址,不是公网地址)
基于CGI协议的HTTP服务器项目_第2张图片

我是按照搜索页面制作的,在下图中,一个简单输入框,输入数据,点击搜索来提交数据

基于CGI协议的HTTP服务器项目_第3张图片

结果页面如下:
基于CGI协议的HTTP服务器项目_第4张图片
上图的url是:http://192.168.183.130:9090/cgi/add?query=abcdef
可以看到是GET方法,?后面是参数

最后,然后点击返回即可可以回到主页面
基于CGI协议的HTTP服务器项目_第5张图片

你可能感兴趣的:(HTTP服务器项目)