精华内容
下载资源
问答
  • C语言web服务器

    2012-06-01 23:23:17
    web,服务器,C语言编写,实现了基本的http协议,在本地直接运行,无需安装
  • C语言Web服务器

    2013-05-27 00:06:11
    C语言描述的单线程Web服务器代码
  • web服务器 C语言

    2016-02-18 12:58:37
    一个用C语言编写的WEB服务器,用socket编程
  • C语言制作Web服务器

    2021-01-06 13:44:43
    本文,我们将使用C语言从零开始实现一个支持静态/动态网页的Web服务器。我们把这个服务器叫做Tiny。 背景知识 客户端-服务器编程模型 使用socket处理请求与响应 HTTP协议与静/动态网页 关键代码解析 实验效果与源码 ...

    阅读经典——《深入理解计算机系统》09
    本文,我们将使用C语言从零开始实现一个支持静态/动态网页的Web服务器。我们把这个服务器叫做Tiny。

    背景知识
    客户端-服务器编程模型
    使用socket处理请求与响应
    HTTP协议与静/动态网页
    关键代码解析
    实验效果与源码
    背景知识
    Web服务器使用HTTP协议与客户端(即浏览器)通信,而HTTP协议又基于TCP/IP协议。因此我们要做的工作就是利用Linux系统提供的TCP通信接口来实现HTTP协议。

    而Linux为我们提供了哪些网络编程接口呢?没错,就是socket(套接字),我们会在后面详细介绍该接口的使用方式。

    另外我们应该清楚Linux的系统I/O和文件系统的关系。在Linux中,所有I/O设备都被看作一个个文件,I/O设备的输入输出被认做读写文件。网络作为一种I/O设备,同样被看作文件,而且是一类特殊的文件,即套接字文件。

    我们还要对网络通信协议TCP/IP有一个大致的了解,知道IP地址和端口的作用。

    接下来我们讲解客户端-服务器编程模型。

    客户端-服务器编程模型
    客户端-服务器编程模型是一个典型的进程间通信模型。客户端进程和服务器进程通常分处两个不同的主机,如下图所示,客户端发送请求给服务器,服务器从本地资源库中查找需要的资源,然后发送响应给客户端,最后客户端(通常是浏览器)处理这个响应,把结果显示在浏览器上。

    client-server transaction
    这个过程看起来很简单,但是我们需要深入具体的实现细节。我们知道,TCP是基于连接的,需要先建立连接才能互相通信。在Linux中,socket为我们提供了方便的解决方案。

    每一对网络连接称为一个socket对,包括两个端点的socket地址,表示如下

    (cliaddr : cliport, servaddr : servport)
    其中, cliaddr和cliport分别是客户端IP地址和客户端端口,servaddr和servport分别是服务器IP地址和服务器端口。举例说明如下:

    connection socket pair
    这对地址和端口唯一确定了连接的双方,在TCP/IP协议网络中就能轻松地找到对方。

    使用socket处理请求与响应
    熟悉TCP协议的朋友们应该很容易理解下面的流程图。

    socket overview
    服务器调用socket函数获取一个socket,然后调用bind函数绑定本机的IP地址和端口,再调用listen函数开启监听,最后调用accept函数等待直到有客户端发起连接。

    另一方面,客户端调用socket函数获取一个socket,然后调用connect函数向指定服务器发起连接请求,当连接成功或出现错误后返回。若连接成功,服务器端的accept函数也会成功返回,返回另一个已连接的socket(不是最初调用socket函数得到的socket),该socket可以直接用于与客户端通信。而服务器最初的那个socket可以继续循环调用accept函数,等待下一次连接的到来。

    连接成功后,无论是客户端还是服务器,只要向socket读写数据就可以实现与对方socket的通信。图中rio_readlineb和rio_written是作者封装的I/O读写函数,与Linux系统提供的read和write作用基本相同,详细介绍见参考资料。

    客户端关闭连接时会发送一个EOF到服务器,服务器读取后关闭连接,进入下一个循环。

    这里面用到的所有Linux网络编程接口都定义在<sys/socket.h>头文件中,为了更清晰地帮助大家理解每个函数的使用方法,我们列出它们的函数声明。

    #include <sys/types.h>
    #include <sys/socket.h>

    /**
    获取一个socket descriptor
    @params:
    domain: 此处固定使用AF_INET
    type: 此处固定使用SOCK_STREAM
    protocol: 此处固定使用0
    @returns:
    nonnegative descriptor if OK, -1 on error.
    */
    int socket(int domain, int type, int protocol);

    /**
    客户端socket向服务器发起连接
    @params:
    sockfd: 发起连接的socket descriptor
    serv_addr: 连接的目标地址和端口
    addrlen: sizeof(*serv_addr)
    @returns:
    0 if OK, -1 on error
    */
    int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

    /**
    服务器socket绑定地址和端口
    @params:
    sockfd: 当前socket descriptor
    my_addr: 指定绑定的本机地址和端口
    addrlen: sizeof(*my_addr)
    @returns:
    0 if OK, -1 on error
    */
    int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

    /**
    将当前socket转变为可以监听外部连接请求的socket
    @params:
    sockfd: 当前socket descriptor
    backlog: 请求队列的最大长度
    @returns:
    0 if OK, -1 on error
    */
    int listen(int sockfd, int backlog);

    /**
    等待客户端请求到达,注意,成功返回得到的是一个新的socket descriptor,
    而不是输入参数listenfd。
    @params:
    listenfd: 当前正在用于监听的socket descriptor
    addr: 客户端请求地址(输出参数)
    addrlen: 客户端请求地址的长度(输出参数)
    @returns:
    成功则返回一个非负的connected descriptor,出错则返回-1
    */
    int accept(int listenfd, struct sockaddr *addr, int *addrlen);
    HTTP协议与静/动态网页
    HTTP协议的具体内容在此不再讲述,不熟悉的朋友们可以查看参考资料中的第二篇文章。

    现在我们有必要说明一下所谓的静态网页和动态网页。静态网页是指内容固定的网页,通常是事先写好的html文档,每次访问得到的都是相同的内容。而动态网页是指多次访问可以得到不同内容的网页,现在流行的动态网页技术有PHP、JSP、ASP等。我们将要实现的服务器同时支持静态网页和动态网页,但动态网页并不采用上述几种技术实现,而是使用早期流行的CGI(Common Gateway Interface)。CGI是一种动态网页标准,规定了外部应用程序(CGI程序)如何与Web服务器交换信息,但由于有许多缺点,现在几乎已经被淘汰。关于CGI的更多信息,可以查看参考资料。

    关键代码解析
    Web服务器主进程从main函数开始,代码如下。

    int main(int argc, char **argv)
    {
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    /* Check command line args */
    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }
    
    listenfd = Open_listenfd(argv[1]);
    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        doit(connfd);
        Close(connfd);
    }
    

    }
    主函数参数需要传入服务器绑定的端口号码,得到这个号码后,调用Open_listenfd函数,该函数完成socket、bind、listen等一系列操作。接着调用accept函数等待客户端请求。注意,Accept是accept的包装函数,用来自动处理可能发生的异常,我们只需把它们当成一样的就行了。当accept成功返回后,我们拿到了connected socket descriptor,然后调用doit函数处理请求。

    doit函数定义如下。

    void doit(int fd)
    {
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filename[MAXLINE], cgiargs[MAXLINE];
    rio_t rio;

    /* Read request line and headers */
    Rio_readinitb(&rio, fd);
    if (!Rio_readlineb(&rio, buf, MAXLINE))
        return;
    printf("%s", buf);
    sscanf(buf, "%s %s %s", method, uri, version);
    if (strcasecmp(method, "GET")) {
        clienterror(fd, method, "501", "Not Implemented",
                    "Tiny does not implement this method");
        return;
    }
    read_requesthdrs(&rio);
    
    /* Parse URI from GET request */
    is_static = parse_uri(uri, filename, cgiargs);
    if (stat(filename, &sbuf) < 0) {
    clienterror(fd, filename, "404", "Not found",
            "Tiny couldn't find this file");
    return;
    }
    
    if (is_static) { /* Serve static content */          
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
        clienterror(fd, filename, "403", "Forbidden",
            "Tiny couldn't read the file");
        return;
    }
    serve_static(fd, filename, sbuf.st_size);
    }
    else { /* Serve dynamic content */
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { 
        clienterror(fd, filename, "403", "Forbidden",
            "Tiny couldn't run the CGI program");
        return;
    }
    serve_dynamic(fd, filename, cgiargs);
    }
    

    }
    为了更接近现实,假设现在接收到的HTTP请求如下。该请求的请求头是空的。

    GET /cgi-bin/adder?15000&213 HTTP/1.0
    代码中,Rio_readlineb和sscanf负责读入请求行并解析出请求方法、请求URI和版本号。接下来调用parse_uri函数,该函数利用请求uri得到访问的文件名、CGI参数,并返回是否按照静态网页处理。如果是,则调用serve_static函数处理,否则调用serve_dynamic函数处理。

    serve_static函数定义如下。

    void serve_static(int fd, char *filename, int filesize)
    {
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sConnection: close\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
    Rio_writen(fd, buf, strlen(buf));
    printf("Response headers:\n");
    printf("%s", buf);
    
    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);
    Rio_writen(fd, srcp, filesize);
    Munmap(srcp, filesize);
    

    }
    直接看最后几行代码。Open以只读方式打开请求的文件,Mmap将该文件直接读取到虚拟地址空间中的任意位置,然后关闭文件。接下来Rio_written把内存中的文件写入fd指定的connected socket descriptor,静态页面响应完成。Munmap删除刚才在虚拟地址空间申请的内存。关于mmap函数的更多介绍见参考资料。

    serve_dynamic函数定义如下。

    void serve_dynamic(int fd, char *filename, char *cgiargs)
    {
    char buf[MAXLINE], *emptylist[] = { NULL };

    /* Return first part of HTTP response */
    sprintf(buf, "HTTP/1.0 200 OK\r\n"); 
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));
    
    if (Fork() == 0) { /* Child */
    /* Real server would set all CGI vars here */
    setenv("QUERY_STRING", cgiargs, 1);
    Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */
    Execve(filename, emptylist, environ); /* Run CGI program */
    }
    Wait(NULL); /* Parent waits for and reaps child */
    

    }
    对于动态网页请求,我们的方法是创建一个子进程,在子进程中执行CGI程序。看代码,Fork函数创建子进程,熟悉Linux进程的朋友们应该知道,该函数会返回两次,一次在父进程中返回,返回值不等于0,另一次在子进程中返回,返回值为0,因此if判断内部是子进程执行的代码。首先设置环境变量,用于把请求参数传递给CGI程序。接下来调用Dup2函数将标准输出重定向到connected socket descriptor,这样一来使用标准输出输出的内容将会直接发送给客户端。然后调用Execve函数在子进程中执行filename指定的CGI程序。最后在父进程中调用了Wait函数用于收割子进程,当子进程终止后该函数才会返回。因此该Web服务器不能同时处理多个访问,只能一个一个处理。

    我们给出了一个CGI程序的实例adder,用于计算两个参数之和。代码如下。

    /*

    • adder.c - a minimal CGI program that adds two numbers together
      */
      int main(void) {
      char *buf, *p;
      char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
      int n1=0, n2=0;

      /* Extract the two arguments */
      if ((buf = getenv(“QUERY_STRING”)) != NULL) {
      p = strchr(buf, ‘&’);
      *p = ‘\0’;
      strcpy(arg1, buf);
      strcpy(arg2, p+1);
      n1 = atoi(arg1);
      n2 = atoi(arg2);
      }

      /* Make the response body */
      sprintf(content, "Welcome to add.com: ");
      sprintf(content, “%sTHE Internet addition portal.\r\n

      ”, content);
      sprintf(content, “%sThe answer is: %d + %d = %d\r\n

      ”,
      content, n1, n2, n1 + n2);
      sprintf(content, “%sThanks for visiting!\r\n”, content);

      /* Generate the HTTP response */
      printf(“Connection: close\r\n”);
      printf(“Content-length: %d\r\n”, (int)strlen(content));
      printf(“Content-type: text/html\r\n\r\n”);
      printf("%s", content);
      fflush(stdout);

      exit(0);
      }
      这段代码就非常简单了,从环境变量中取出请求参数,得到两个加数的值,相加后输出。需要注意的是,由于刚才已经重定向标准输出,因此使用printf就可以把内容输出给客户端。输出内容需要遵照HTTP协议的格式,才能在浏览器中正确显示出来。

    实验效果与源码
    输入如下命令启动Web服务器,并绑定8000端口:

    ./tiny 8000

    静态网页效果:访问http://localhost:8000

    静态网页效果
    动态网页效果:访问http://localhost:8000/cgi-bin/adder?1&2

    动态网页效果
    至此,我们的Web服务器终于大功告成。大家可以下载源码,并在自己的计算机上部署测试。

    关注作者或文集《深入理解计算机系统》,第一时间获取最新发布文章。

    参考资料
    Linux IO操作详解——RIO包 金樽对月的成长脚步
    深入理解HTTP协议 micro36
    CGI与Servlet的比较 YTTCJJ
    我所了解的cgi 掸尘
    Linux内存管理之mmap详解 heavent2010

    文/金戈大王(简书作者)
    原文链接:http://www.jianshu.com/p/dd580395bf11
    著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

    展开全文
  • 本文,我们将使用C语言从零开始实现一个支持静态、动态网页的Web服务器。我们把这个服务器叫做Tiny。背景知识Web服务器使用HTTP协议与客户端(即浏览器)通信,而HTTP协议又基于TCP/IP协议。因此我们要做的工作就是...

    本文,我们将使用C语言从零开始实现一个支持静态、动态网页的Web服务器。我们把这个服务器叫做Tiny。

    背景知识

    Web服务器使用HTTP协议与客户端(即浏览器)通信,而HTTP协议又基于TCP/IP协议。因此我们要做的工作就是利用Linux系统提供的TCP通信接口来实现HTTP协议。

    而Linux为我们提供了哪些网络编程接口呢?没错,就是socket(套接字),我们会在后面详细介绍该接口的使用方式。

    另外我们应该清楚Linux的系统I/O和文件系统的关系。在Linux中,所有I/O设备都被看作一个个文件,I/O设备的输入输出被认做读写文件。网络作为一种I/O设备,同样被看作文件,而且是一类特殊的文件,即套接字文件。

    我们还要对网络通信协议TCP/IP有一个大致的了解,知道IP地址和端口的作用。

    接下来我们讲解客户端-服务器编程模型。

    客户端-服务器编程模型

    客户端-服务器编程模型是一个典型的进程间通信模型。客户端进程和服务器进程通常分处两个不同的主机,如下图所示,客户端发送请求给服务器,服务器从本地资源库中查找需要的资源,然后发送响应给客户端,最后客户端(通常是浏览器)处理这个响应,把结果显示在浏览器上。

    85b17115b5351c597f7c187a562f5b6e.png

    这个过程看起来很简单,但是我们需要深入具体的实现细节。我们知道,TCP是基于连接的,需要先建立连接才能互相通信。在Linux中,socket为我们提供了方便的解决方案。

    每一对网络连接称为一个socket对,包括两个端点的socket地址,表示如下

    (cliaddr : cliport, servaddr : servport)

    其中, cliaddrcliport分别是客户端IP地址和客户端端口号,servaddrservport分别是服务器IP地址和服务器端口。举例说明如下:

    b38d56c363e465cfceb3727357b0ac5a.png

    这对地址和端口唯一确定了连接的双方,在TCP/IP协议网络中就能轻松地找到对方。

    使用socket处理请求与响应

    熟悉TCP协议的朋友们应该很容易理解下面的流程图。

    825835f1308784018342b26952ead082.png

    服务器调用socket函数获取一个socket,然后调用bind函数绑定本机的IP地址和端口,再调用listen函数开启监听,最后调用accept函数等待直到有客户端发起连接。

    另一方面,客户端调用socket函数获取一个socket,然后调用connect函数向指定服务器发起连接请求,当连接成功或出现错误后返回。若连接成功,服务器端的accept函数也会成功返回,返回另一个已连接的socket(不是最初调用socket函数得到的socket),该socket可以直接用于与客户端通信。而服务器最初的那个socket可以继续循环调用accept函数,等待下一次连接的到来。

    连接成功后,无论是客户端还是服务器,只要向socket读写数据就可以实现与对方socket的通信。图中rio_readlinebrio_written是作者封装的I/O读写函数,与Linux系统提供的readwrite作用基本相同,详细介绍见参考资料。

    客户端关闭连接时会发送一个EOF到服务器,服务器读取后关闭连接,进入下一个循环。

    这里面用到的所有Linux网络编程接口都定义在<sys/socket.h>头文件中,为了更清晰地帮助大家理解每个函数的使用方法,我们列出它们的函数声明。

    #include <sys/types.h>
    #include <sys/socket.h>
    
    /**
    获取一个socket descriptor
    @params:
        domain: 此处固定使用AF_INET
        type: 此处固定使用SOCK_STREAM
        protocol: 此处固定使用0
    @returns:
        nonnegative descriptor if OK, -1 on error.
    */
    int socket(int domain, int type, int protocol);
    
    /**
    客户端socket向服务器发起连接
    @params:
        sockfd: 发起连接的socket descriptor
        serv_addr: 连接的目标地址和端口
        addrlen: sizeof(*serv_addr)
    @returns:
        0 if OK, -1 on error
    */
    int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
    
    /**
    服务器socket绑定地址和端口
    @params:
        sockfd: 当前socket descriptor
        my_addr: 指定绑定的本机地址和端口
        addrlen: sizeof(*my_addr)
    @returns:
        0 if OK, -1 on error
    */
    int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
    
    /**
    将当前socket转变为可以监听外部连接请求的socket
    @params:
        sockfd: 当前socket descriptor
        backlog: 请求队列的最大长度
    @returns:
        0 if OK, -1 on error
    */
    int listen(int sockfd, int backlog);
    
    /**
    等待客户端请求到达,注意,成功返回得到的是一个新的socket descriptor,
    而不是输入参数listenfd。
    @params:
        listenfd: 当前正在用于监听的socket descriptor
        addr: 客户端请求地址(输出参数)
        addrlen: 客户端请求地址的长度(输出参数)
    @returns:
        成功则返回一个非负的connected descriptor,出错则返回-1
    */
    int accept(int listenfd, struct sockaddr *addr, int *addrlen);

    HTTP协议与静/动态网页

    HTTP协议的具体内容在此不再讲述,不熟悉的朋友们可以查看参考资料中的第二篇文章。

    现在我们有必要说明一下所谓的静态网页和动态网页。静态网页是指内容固定的网页,通常是事先写好的html文档,每次访问得到的都是相同的内容。而动态网页是指多次访问可以得到不同内容的网页,现在流行的动态网页技术有JavaScript、PHP、JSP、ASP等。我们将要实现的服务器同时支持静态网页和动态网页,但动态网页并不采用上述几种技术实现,而是使用早期流行的CGI(Common Gateway Interface)。CGI是一种动态网页标准,规定了外部应用程序(CGI程序)如何与Web服务器交换信息,但由于有许多缺点,现在几乎已经被淘汰。不过,由于历史原因,本文介绍的服务器底层仍然使用CGI实现。

    关键代码解析

    Web服务器主进程从main函数开始,代码如下。

    int main(int argc, char **argv) 
    {
        int listenfd, connfd;
        socklen_t clientlen;
        struct sockaddr_storage clientaddr;
    
        /* Check command line args */
        if (argc != 2) {
            fprintf(stderr, "usage: %s <port>n", argv[0]);
            exit(1);
        }
    
        listenfd = Open_listenfd(argv[1]);
        while (1) {
            clientlen = sizeof(clientaddr);
            connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
            doit(connfd);
            Close(connfd);
        }
    }

    主函数参数需要传入服务器绑定的端口号码,得到这个号码后,调用Open_listenfd函数,该函数完成socketbindlisten等一系列操作。接着调用accept函数等待客户端请求。注意,Acceptaccept的包装函数,用来自动处理可能发生的异常,我们只需把它们当成一样的就行了。当accept成功返回后,我们拿到了connected socket descriptor,然后调用doit函数处理请求。

    doit函数定义如下。

    void doit(int fd) 
    {
        int is_static;
        struct stat sbuf;
        char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
        char filename[MAXLINE], cgiargs[MAXLINE];
        rio_t rio;
    
        /* Read request line and headers */
        Rio_readinitb(&rio, fd);
        if (!Rio_readlineb(&rio, buf, MAXLINE))
            return;
        printf("%s", buf);
        sscanf(buf, "%s %s %s", method, uri, version);
        if (strcasecmp(method, "GET")) {
            clienterror(fd, method, "501", "Not Implemented",
                        "Tiny does not implement this method");
            return;
        }
        read_requesthdrs(&rio);
    
        /* Parse URI from GET request */
        is_static = parse_uri(uri, filename, cgiargs);
        if (stat(filename, &sbuf) < 0) {
        clienterror(fd, filename, "404", "Not found",
                "Tiny couldn't find this file");
        return;
        }
    
        if (is_static) { /* Serve static content */          
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
            clienterror(fd, filename, "403", "Forbidden",
                "Tiny couldn't read the file");
            return;
        }
        serve_static(fd, filename, sbuf.st_size);
        }
        else { /* Serve dynamic content */
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { 
            clienterror(fd, filename, "403", "Forbidden",
                "Tiny couldn't run the CGI program");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);
        }
    }

    为了更接近现实,假设现在接收到的HTTP请求如下。该请求的请求头是空的。

    GET /cgi-bin/adder?15000&213 HTTP/1.0

    代码中,Rio_readlinebsscanf负责读入请求行并解析出请求方法、请求URI和版本号。接下来调用parse_uri函数,该函数利用请求uri得到访问的文件名、CGI参数,并返回是否按照静态网页处理。如果是,则调用serve_static函数处理,否则调用serve_dynamic函数处理。

    serve_static函数定义如下。

    void serve_static(int fd, char *filename, int filesize) 
    {
        int srcfd;
        char *srcp, filetype[MAXLINE], buf[MAXBUF];
     
        /* Send response headers to client */
        get_filetype(filename, filetype);
        sprintf(buf, "HTTP/1.0 200 OKrn");
        sprintf(buf, "%sServer: Tiny Web Serverrn", buf);
        sprintf(buf, "%sConnection: closern", buf);
        sprintf(buf, "%sContent-length: %drn", buf, filesize);
        sprintf(buf, "%sContent-type: %srnrn", buf, filetype);
        Rio_writen(fd, buf, strlen(buf));
        printf("Response headers:n");
        printf("%s", buf);
    
        /* Send response body to client */
        srcfd = Open(filename, O_RDONLY, 0);
        srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
        Close(srcfd);
        Rio_writen(fd, srcp, filesize);
        Munmap(srcp, filesize);
    }

    直接看最后几行代码。Open以只读方式打开请求的文件,Mmap将该文件直接读取到虚拟地址空间中的任意位置,然后关闭文件。接下来Rio_written把内存中的文件写入fd指定的connected socket descriptor,静态页面响应完成。Munmap删除刚才在虚拟地址空间申请的内存。关于mmap函数的更多介绍见参考资料。

    serve_dynamic函数定义如下。

    void serve_dynamic(int fd, char *filename, char *cgiargs) 
    {
        char buf[MAXLINE], *emptylist[] = { NULL };
    
        /* Return first part of HTTP response */
        sprintf(buf, "HTTP/1.0 200 OKrn"); 
        Rio_writen(fd, buf, strlen(buf));
        sprintf(buf, "Server: Tiny Web Serverrn");
        Rio_writen(fd, buf, strlen(buf));
      
        if (Fork() == 0) { /* Child */
        /* Real server would set all CGI vars here */
        setenv("QUERY_STRING", cgiargs, 1);
        Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */
        Execve(filename, emptylist, environ); /* Run CGI program */
        }
        Wait(NULL); /* Parent waits for and reaps child */
    }

    对于动态网页请求,我们的方法是创建一个子进程,在子进程中执行CGI程序。看代码,Fork函数创建子进程,熟悉Linux进程的朋友们应该知道,该函数会返回两次,一次在父进程中返回,返回值不等于0,另一次在子进程中返回,返回值为0,因此if判断内部是子进程执行的代码。首先设置环境变量,用于把请求参数传递给CGI程序。接下来调用Dup2函数将标准输出重定向到connected socket descriptor,这样一来使用标准输出输出的内容将会直接发送给客户端。然后调用Execve函数在子进程中执行filename指定的CGI程序。最后在父进程中调用了Wait函数用于收割子进程,当子进程终止后该函数才会返回。因此该Web服务器不能同时处理多个访问,只能一个一个处理。

    我们给出了一个CGI程序的实例adder,用于计算两个参数之和。代码如下。

    /*
     * adder.c - a minimal CGI program that adds two numbers together
     */
    int main(void) {
        char *buf, *p;
        char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
        int n1=0, n2=0;
    
        /* Extract the two arguments */
        if ((buf = getenv("QUERY_STRING")) != NULL) {
        p = strchr(buf, '&');
        *p = '0';
        strcpy(arg1, buf);
        strcpy(arg2, p+1);
        n1 = atoi(arg1);
        n2 = atoi(arg2);
        }
    
        /* Make the response body */
        sprintf(content, "Welcome to add.com: ");
        sprintf(content, "%sTHE Internet addition portal.rn<p>", content);
        sprintf(content, "%sThe answer is: %d + %d = %drn<p>", 
            content, n1, n2, n1 + n2);
        sprintf(content, "%sThanks for visiting!rn", content);
      
        /* Generate the HTTP response */
        printf("Connection: closern");
        printf("Content-length: %drn", (int)strlen(content));
        printf("Content-type: text/htmlrnrn");
        printf("%s", content);
        fflush(stdout);
    
        exit(0);
    }

    这段代码就非常简单了,从环境变量中取出请求参数,得到两个加数的值,相加后输出。需要注意的是,由于刚才已经重定向标准输出,因此使用printf就可以把内容输出给客户端。输出内容需要遵照HTTP协议的格式,才能在浏览器中正确显示出来。

    实验效果与源码

    输入如下命令启动Web服务器,并绑定8000端口:

    ./tiny 8000

    静态网页效果:访问http://localhost:8000

    789e20f8e0886b510745a3d7cb21231c.png

    动态网页效果:访问http://localhost:8000/cgi-bin/adder?1&2

    9d1c75ebd884dc3900b72f2deff20a6b.png

    至此,我们的Web服务器终于大功告成。大家可以下载源码,并在自己的计算机上部署测试。

    参考资料

    Linux IO操作详解——RIO包 金樽对月的成长脚步
    深入理解HTTP协议 micro36
    CGI与Servlet的比较 YTTCJJ
    我所了解的cgi 掸尘
    Linux内存管理之mmap详解 heavent2010

    展开全文
  • c语言实现web服务器

    2019-11-11 23:13:45
    #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<string.h> #include<sys/epoll.h> #include<arpa/inet.h>...#i...
    #include <stdio.h>
    
    #include <unistd.h>
    
    #include <stdlib.h>
    
    #include <sys/types.h>
    
    #include <string.h>
    
    #include <sys/epoll.h>
    
    #include <arpa/inet.h>
    
    #include <fcntl.h>
    
    #include <dirent.h>
    
    #include <sys/stat.h>
    
    #include <ctype.h>
    
    
    
    #define MAXSIZE 2000
    
    
    
    void epoll_run(int port)
    
    {
    
        // 创建一个epoll树的根节点
    
        int epfd = epoll_create(MAXSIZE);
    
        if(epfd == -1)
    
        {
    
            perror("epoll_create error");
    
            exit(1);
    
        }
    
    
    
        // 添加要监听的节点
    
        // 先添加监听lfd
    
        int lfd = init_listen_fd(port, epfd);
    
    
    
        // 委托内核检测添加到树上的节点
    
        struct epoll_event all[MAXSIZE];
    
        while(1)
    
        {
    
            int ret = epoll_wait(epfd, all, MAXSIZE, -1);
    
            if(ret == -1)
    
            {
    
                perror("epoll_wait error");
    
                exit(1);
    
            }
    
    
    
            // 遍历发生变化的节点
    
            for(int i=0; i<ret; ++i)
    
            {
    
                // 只处理读事件, 其他事件默认不处理
    
                struct epoll_event *pev = &all[i];
    
                if(!(pev->events & EPOLLIN))
    
                {
    
                    // 不是读事件
    
                    continue;
    
                }
    
    
    
                if(pev->data.fd == lfd)
    
                {
    
                    // 接受连接请求
    
                    do_accept(lfd, epfd);
    
                }
    
                else
    
                {
    
                    // 读数据
    
                    do_read(pev->data.fd, epfd);
    
                }
    
            }
    
        }
    
    }
    
    
    
    // 读数据
    
    void do_read(int cfd, int epfd)
    
    {
    
        // 将浏览器发过来的数据, 读到buf中 
    
        char line[1024] = {0};
    
        // 读请求行
    
        int len = get_line(cfd, line, sizeof(line));
    
        if(len == 0)
    
        {
    
            printf("客户端断开了连接...\n");
    
            // 关闭套接字, cfd从epoll上del
    
            disconnect(cfd, epfd);         
    
        }
    
        else
    
        {
    
            printf("请求行数据: %s", line);
    
            printf("============= 请求头 ============\n");
    
            // 还有数据没读完
    
            // 继续读
    
            while(len)
    
            {
    
                char buf[1024] = {0};
    
                len = get_line(cfd, buf, sizeof(buf));
    
                printf("-----: %s", buf);
    
            }
    
            printf("============= The End ============\n");
    
        }
    
    
    
        // 请求行: get /xxx http/1.1
    
        // 判断是不是get请求
    
        if(strncasecmp("get", line, 3) == 0)
    
        {
    
            // 处理http请求
    
            http_request(line, cfd);
    
            // 关闭套接字, cfd从epoll上del
    
            disconnect(cfd, epfd);         
    
        }
    
    }
    
    
    
    // 断开连接的函数
    
    void disconnect(int cfd, int epfd)
    
    {
    
        int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
    
        if(ret == -1)
    
        {
    
            perror("epoll_ctl del cfd error");
    
            exit(1);
    
        }
    
        close(cfd);
    
    }
    
    
    
    // http请求处理
    
    void http_request(const char* request, int cfd)
    
    {
    
        // 拆分http请求行
    
        // get /xxx http/1.1
    
        char method[12], path[1024], protocol[12];
    
        sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
    
    
    
        printf("method = %s, path = %s, protocol = %s\n", method, path, protocol);
    
    
    
        // 转码 将不能识别的中文乱码 - > 中文
    
        // 解码 %23 %34 %5f
    
        decode_str(path, path);
    
            // 处理path  /xx
    
            // 去掉path中的/
    
            char* file = path+1;
    
        // 如果没有指定访问的资源, 默认显示资源目录中的内容
    
        if(strcmp(path, "/") == 0)
    
        {
    
            // file的值, 资源目录的当前位置
    
            file = "./";
    
        }
    
    
    
        // 获取文件属性
    
        struct stat st;
    
        int ret = stat(file, &st);
    
        if(ret == -1)
    
        {
    
            // show 404
    
            send_respond_head(cfd, 404, "File Not Found", ".html", -1);
    
            send_file(cfd, "404.html");
    
        }
    
    
    
        // 判断是目录还是文件
    
        // 如果是目录
    
        if(S_ISDIR(st.st_mode))
    
        {
    
            // 发送头信息
    
            send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1);
    
            // 发送目录信息
    
            send_dir(cfd, file);
    
        }
    
        else if(S_ISREG(st.st_mode))
    
        {
    
            // 文件
    
            // 发送消息报头
    
            send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size);
    
            // 发送文件内容
    
            send_file(cfd, file);
    
        }
    
    }
    
    
    
    // 发送目录内容
    
    void send_dir(int cfd, const char* dirname)
    
    {
    
        // 拼一个html页面<table></table>
    
        char buf[4094] = {0};
    
    
    
        sprintf(buf, "<html><head><title>目录名: %s</title></head>", dirname);
    
        sprintf(buf+strlen(buf), "<body><h1>当前目录: %s</h1><table>", dirname);
    
    
    
        char enstr[1024] = {0};
    
        char path[1024] = {0};
    
        // 目录项二级指针
    
        struct dirent** ptr;
    
        int num = scandir(dirname, &ptr, NULL, alphasort);
    
        // 遍历
    
        for(int i=0; i<num; ++i)
    
        {
    
            char* name = ptr[i]->d_name;
    
    
    
            // 拼接文件的完整路径
    
            sprintf(path, "%s/%s", dirname, name);
    
            printf("path = %s ===================\n", path);
    
            struct stat st;
    
            stat(path, &st);
    
    
    
            encode_str(enstr, sizeof(enstr), name);
    
            // 如果是文件
    
            if(S_ISREG(st.st_mode))
    
            {
    
                sprintf(buf+strlen(buf), 
    
                        "<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
    
                        enstr, name, (long)st.st_size);
    
            }
    
            // 如果是目录
    
            else if(S_ISDIR(st.st_mode))
    
            {
    
                sprintf(buf+strlen(buf), 
    
                        "<tr><td><a href=\"%s/\">%s/</a></td><td>%ld</td></tr>",
    
                        enstr, name, (long)st.st_size);
    
            }
    
            send(cfd, buf, strlen(buf), 0);
    
            memset(buf, 0, sizeof(buf));
    
            // 字符串拼接
    
        }
    
    
    
        sprintf(buf+strlen(buf), "</table></body></html>");
    
        send(cfd, buf, strlen(buf), 0);
    
    
    
        printf("dir message send OK!!!!\n");
    
    #if 0
    
        // 打开目录
    
        DIR* dir = opendir(dirname);
    
        if(dir == NULL)
    
        {
    
            perror("opendir error");
    
            exit(1);
    
        }
    
    
    
        // 读目录
    
        struct dirent* ptr = NULL;
    
        while( (ptr = readdir(dir)) != NULL )
    
        {
    
            char* name = ptr->d_name;
    
        }
    
        closedir(dir);
    
    #endif
    
    }
    
    
    
    // 发送响应头
    
    void send_respond_head(int cfd, int no, const char* desp, const char* type, long len)
    
    {
    
        char buf[1024] = {0};
    
        // 状态行
    
        sprintf(buf, "http/1.1 %d %s\r\n", no, desp);
    
        send(cfd, buf, strlen(buf), 0);
    
        // 消息报头
    
        sprintf(buf, "Content-Type:%s\r\n", type);
    
        sprintf(buf+strlen(buf), "Content-Length:%ld\r\n", len);
    
        send(cfd, buf, strlen(buf), 0);
    
        // 空行
    
        send(cfd, "\r\n", 2, 0);
    
    }
    
    
    
    // 发送文件
    
    void send_file(int cfd, const char* filename)
    
    {
    
        // 打开文件
    
        int fd = open(filename, O_RDONLY);
    
        if(fd == -1)
    
        {
    
            // show 404
    
            return;
    
        }
    
    
    
        // 循环读文件
    
        char buf[4096] = {0};
    
        int len = 0;
    
        while( (len = read(fd, buf, sizeof(buf))) > 0 )
    
        {
    
            // 发送读出的数据
    
            send(cfd, buf, len, 0);
    
        }
    
        if(len == -1)
    
        {
    
            perror("read file error");
    
            exit(1);
    
        }
    
    
    
        close(fd);
    
    }
    
    
    
    // 解析http请求消息的每一行内容
    
    int get_line(int sock, char *buf, int size)
    
    {
    
        int i = 0;
    
        char c = '\0';
    
        int n;
    
        while ((i < size - 1) && (c != '\n'))
    
        {
    
            n = recv(sock, &c, 1, 0);
    
            if (n > 0)
    
            {
    
                if (c == '\r')
    
                {
    
                    n = recv(sock, &c, 1, MSG_PEEK);
    
                    if ((n > 0) && (c == '\n'))
    
                    {
    
                        recv(sock, &c, 1, 0);
    
                    }
    
                    else
    
                    {
    
                        c = '\n';
    
                    }
    
                }
    
                buf[i] = c;
    
                i++;
    
            }
    
            else
    
            {
    
                c = '\n';
    
            }
    
        }
    
        buf[i] = '\0';
    
    
    
        return i;
    
    }
    
    
    
    // 接受新连接处理
    
    void do_accept(int lfd, int epfd)
    
    {
    
        struct sockaddr_in client;
    
        socklen_t len = sizeof(client);
    
        int cfd = accept(lfd, (struct sockaddr*)&client, &len);
    
        if(cfd == -1)
    
        {
    
            perror("accept error");
    
            exit(1);
    
        }
    
    
    
        // 打印客户端信息
    
        char ip[64] = {0};
    
        printf("New Client IP: %s, Port: %d, cfd = %d\n",
    
               inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)),
    
               ntohs(client.sin_port), cfd);
    
    
    
        // 设置cfd为非阻塞
    
        int flag = fcntl(cfd, F_GETFL);
    
        flag |= O_NONBLOCK;
    
        fcntl(cfd, F_SETFL, flag);
    
    
    
        // 得到的新节点挂到epoll树上
    
        struct epoll_event ev;
    
        ev.data.fd = cfd;
    
        // 边沿非阻塞模式
    
        ev.events = EPOLLIN | EPOLLET;
    
        int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    
        if(ret == -1)
    
        {
    
            perror("epoll_ctl add cfd error");
    
            exit(1);
    
        }
    
    }
    
    
    
    int init_listen_fd(int port, int epfd)
    
    {
    
        // 创建监听的套接字
    
        int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
        if(lfd == -1)
    
        {
    
            perror("socket error");
    
            exit(1);
    
        }
    
    
    
        // lfd绑定本地IP和port
    
        struct sockaddr_in serv;
    
        memset(&serv, 0, sizeof(serv));
    
        serv.sin_family = AF_INET;
    
        serv.sin_port = htons(port);
    
        serv.sin_addr.s_addr = htonl(INADDR_ANY);
    
    
    
        // 端口复用
    
        int flag = 1;
    
        setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    
        int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv));
    
        if(ret == -1)
    
        {
    
            perror("bind error");
    
            exit(1);
    
        }
    
    
    
        // 设置监听
    
        ret = listen(lfd, 64);
    
        if(ret == -1)
    
        {
    
            perror("listen error");
    
            exit(1);
    
        }
    
    
    
        // lfd添加到epoll树上
    
        struct epoll_event ev;
    
        ev.events = EPOLLIN;
    
        ev.data.fd = lfd;
    
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    
        if(ret == -1)
    
        {
    
            perror("epoll_ctl add lfd error");
    
            exit(1);
    
        }
    
    
    
        return lfd;
    
    }
    
    
    
    // 16进制数转化为10进制
    
    int hexit(char c)
    
    {
    
        if (c >= '0' && c <= '9')
    
            return c - '0';
    
        if (c >= 'a' && c <= 'f')
    
            return c - 'a' + 10;
    
        if (c >= 'A' && c <= 'F')
    
            return c - 'A' + 10;
    
    
    
        return 0;
    
    }
    
    
    
    /*
    
     *  这里的内容是处理%20之类的东西!是"解码"过程。
    
     *  %20 URL编码中的‘ ’(space)
    
     *  %21 '!' %22 '"' %23 '#' %24 '$'
    
     *  %25 '%' %26 '&' %27 ''' %28 '('......
    
     *  相关知识html中的‘ ’(space)是&nbsp
    
     */
    
    void encode_str(char* to, int tosize, const char* from)
    
    {
    
        int tolen;
    
    
    
        for (tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from) 
    
        {
    
            if (isalnum(*from) || strchr("/_.-~", *from) != (char*)0) 
    
            {
    
                *to = *from;
    
                ++to;
    
                ++tolen;
    
            } 
    
            else 
    
            {
    
                sprintf(to, "%%%02x", (int) *from & 0xff);
    
                to += 3;
    
                tolen += 3;
    
            }
    
    
    
        }
    
        *to = '\0';
    
    }
    
    
    
    
    void decode_str(char *to, char *from)
    
    {
    
        for ( ; *from != '\0'; ++to, ++from  ) 
    
        {
    
            if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) 
    
            { 
    
    
    
                *to = hexit(from[1])*16 + hexit(from[2]);
    
    
    
                from += 2;                      
    
            } 
    
            else
    
            {
    
                *to = *from;
    
    
    
            }
    
    
    
        }
    
        *to = '\0';
    
    
    
    }
    
    
    
    // 通过文件名获取文件的类型
    
    const char *get_file_type(const char *name)
    
    {
    
        char* dot;
    
    
    
        // 自右向左查找‘.’字符, 如不存在返回NULL
    
        dot = strrchr(name, '.');   
    
        if (dot == NULL)
    
            return "text/plain; charset=utf-8";
    
        if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
    
            return "text/html; charset=utf-8";
    
        if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
    
            return "image/jpeg";
    
        if (strcmp(dot, ".gif") == 0)
    
            return "image/gif";
    
        if (strcmp(dot, ".png") == 0)
    
            return "image/png";
    
        if (strcmp(dot, ".css") == 0)
    
            return "text/css";
    
        if (strcmp(dot, ".au") == 0)
    
            return "audio/basic";
    
        if (strcmp( dot, ".wav" ) == 0)
    
            return "audio/wav";
    
        if (strcmp(dot, ".avi") == 0)
    
            return "video/x-msvideo";
    
        if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
    
            return "video/quicktime";
    
        if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
    
            return "video/mpeg";
    
        if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
    
            return "model/vrml";
    
        if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
    
            return "audio/midi";
    
        if (strcmp(dot, ".mp3") == 0)
    
            return "audio/mpeg";
    
        if (strcmp(dot, ".ogg") == 0)
    
            return "application/ogg";
    
        if (strcmp(dot, ".pac") == 0)
    
            return "application/x-ns-proxy-autoconfig";
    
    
    
        return "text/plain; charset=utf-8";
    
    }
    
    int main(int argc, const char* argv[])
    
    {
    
        if(argc < 3)
    
        {
    
            printf("eg: ./a.out port path\n");
    
            exit(1);
    
        }
    
    
    
        // 端口
    
        int port = atoi(argv[1]);
    
        // 修改进程的工作目录, 方便后续操作
    
        int ret = chdir(argv[2]);
    
        if(ret == -1)
    
        {
    
            perror("chdir error");
    
            exit(1);
    
        }
    
        
    
        // 启动epoll模型 
    
        epoll_run(port);
    
    
    
        return 0;
    
    }

     

    展开全文
  • 本文,我们将使用C语言从零开始实现一个支持静态、动态网页的Web服务器。我们把这个服务器叫做Tiny。背景知识Web服务器使用HTTP协议与客户端(即浏览器)通信,而HTTP协议又基于TCP/IP协议。因此我们要做的工作就是...

    本文,我们将使用C语言从零开始实现一个支持静态、动态网页的Web服务器。我们把这个服务器叫做Tiny。

    背景知识

    Web服务器使用HTTP协议与客户端(即浏览器)通信,而HTTP协议又基于TCP/IP协议。因此我们要做的工作就是利用Linux系统提供的TCP通信接口来实现HTTP协议。

    而Linux为我们提供了哪些网络编程接口呢?没错,就是socket(套接字),我们会在后面详细介绍该接口的使用方式。

    另外我们应该清楚Linux的系统I/O和文件系统的关系。在Linux中,所有I/O设备都被看作一个个文件,I/O设备的输入输出被认做读写文件。网络作为一种I/O设备,同样被看作文件,而且是一类特殊的文件,即套接字文件。

    我们还要对网络通信协议TCP/IP有一个大致的了解,知道IP地址和端口的作用。

    接下来我们讲解客户端-服务器编程模型。

    客户端-服务器编程模型

    客户端-服务器编程模型是一个典型的进程间通信模型。客户端进程和服务器进程通常分处两个不同的主机,如下图所示,客户端发送请求给服务器,服务器从本地资源库中查找需要的资源,然后发送响应给客户端,最后客户端(通常是浏览器)处理这个响应,把结果显示在浏览器上。

    2b389a339dee9c0d4830780a09271e08.png

    这个过程看起来很简单,但是我们需要深入具体的实现细节。我们知道,TCP是基于连接的,需要先建立连接才能互相通信。在Linux中,socket为我们提供了方便的解决方案。

    每一对网络连接称为一个socket对,包括两个端点的socket地址,表示如下

    (cliaddr : cliport, servaddr : servport)

    其中, cliaddrcliport分别是客户端IP地址和客户端端口号,servaddrservport分别是服务器IP地址和服务器端口。举例说明如下:

    4f954381517491f47c5c50e7f569a5a8.png

    这对地址和端口唯一确定了连接的双方,在TCP/IP协议网络中就能轻松地找到对方。

    使用socket处理请求与响应

    熟悉TCP协议的朋友们应该很容易理解下面的流程图。

    625b2e4c36bd911c01a6a5207e477102.png

    服务器调用socket函数获取一个socket,然后调用bind函数绑定本机的IP地址和端口,再调用listen函数开启监听,最后调用accept函数等待直到有客户端发起连接。

    另一方面,客户端调用socket函数获取一个socket,然后调用connect函数向指定服务器发起连接请求,当连接成功或出现错误后返回。若连接成功,服务器端的accept函数也会成功返回,返回另一个已连接的socket(不是最初调用socket函数得到的socket),该socket可以直接用于与客户端通信。而服务器最初的那个socket可以继续循环调用accept函数,等待下一次连接的到来。

    连接成功后,无论是客户端还是服务器,只要向socket读写数据就可以实现与对方socket的通信。图中rio_readlinebrio_written是作者封装的I/O读写函数,与Linux系统提供的readwrite作用基本相同,详细介绍见参考资料。

    客户端关闭连接时会发送一个EOF到服务器,服务器读取后关闭连接,进入下一个循环。

    这里面用到的所有Linux网络编程接口都定义在<sys/socket.h>头文件中,为了更清晰地帮助大家理解每个函数的使用方法,我们列出它们的函数声明。

    #include <sys/types.h>
    #include <sys/socket.h>
    
    /**
    获取一个socket descriptor
    @params:
        domain: 此处固定使用AF_INET
        type: 此处固定使用SOCK_STREAM
        protocol: 此处固定使用0
    @returns:
        nonnegative descriptor if OK, -1 on error.
    */
    int socket(int domain, int type, int protocol);
    
    /**
    客户端socket向服务器发起连接
    @params:
        sockfd: 发起连接的socket descriptor
        serv_addr: 连接的目标地址和端口
        addrlen: sizeof(*serv_addr)
    @returns:
        0 if OK, -1 on error
    */
    int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
    
    /**
    服务器socket绑定地址和端口
    @params:
        sockfd: 当前socket descriptor
        my_addr: 指定绑定的本机地址和端口
        addrlen: sizeof(*my_addr)
    @returns:
        0 if OK, -1 on error
    */
    int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
    
    /**
    将当前socket转变为可以监听外部连接请求的socket
    @params:
        sockfd: 当前socket descriptor
        backlog: 请求队列的最大长度
    @returns:
        0 if OK, -1 on error
    */
    int listen(int sockfd, int backlog);
    
    /**
    等待客户端请求到达,注意,成功返回得到的是一个新的socket descriptor,
    而不是输入参数listenfd。
    @params:
        listenfd: 当前正在用于监听的socket descriptor
        addr: 客户端请求地址(输出参数)
        addrlen: 客户端请求地址的长度(输出参数)
    @returns:
        成功则返回一个非负的connected descriptor,出错则返回-1
    */
    int accept(int listenfd, struct sockaddr *addr, int *addrlen);

    HTTP协议与静/动态网页

    HTTP协议的具体内容在此不再讲述,不熟悉的朋友们可以查看参考资料中的第二篇文章。

    现在我们有必要说明一下所谓的静态网页和动态网页。静态网页是指内容固定的网页,通常是事先写好的html文档,每次访问得到的都是相同的内容。而动态网页是指多次访问可以得到不同内容的网页,现在流行的动态网页技术有JavaScript、PHP、JSP、ASP等。我们将要实现的服务器同时支持静态网页和动态网页,但动态网页并不采用上述几种技术实现,而是使用早期流行的CGI(Common Gateway Interface)。CGI是一种动态网页标准,规定了外部应用程序(CGI程序)如何与Web服务器交换信息,但由于有许多缺点,现在几乎已经被淘汰。不过,由于历史原因,本文介绍的服务器底层仍然使用CGI实现。

    关键代码解析

    Web服务器主进程从main函数开始,代码如下。

    int main(int argc, char **argv) 
    {
        int listenfd, connfd;
        socklen_t clientlen;
        struct sockaddr_storage clientaddr;
    
        /* Check command line args */
        if (argc != 2) {
            fprintf(stderr, "usage: %s <port>n", argv[0]);
            exit(1);
        }
    
        listenfd = Open_listenfd(argv[1]);
        while (1) {
            clientlen = sizeof(clientaddr);
            connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
            doit(connfd);
            Close(connfd);
        }
    }

    主函数参数需要传入服务器绑定的端口号码,得到这个号码后,调用Open_listenfd函数,该函数完成socketbindlisten等一系列操作。接着调用accept函数等待客户端请求。注意,Acceptaccept的包装函数,用来自动处理可能发生的异常,我们只需把它们当成一样的就行了。当accept成功返回后,我们拿到了connected socket descriptor,然后调用doit函数处理请求。

    doit函数定义如下。

    void doit(int fd) 
    {
        int is_static;
        struct stat sbuf;
        char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
        char filename[MAXLINE], cgiargs[MAXLINE];
        rio_t rio;
    
        /* Read request line and headers */
        Rio_readinitb(&rio, fd);
        if (!Rio_readlineb(&rio, buf, MAXLINE))
            return;
        printf("%s", buf);
        sscanf(buf, "%s %s %s", method, uri, version);
        if (strcasecmp(method, "GET")) {
            clienterror(fd, method, "501", "Not Implemented",
                        "Tiny does not implement this method");
            return;
        }
        read_requesthdrs(&rio);
    
        /* Parse URI from GET request */
        is_static = parse_uri(uri, filename, cgiargs);
        if (stat(filename, &sbuf) < 0) {
        clienterror(fd, filename, "404", "Not found",
                "Tiny couldn't find this file");
        return;
        }
    
        if (is_static) { /* Serve static content */          
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
            clienterror(fd, filename, "403", "Forbidden",
                "Tiny couldn't read the file");
            return;
        }
        serve_static(fd, filename, sbuf.st_size);
        }
        else { /* Serve dynamic content */
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { 
            clienterror(fd, filename, "403", "Forbidden",
                "Tiny couldn't run the CGI program");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);
        }
    }

    为了更接近现实,假设现在接收到的HTTP请求如下。该请求的请求头是空的。

    GET /cgi-bin/adder?15000&213 HTTP/1.0

    代码中,Rio_readlinebsscanf负责读入请求行并解析出请求方法、请求URI和版本号。接下来调用parse_uri函数,该函数利用请求uri得到访问的文件名、CGI参数,并返回是否按照静态网页处理。如果是,则调用serve_static函数处理,否则调用serve_dynamic函数处理。

    serve_static函数定义如下。

    void serve_static(int fd, char *filename, int filesize) 
    {
        int srcfd;
        char *srcp, filetype[MAXLINE], buf[MAXBUF];
     
        /* Send response headers to client */
        get_filetype(filename, filetype);
        sprintf(buf, "HTTP/1.0 200 OKrn");
        sprintf(buf, "%sServer: Tiny Web Serverrn", buf);
        sprintf(buf, "%sConnection: closern", buf);
        sprintf(buf, "%sContent-length: %drn", buf, filesize);
        sprintf(buf, "%sContent-type: %srnrn", buf, filetype);
        Rio_writen(fd, buf, strlen(buf));
        printf("Response headers:n");
        printf("%s", buf);
    
        /* Send response body to client */
        srcfd = Open(filename, O_RDONLY, 0);
        srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
        Close(srcfd);
        Rio_writen(fd, srcp, filesize);
        Munmap(srcp, filesize);
    }

    直接看最后几行代码。Open以只读方式打开请求的文件,Mmap将该文件直接读取到虚拟地址空间中的任意位置,然后关闭文件。接下来Rio_written把内存中的文件写入fd指定的connected socket descriptor,静态页面响应完成。Munmap删除刚才在虚拟地址空间申请的内存。关于mmap函数的更多介绍见参考资料。

    serve_dynamic函数定义如下。

    void serve_dynamic(int fd, char *filename, char *cgiargs) 
    {
        char buf[MAXLINE], *emptylist[] = { NULL };
    
        /* Return first part of HTTP response */
        sprintf(buf, "HTTP/1.0 200 OKrn"); 
        Rio_writen(fd, buf, strlen(buf));
        sprintf(buf, "Server: Tiny Web Serverrn");
        Rio_writen(fd, buf, strlen(buf));
      
        if (Fork() == 0) { /* Child */
        /* Real server would set all CGI vars here */
        setenv("QUERY_STRING", cgiargs, 1);
        Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */
        Execve(filename, emptylist, environ); /* Run CGI program */
        }
        Wait(NULL); /* Parent waits for and reaps child */
    }

    对于动态网页请求,我们的方法是创建一个子进程,在子进程中执行CGI程序。看代码,Fork函数创建子进程,熟悉Linux进程的朋友们应该知道,该函数会返回两次,一次在父进程中返回,返回值不等于0,另一次在子进程中返回,返回值为0,因此if判断内部是子进程执行的代码。首先设置环境变量,用于把请求参数传递给CGI程序。接下来调用Dup2函数将标准输出重定向到connected socket descriptor,这样一来使用标准输出输出的内容将会直接发送给客户端。然后调用Execve函数在子进程中执行filename指定的CGI程序。最后在父进程中调用了Wait函数用于收割子进程,当子进程终止后该函数才会返回。因此该Web服务器不能同时处理多个访问,只能一个一个处理。

    我们给出了一个CGI程序的实例adder,用于计算两个参数之和。代码如下。

    /*
     * adder.c - a minimal CGI program that adds two numbers together
     */
    int main(void) {
        char *buf, *p;
        char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
        int n1=0, n2=0;
    
        /* Extract the two arguments */
        if ((buf = getenv("QUERY_STRING")) != NULL) {
        p = strchr(buf, '&');
        *p = '0';
        strcpy(arg1, buf);
        strcpy(arg2, p+1);
        n1 = atoi(arg1);
        n2 = atoi(arg2);
        }
    
        /* Make the response body */
        sprintf(content, "Welcome to add.com: ");
        sprintf(content, "%sTHE Internet addition portal.rn<p>", content);
        sprintf(content, "%sThe answer is: %d + %d = %drn<p>", 
            content, n1, n2, n1 + n2);
        sprintf(content, "%sThanks for visiting!rn", content);
      
        /* Generate the HTTP response */
        printf("Connection: closern");
        printf("Content-length: %drn", (int)strlen(content));
        printf("Content-type: text/htmlrnrn");
        printf("%s", content);
        fflush(stdout);
    
        exit(0);
    }

    这段代码就非常简单了,从环境变量中取出请求参数,得到两个加数的值,相加后输出。需要注意的是,由于刚才已经重定向标准输出,因此使用printf就可以把内容输出给客户端。输出内容需要遵照HTTP协议的格式,才能在浏览器中正确显示出来。

    实验效果与源码

    输入如下命令启动Web服务器,并绑定8000端口:

    ./tiny 8000

    静态网页效果:访问http://localhost:8000

    3e43a2689de8cf1dd8a5e9f62f8a0cec.png

    动态网页效果:访问http://localhost:8000/cgi-bin/adder?1&2

    ac3ed6873dacaee68d8d08e37a77e960.png

    至此,我们的Web服务器终于大功告成。大家可以下载源码,并在自己的计算机上部署测试。

    参考资料

    Linux IO操作详解——RIO包 金樽对月的成长脚步
    深入理解HTTP协议 micro36
    CGI与Servlet的比较 YTTCJJ
    我所了解的cgi 掸尘
    Linux内存管理之mmap详解 heavent2010

    展开全文
  • 个人博客主页 https://www.boolo.top C语言实现简单的Web服务器。之前的文章利用C语言实现了基于TCP的WinSock套接字编程。基于此,同样可以使用C语言来实现简单的Web服务器
  • C语言实现的Web服务器

    2020-06-16 18:09:58
    标准C实现WEB服务器 http://blog.sina.com.cn/s/blog_4b73e7600100b02c.html 本文原文地址: http://blog.sina.com.cn/s/blog_4b73e760010007id.html 自己研究了好几天终于写出来一个,哈哈,当然也从网上得到了很...
  • c语言实现简单web服务器

    千次阅读 2018-01-11 17:26:54
    host主机地址:port端口/urlhost会被DNS服务器 解析成IP地址,所以有时候可以直接用域名,http默认访问80端口,https默认访问443端口大致流程就是:浏览器输入地址后,首先和web服务器建立tcp连接,然后浏览器发送http请求...
  • } 步骤: 浏览器(客户端)访问服务器时发送的是这些内容: GET /index.html HTTP/1.1 Host: 127.0.0.1:50000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20131029 Firefox/17.0 Accept: text/html...
  • C语言实现简单的web服务器
  • 附:需要了解C语言socket,进程,线程,以及http通信基本原理。 需要学习了解CGI,想进一步学习的可了解apache和ngxin服务器。 参考:CGI与FastCGI区别-》http://www.cnblogs.com/wanghetao/p/3934350.html ...
  • linux平台下使用标准C语言编写的Web服务器,代码简单易懂,适合新手.do_trans.c为完成请求主要实现代码,servers.c为shock建立实现代码,默认端口6666,网页根目录/home/wwwroot
  • C语言搭建的简单web服务器

    热门讨论 2010-09-15 13:49:18
    用纯C语言编写的简单WEB服务器,实现网页的访问,和简单CGI功能。
  • 有没有人用C 语言实现动态网站啊???
  • <html><head>C语言构建小型Web服务器</title></head><body><h2>欢迎</h2><p>Hello,World</p></body> " ; write(client_socket_fp, status, sizeof (status)); write(client_socket_fp, header, sizeof (header...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,516
精华内容 606
关键字:

c语言web服务器

c语言 订阅