某某茶叶有限公司欢迎您!
金沙棋牌在线 > 服务器&运维 > 自己实现简单Web服务器,支持GET POST请求

自己实现简单Web服务器,支持GET POST请求

时间:2020-02-15 06:59

这个主要是在CSAPP基础上做的,添加了POST,SSL,目录显示等功能。

最近项目上遇到一个需求,最后想到的解决方案是自己实现一个web服务器去处理请求,然后再将信息发送到另外一个程序。然后返回处理之后的结果呈现出来。

本文实例讲述了Go语言服务器开发实现最简单HTTP的GET与POST接口。分享给大家供大家参考。具体分析如下:

阅读经典——《深入理解计算机系统》09

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

  1. 背景知识
  2. 客户端-服务器编程模型
  3. 使用socket处理请求与响应
  4. HTTP协议与静/动态网页
  5. 关键代码解析
  6. 实验效果与源码

一、 实现功能:

1.支持GET/POST方法

2.支持SSL安全连接即HTTPS

3.支持CGI

4.基于IP地址和掩码的认证

5.目录显示

6.日志功能

7.错误提示页面

github地址:

源代码下载地址:点击打开链接

现在我就来分享一下如何实现的。

Go语言提供了http包,可以很轻松的开发http接口。以下为示例代码:

背景知识

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

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

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

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

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

二、设计原理

首先介绍一些HTTP协议基本知识。

#1.GET/POST

本实现支持GET/POST方法,都是HTTP协议需要支持的标准方法。

GET方法主要是通过URL发送请求和传送数据,而POST方法在请求头空一格之后传送数据,所以POST方法比GET方法安全性高,因为GET方法可以直接看到传送的数据。另外一个区别就是GET方法传输的数据较小,而POST方法很大。所以一般表单,登陆页面等都是通过POST方法。

#2.MIME类型

当服务器获取客户端的请求的文件名,将分析文件的MIME类型,然后告诉浏览器改文件的MIME类型,浏览器通过MIME类型解析传送过来的数据。具体来说,浏览器请求一个主页面,该页面是一个HTML文件,那么服务器将”text/html”类型发给浏览器,浏览器通过HTML解析器识别发送过来的内容并显示。

下面将描述一个具体情景。

客户端使用浏览器通过URL发送请求,服务器获取请求。

如浏览器URL为:127.0.0.1/postAuth.html,

那么服务器获取到的请求为:GET  /postAuth.html  HTTP/1.1

意思是需要根目录下postAuth.html文件的内容,通过GET方法,使用HTTP/1.1协议(1.1是HTTP的版本号)。这是服务器将分析文件名,得知postAuth.html是一个HTML文件,所以将”text/html”发送给浏览器,然后读取postAuth.html内容发给浏览器。

实现简单的MIME类型识别代码如下:

主要就是通过文件后缀获取文件类型。

static void get_filetype(const char *filename, char *filetype)   
{  
    if (strstr(filename, ".html"))  
        strcpy(filetype, "text/html");  
    else if (strstr(filename, ".gif"))  
        strcpy(filetype, "image/gif");  
    else if (strstr(filename, ".jpg"))  
        strcpy(filetype, "image/jpeg");  
    else if (strstr(filename, ".png"))  
        strcpy(filetype, "image/png");  
    else  
    strcpy(filetype, "text/plain");  
}

如果支持HTTPS的话,那么我们就#define HTTPS,这主要通过gcc 的D选项实现的,具体细节可参考man手册。

