http服务器实现(三)

前言

本文基于http服务器实现(二)来完成一个能处理http静态页面和动态页面请求的web服务器。
涉及到的内容有:

  1. CGI 理解
  2. 程序中注意的坑提醒
  3. 源码中对get请求的分析
  4. 用火狐浏览器测试
  5. 未解决的问题
  6. 服务器程序全部源码

http服务器实现(一)讲的是http服务器软件整体的架构,http服务器实现(二)主要讲的是http报文解析部分,这一节紧随前面的步骤实现一个小型的http服务器。服务器目前支持get请求,可以获取静态页面和动态(cgi) 页面。稍后,将结合火狐浏览器来演示成果。在后面的文章中,会相继完善http服务器的功能。例如,支持处理客户端的post请求,把程序做成守护进程,增加一些信号的处理等等。看完本节之后,你会发现开发一个web服务器的基本功能是如此的简单!但是,开发一个长期运行而不奔溃的健壮的web服务器是一件很困难的事情,需要方方面面考虑到,像高并发处理、效率优化、信号捕捉、安全性能、HTTPS 协议支持、各种后台语言的支持等等。刚调试好了程序,现在把成果分享出来。
快速理解本文的方法是:把本节代码下载下来,然后自己动手实验,通过log打印、实验结果去理解。代码能够编译得过且能运行!

一、CGI(通用网关接口)

CGI 是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据库API 与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。可以简单理解是CGI是可执行程序,例如二进制可执行文件,shell脚本等。CGI经常向标准输出,也就是终端打印出HTML数据。在服务器程序中,我们只需要用dup2函数把标准输出重定向到socket描述符,把CGI输出的数据发送给浏览器,然后浏览器将显示这些数据。来看看一些实例。
如下,是一个C语言写的程序,我们看到str字符指针上存放一串字符串,然后向终端打印出来。

#include <stdio.h>

int main()
{
    printf("Content-Type: text/html\n\n");
    const char* str = "<html>"
    "<head><title>C CGI</title></head>\r\n"
    "<body><p>this is a C</p></body>"
    "</html>";    
    printf("%s",str);
    return 0;
}

gcc c_program.c -o c_program.cgi编译成二进制文件。c_program.cgi就是一个CGI可执行程序。我们关注的重点是打印出来的数据。我们可以看出打印出来的是

Content-Type: text/html

<html><head><title>C CGI</title></head>
<body><p>this is a C</p></body></html>

我们要做的就是把这段打印传输给浏览器。很简单,只需要用dup2(fd, STDOUT_FILENO)把标准输出重定向到sockfd。来看看浏览器接收到数据之后是怎样显示的,画面如下:
这里写图片描述

跟我们预料的一样,通过上面的实验,是否更加形象的明白CGI究竟是怎样的回事呢?
我们还可以用shell来编写,再看看下面的例子:

#!/bin/bash
echo "Content-type:text/html"
echo ""
echo "this is a shell program"
echo ""
echo "a text"
echo ""

通过上面两个例子,大致知道了CGI无非就是要往终端打印出浏览器要接收的数据,运用重定向的方法可以很方便的实现。我们还可以往CGI程序传入参数,然后CGI程序根据我们传进去的参数动态修改打印的数据。这些知识点在下面的程序中都将会遇到。

二、需要注意的点、坑

在这里讲下,我再调试程序的时候遇到的坑和需要注意到的点。

bind函数

所谓bind,就是指绑定本地接受端口。 指定ip,是为了分辨多ip主机。
比如你的机器有两个ip
192.168.1.105
192.168.2.1
如果你server_sockaddr.sin_addr.s_addr = inet_addr("192.168.1.105"); 然后监听6000端口
这时其他机器只有连接上192.168.1.105:6000才能访问到服务器,连接 192.168.2.1 将会失败。
如果server_sockaddr.sin_addr.s_addr =htonl(INADDR_ANY); 的话,两个ip 的6000端口都能连接得上。
如果不知道本地ip地址,可以在终端用ifconfig命令打印出来,bind绑定的ip必须是本地有才可以,随便绑定一个ip是不会成功的。

mmap函数

在程序中,处理get静态请求的时候,用到了mmap函数。它的作用是把文件的内容映射到内存空间中,我们可以操作这段内存来读写数据。那为什么要这么做呢?常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。因此mmap效率更高。

dup2函数

这里简单理解为重定向函数,可以把一个文件描述符重定向到另一个文件描述符。这也是实现CGI机制的关键。

Firefox设置

本文我用到了火狐浏览器实验。在访问服务器的时候,火狐弹出了警告:
此网址使用了一个通常用于网络浏览以外目的的端口。出于安全原因,Firefox 取消了该请求
用如下方法或者使用ie浏览器可以规避。
解决方法如下:
在Firefox地址栏输入about:config,然后在右键新建一个字符串键network.security.ports.banned.override,将需访问的端口号添加上,这里我添加了端口号6000。

三、服务器get请求源码分析

http服务器实现(二)的代码为基础,在header_parse函数下 status == BODY_READ 的地方,添加了process_header_end函数来针对http GET请求的处理。先来看下这部分代码:

int process_header_end()
{
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filename[MAXLINE], cgiargs[MAXLINE];
    //前面已经把方法解析出来放入method全局变量,这里判断是不是GET请求,注:当前版本只支持GET请求。
    if (method != M_GET) {
        perror("does not implement this method");
        //此处发送501响应
        return;
    }
    //parse_uri函数有两个功能,一是解析出请求的文件路径,二是判断该请求是静态的还是动态的。
    is_static = parse_uri(request_uri, filename, cgiargs);       
    if (stat(filename, &sbuf) < 0) {
        perror("couldn't find this file");
        return;
    }                                                    

    if (is_static) { //对静态请求处理    
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { 
        perror("couldn't read the file");
        return;
    }
    serve_static(fd, filename, sbuf.st_size);        
    }
    else { //对动态请求处理
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { 
        perror("couldn't run the CGI program");
        return;
    }
    serve_dynamic(fd, filename, cgiargs);           
    }
    return 0;
}

以上代码,利用之前对请求行解析出来的结果,判断是不是GET请求,如果是,继续解析URI,如果是静态GET,就走静态处理路线,如果是CGI请求,就走动态处理路线。接下来看看静态GET请求的代码:

void serve_static(int fd, char *filename, int filesize) 
{
    printf("this is serve_static\n");
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXLINE];

    //发送响应头给客户端
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    if (rio_writen(fd, buf, strlen(buf)) != strlen(buf))      
        linux_error("rio_writen");

    //发送响应体给客户端
    if((srcfd = open(filename, O_RDONLY, 0)) < 0)
        linux_error("open");
    //将文件内容映射到虚拟内存中,提高文件的读写效率
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    close(srcfd);   
    //将请求的内容发送给浏览器
    if (rio_writen(fd, srcp, filesize) != filesize)
        linux_error("rio_writen");
    //解除映射
    Munmap(srcp, filesize); 
    close(fd);
}

从上面可以看出,serve_static函数主要干的事情是打开浏览器请求的文件,然后把文件内容映射到虚拟内存中,然后再把虚拟内存中的数据(文件的内容)发送给浏览器。其中,rio_writen函数是对write函数的包装,详情见源码。动态内容请求处理函数如下:

void serve_dynamic(int fd, char *filename, char *cgiargs) 
{
    printf("this is serve_dynamic\n");
    char buf[MAXLINE], *emptylist[] = { NULL };
    // 发送响应头第一行 
    sprintf(buf, "HTTP/1.1 200 OK\r\n"); 
    if (rio_writen(fd, buf, strlen(buf)) != strlen(buf))
        linux_error("rio_writen");
    //发送响应头选项
    sprintf(buf, "Server: Tiny Web Server\r\n");
    if (rio_writen(fd, buf, strlen(buf)) != strlen(buf))
        linux_error("rio_writen");

    printf("filename=%s\n",filename);
    //fork出子
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值