静态内容显示实现如下:

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, "%sContent-length: %drn", buf, filesize);
    sprintf(buf, "%sContent-type: %srnrn", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

#3.CGI规范

如果只能显示页面那么无疑缺少动态交互能力,于是CGI产生了。CGI是公共网关接口(Common Gateway Interface),是在CGI程序和Web服务器之间传递信息的规则。CGI允许Web服务器执行外部程序,并将它们的输出发送给浏览器。这样就提供了动态交互能力。

那么服务器是如何分开处理静态页面和动态CGI程序的呢?这主要是通过解析URL的方式。我们可以定义CGI程序的目录,如cgi-bin,那么如果URL包含”cgi-bin”字符串则这是动态程序,且将URL的参数给cgiargs。如果是静态页面,parse_uri返回1,反正返回0。所以我们可以通过返回值区别不同的服务类型。

具体解析URL方式如下:

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, "%sContent-length: %drn", buf, filesize);
    sprintf(buf, "%sContent-type: %srnrn", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

GET方式的CGI规范实现原理:

服务器通过URL获取传给CGI程序的参数,设置环境变量QUERY_STRING,并将标准输出重定向到文件描述符,然后通过EXEC函数簇执行外部CGI程序。外部CGI程序获取QUERY_STRING并处理,处理完后输出结果。由于此时标准输出已重定向到文件描述符,即发送给了浏览器。

实现细节如下:由于涉及到HTTPS,所以稍微有点复杂。

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, "%sContent-length: %drn", buf, filesize);
    sprintf(buf, "%sContent-type: %srnrn", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

POST方式的CGI规范实现原理:

由于POST方式不是通过URL传递参数,所以实现方式与GET方式不一样。

POST方式获取浏览器发送过来的参数长度设置为环境变量CONTENT-LENGTH。并将参数重定向到CGI的标准输入,这主要通过pipe管道实现的。CGI程序从标准输入读取CONTENT-LENGTH个字符就获取了浏览器传送的参数,并将处理结果输出到标准输出,同理标准输出已重定向到文件描述符,所以浏览器就能收到处理的响应。

具体实现细节如下:

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, "%sContent-length: %drn", buf, filesize);
    sprintf(buf, "%sContent-type: %srnrn", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

目录显示功能原理:

主要是通过URL获取所需目录,然后获取该目录下所有文件,并发送相应信息,包括文件格式对应图片,文件名,文件大小,最后修改时间等。由于我们发送的文件名是通过超链接的形式,所以我们可以点击文件名继续浏览信息。

具体实现细节如下:

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, "%sContent-length: %drn", buf, filesize);
    sprintf(buf, "%sContent-type: %srnrn", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

HTTPS的实现:

HTTPS主要基于openssl的开源库实现。如果没有安装,那么我们就不#define HTTPS。
HTTPS的功能主要就是提供安全的连接,服务器和浏览器之间传送的数据是通过加密的,加密方式可以自己选定。

开始连接时,服务器需要发送CA,由于我们的CA是自己签发的,所以需要我们自己添加为可信。

访问控制功能:

主要是通过获取客户端IP地址,并转换为整数,与上配置文件中定义的掩码,如果符合配置文件中允许的网段,那么可以访问,否则不可以。

具体实现如下。

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, "%sContent-length: %drn", buf, filesize);
    sprintf(buf, "%sContent-type: %srnrn", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

配置文件的读取:

主要选项信息都定义与配置文件中。

格式举例如下;

#HTTP PORT
PORT = 8888

所以读取配置文件函数具体如下:

static char* getconfig(char* name)  
{  
/* 
pointer meaning: 

...port...=...8000... 
   |  |   |   |  | 
  *fs |   |   |  *be    f->forward  b-> back 
      *fe |   *bs       s->start    e-> end 
          *equal 
*/  
    static char info[64];  
    int find=0;  
    char tmp[256],fore[64],back[64],tmpcwd[MAXLINE];  
    char *fs,*fe,*equal,*bs,*be,*start;  

    strcpy(tmpcwd,cwd);  
    strcat(tmpcwd,"/");  
    FILE *fp=getfp(strcat(tmpcwd,"config.ini"));  
    while(fgets(tmp,255,fp)!=NULL)  
    {  
        start=tmp;  
        equal=strchr(tmp,'=');  

        while(isblank(*start))  
            ++start;  
        fs=start;  

        if(*fs=='#')  
            continue;  
        while(isalpha(*start))  
            ++start;  
        fe=start-1;  

        strncpy(fore,fs,fe-fs+1);  
        fore[fe-fs+1]='';  
        if(strcmp(fore,name)!=0)  
            continue;  
        find=1;  

        start=equal+1;  
        while(isblank(*start))  
            ++start;  
        bs=start;  

        while(!isblank(*start)&&*start!='n')  
            ++start;  
        be=start-1;  

        strncpy(back,bs,be-bs+1);  
        back[be-bs+1]='';  
        strcpy(info,back);  
        break;  
    }  
    if(find)  
        return info;  
    else  
        return NULL;  
}

 

复制代码 代码如下:

客户端-服务器编程模型

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

图片 1client-server transaction

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

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

(cliaddr : cliport, servaddr : servport)

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

图片 2connection socket pair

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

三、 测试

本次测试

使用了两台机器。一台Ubuntu的浏览器作为客户端,一台Redhat作为服务器端,其中Redhat是Ubuntu上基于VirtualBox的一台虚拟机。

IP地址信息如下:Ubuntu的vboxnet0:

<img src="http://jbcdn2.b0.upaiyun.com/2016/11/73d81eee84e5fe07de8e076ee6b5d25a.jpg" alt="图片 3" />

RedHateth0:

图片 4

RedHat主机编译项目:

图片 5

由于我们同事监听了8000和4444,所以有两个进程启动。

HTTP的首页:

图片 6

目录显示功能:

图片 7

HTTP GET页面:

图片 8

HTTPGET响应:

图片 9

从HTTP GET响应中我们观察URL,参数的确是通过URL传送过去的。

其中getAuth.c如下:

#include "wrap.h"  
#include "parse.h"  

int main(void) {  
    char *buf, *p;  
    char name[MAXLINE], passwd[MAXLINE],content[MAXLINE];  

    /* Extract the two arguments */  
    if ((buf = getenv("QUERY_STRING")) != NULL) {  
    p = strchr(buf, '&');  
    *p = '';  
    strcpy(name, buf);  
    strcpy(passwd, p+1);  
    }  

    /* Make the response body */  
    sprintf(content, "Welcome to auth.com:%s and %srn<p>",name,passwd);  
    sprintf(content, "%srn", content);  

    sprintf(content, "%sThanks for visiting!rn", content);  

    /* Generate the HTTP response */  
    printf("Content-length: %drn", strlen(content));  
    printf("Content-type: text/htmlrnrn");  
    printf("%s", content);  
    fflush(stdout);  
    exit(0);  
}

HTTPS的首页:由于我们的CA不可信,所以需要我们认可

图片 10

认可后HTTPS首页:

图片 11

HTTPS POST页面:

图片 12

HTTPS POST响应:

图片 13

从上我们可以看出,POST提交的参数的确不是通过URL传送的。

通过.NET 为我们提供的HttpListener类实现对Http协议的处理,实现简单的web服务器。

package webserver 
 
import ( 
    "encoding/json" 
    "fmt" 
    "net/http" 
    "time" 

 
func WebServerBase() { 
    fmt.Println("This is webserver base!") 
 
    //第一个参数为客户端发起http请求时的接口名,第二个参数是一个func,负责处理这个请求。 
    http.HandleFunc("/login", loginTask) 
 
    //服务器要监听的主机地址和端口号 
    err := http.ListenAndServe("192.168.1.27:8081", nil) 
 
    if err != nil { 
        fmt.Println("ListenAndServe error: ", err.Error()) 
    } 

 
func loginTask(w http.ResponseWriter, req *http.Request) { 
    fmt.Println("loginTask is running...") 
 
    //模拟延时 
    time.Sleep(time.Second * 2) 
 
    //获取客户端通过GET/POST方式传递的参数 
    req.ParseForm() 
    param_userName, found1 := req.Form["userName"] 
    param_password, found2 := req.Form["password"] 
 
    if !(found1 && found2) { 
        fmt.Fprint(w, "请勿非法访问") 
        return 
    } 
 
    result := NewBaseJsonBean() 
    userName := param_userName[0] 
    password := param_password[0] 
 
    s := "userName:" + userName + ",password:" + password 
    fmt.Println(s) 
 
    if userName == "zhangsan" && password == "123456" { 
        result.Code = 100 
        result.Message = "登录成功" 
    } else { 
        result.Code = 101 
        result.Message = "用户名或密码不正确" 
    } 
 
        //向客户端返回JSON数据 
    bytes, _ := json.Marshal(result) 
    fmt.Fprint(w, string(bytes)) 
}

使用socket处理请求与响应

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

图片 14socket overview

服务器调用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@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);

注意:此类在 .NET Framework 2.0 版中是新增的。所以支持.NET Framework 2.0以上版本。该类仅在运行 Windows XP SP2 或 Windows Server 2003 操作系统的计算机上可用。

NewBaseJsonBean用于创建一个struct对象:

HTTP协议与静/动态网页

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

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

引用命名空间:using System.Net;

复制代码 代码如下:

关键代码解析

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; } listenfd = Open_listenfd; while  { clientlen = sizeof(clientaddr); connfd = Accept(listenfd, &clientaddr, &clientlen); doit; Close; }}

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

doit函数定义如下。

void doit { 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; /* 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; 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; 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; sprintf(buf, "Server: Tiny Web Serverrn"); Rio_writen(fd, buf, strlen; if  == 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; /* 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 { 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 = ''; strcpy(arg1, buf); strcpy(arg2, p+1); n1 = atoi; n2 = atoi; } /* 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", strlen; printf("Content-type: text/htmlrnrn"); printf("%s", content); fflush; exit;}

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

使用Http服务一般步骤如下:

package webserver 
 
type BaseJsonBean struct { 
    Code    int         `json:"code"` 
    Data    interface{} `json:"data"` 
    Message string      `json:"message"` 

 
func NewBaseJsonBean() *BaseJsonBean { 
    return &BaseJsonBean{} 
}

实验效果与源码

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

./tiny 8000

静态网页效果:访问

图片 15静态网页效果

动态网页效果:访问

图片 16动态网页效果

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

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

  1. 创建一个HTTP侦听器对象并初始化

  2. 开始侦听来自客户端的请求

  3. 处理客户端的Http请求

  4. 关闭HTTP侦听器

希望本文所述对大家的Go语言程序设计有所帮助。

参考资料

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

创建一个HTTP侦听器对象

您可能感兴趣的文章:

  • 基于Django URL传参 FORM表单传数据 get post的用法实例
  • Go语言中利用http发起Get和Post请求的方法示例
  • Go语言Web编程实现Get和Post请求发送与解析的方法详解
  • Go语言开发发送Get和Post请求的示例

创建HTTP侦听器对象只需要新建一个HttpListener对象即可。

HttpListener listener = new HttpListener();