go网络编程学习笔记7(关于HTTP)

简介

万维网是一个庞大的,拥有数以百万计用户的分布式系统。网站就是一个运行着 HTTP 服务器的 Web 主机。而 Web 客户端通常是浏览器用户,当然也还有许多其他的“用户”,如网络蜘蛛,Web 应用程序客户端等。
Web 使用的 HTTP(超文本传输协议)基于 TCP 协议的。HTTP 有三个公开可用的版本,目前最常用的是最新的版本 1.1

HTTP 概述

URL 和资源

URL指定资源的位置。资源通常是 HTML 文档、图片、声音文件这样的静态文件,但越来越多的资源是动态生成的对象,比如根据数据库信息生成。
“用户”请求资源时,返回的并不是资源本身,而是资源的代表。如果资源是静态文件,那么返回给用户的就是文件的一个副本。
不同的 URL 可以指向相同的资源,HTTP 服务器会给每个 URL 返回适当的代表。例如,针对同一个产品,某公司可以使用不同的 URL 给本地和外地的用户查看其产品信息,本地用户可以看到本地产品联系人这类内容,而外地用户看到的内容则包括产品销售门店的地址等等。
这其实就意味着,HTTP 协议本身非常简单直接,但 HTTP 服务器却可能非常复杂。HTTP将用户请求发送到服务器,并返回字节流,而服务器针对该请求可能需要做很多很多处理。

HTTP 的特点

HTTP 协议是无状态面向连接可靠的。最简单的形式是,每个从用户发起的请求被可靠地处理,然后断开连接。每次请求都包括一个独立的 TCP 连接,所以如果要请求很多资源(如在 HTML 页面中嵌入的图像),则必须在很短的时间内建立并断开许多 TCP 连接。
为构建更高效更可靠的协议,有许多在这种简单结构基础上添加复杂性的优化技术。

版本

HTTP 有三个版本

  • Version 0.9 - 完全废弃
  • Version 1.0 - 基本废弃
  • Version 1.1 - 当前版本
    每个版本必须兼容早期的版本。

HTTP 0.9

请求格式

Request = Simple- Request
Simple-Request = "GET" SP Request-URI CRLF

响应格式

Response = Simple- Response
Simple-Response = [Entity-Body] 

HTTP1.0

该版本在请求和响应中增加了很多信息。与其说是 0.9 的升级版,还不如说它是一个全新的版本。

请求格式

从客户端到服务器端的请求格式:

Request = Simple-Request | Full Request

Simple-Request = "GET" SP Request-URI CRLF

Full-Request = Request-Line
    *(General-Header
    | Request-Header
    | Entity-Header)
    CRLF
    [Entity-Body] 

简单请求(Simple-Request)表明是一个 HTTP/0.9 请求,必须回复简单响应(Simple-Response)。
请求行(Request-Line)的格式如下:

Request-Line = Method SP Request-URI SP HTTP-Version CRLF

其中

Method = "GET" | "HEAD" | POST |
    extension-method 

如:GET http://jan.newmarch.name/index.html HTTP/1.0

响应格式

响应的形式如下:

Response = Simple-Response | Full-Response

Simple-Response = [Entity-Body]

Full-Response = Status-Line
    *(General-Header
    | Response-Header
    | Entity-Header)
    CRLF
    [Entity-Body] 

状态行(Status-Line)给出请求的最后的状态信息:

Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

HTTP/1.0 200 OK
状态码:

Status-Code = "200" ; OK
            | "201" ; Created
            | "202" ; Accepted
            | "204" ; No Content
            | "301" ; Moved permanently
            | "302" ; Moved temporarily
            | "304" ; Not modified
            | "400" ; Bad request
            | "401" ; Unauthorised
            | "403" ; Forbidden
            | "404" ; Not found
            | "500" ; Internal server error
            | "501" ; Not implemented
            | "502" ; Bad gateway
            | "503" | Service unavailable
            | extension-code 

实体头(Entity-Header)包含了有关实体(Entity-Body)的有用信息

Entity-Header = Allow
            | Content-Encoding
            | Content-Length 
            | Content-Type
            | Expires
            | Last-Modified
            | extension-header

例如:

HTTP/1.1 200 OK
Date: Fri, 29 Aug 2003 00:59:56 GMT
Server: Apache/2.0.40 (Unix)
Accept-Ranges: bytes
Content-Length: 1595
Connection: close
Content-Type: text/html; charset=ISO-8859-1 

HTTP 1.1

HTTP 1.1 修复了 HTTP 1.0 中的很多问题,因此更加复杂。例如此版本中扩展和完善了HTTP 1.0 中的可选项

  • 增加了命令,如TRACECONNECT
  • 注意在通过代理服务器进行连接时,应当使用绝对路径。如:
GET http://www.w3.org/index.html HTTP/1.1
  • 增加了更多属性,例如针对代理服务器的 If-Modified-Since

这些变动包括:

  • 主机名识别(支持虚拟主机)
  • 内容协商(多语言)
  • 持久连接(降低 TCP 开销)
  • 分块传送
  • 字节范围(请求文件部分内容)
  • 代理支持

0.9 版本的协议只有一页,1.0 版本用了大约 20 页来说明,而 1.1 则用了 120 页

简单用户代理(Simple user-agents)

用户代理(User agent)(例如浏览器)用来发起请求和接收响应。代码中的 response type 如下:

type Response struct {
    Status string // e.g. "200 OK"
    StatusCode int // e.g. 200
    Proto string // e.g. "HTTP/1.0"
    ProtoMajor int // e.g. 1
    ProtoMinor int // e.g. 0
   
    RequestMethod string // e.g. "HEAD", "CONNECT", "GET", etc.
   
    Header map[string]string
   
    Body io.ReadCloser
   
    ContentLength int64
   
    TransferEncoding []string
   
    Close bool
   
    Trailer map[string]string
}

通过实例可以了解其数据结构。最简单的请求是由用户代理发起"HEAD"命令,其中包括请求的资源和 HTTP 服务器。函数

func Head(url string) (r *Response, err os.Error) 

可用来发起此请求。
响应状态对应 response 中的 Status 属性,而 Header 属性对应 HTTP 响应的 header 域。下面的程序用来发起请求和显示结果:

/* Head
*/

package main

import (
    "fmt"
    "net/http"
    "os"
)
  
func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "host:port")
        os.Exit(1)
    }
    url := os.Args[1]
 
    response, err := http.Head(url)
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(2)
    }
 
    fmt.Println(response.Status)
    for k, v := range response.Header{
        fmt.Println(k+":", v)
    }
 
    os.Exit(0)
} 

程序运行请求资源,Head http://www.baidu.com/,输出结果类似:

[root@localhost home]# ./test http://www.baidu.com
200 OK
Connection: [Keep-Alive]
Content-Type: [text/html]
Etag: ["575e1f8a-115"]
Accept-Ranges: [bytes]
Cache-Control: [private, no-cache, no-store, proxy-revalidate, no-transform]
Last-Modified: [Mon, 13 Jun 2016 02:50:50 GMT]
Pragma: [no-cache]
Server: [bfe/1.0.8.18]
Content-Length: [277]
Date: [Fri, 14 Jun 2019 07:26:28 GMT]

通常我们希望接收到一个资源内容而不是其有关信息"GET"请求就是做来做这个的,使用如下函数即可:

func Get(url string) (r *Response, err os.Error)

响应内容response 的 Body 属性。它是一个 io.ReadCloser类型。我们可以用以下程序在屏幕上打印相应内容

/* Get*/

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
    "os"
    "strings"
) 
 
func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "host:port")
        os.Exit(1)
    }
    url := os.Args[1]
 
    response, err := http.Get(url)
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(2)
    }
 
    if response.Status!= "200 OK"{
        fmt.Println(response.Status)
        os.Exit(2)
    }
 
    b, _:= httputil.DumpResponse(response, false)
    fmt.Print(string(b))
 
    contentTypes := response.Header["Content-Type"]
    if !acceptableCharset(contentTypes) {
        fmt.Println("Cannot handle", contentTypes)
        os.Exit(4)
    }
 
    var buf [512]byte 
    reader := response.Body
    for{
        n, err := reader.Read(buf[0:])
        if err != nil {
            os.Exit(0)
        }
        fmt.Print(string(buf[0:n]))
    }
    os.Exit(0)
}
 
func acceptableCharset(contentTypes []string) bool {
    // each type is like [text/html; charset=UTF-8]
    // we want the UTF-8 only
    for _, cType := range contentTypes {
        //这里存在一个问题,经过测试,大多数页面采用默认编码,未指定charset,则会产生异常
        if strings.Index(cType, "UTF-8") != -1 {
            return true
        }
    }
    return false
} 

注意这里有一个重要的字符集类型问题,在前面章节也讨论过。服务器提供内容时使用的字符集编码,甚至传输编码,通常是用户代理和服务器之间协商的结果,但我们使用的 Get的命令很简单,它不包括用户代理的内容协商组件。因此,服务器可以自行决定使用什么字符编码。
我第一次写的时候是在中国。当我用这个程序访问 www.google.com 时,谷歌的服务器尝试猜测我的地理位置,然后很厉害地使用了 Big5 码给我发送文本!后面会讨论如何告知服务器给我什么字符编码最好。

设置 HTTP 请求

Go 还提供一个较低级别的用户代理接口用来与 HTTP 服务器进行通信。你可能已经想到,这样可以更灵活地控制客户端请求,当然创建请求也会更费力气。不过这只需要多费一点点力气。
用来创建请求的数据类型是Request。这是个复杂的类型,Go 语言文档中给出的定义如下:

type Request struct {
    Method string // GET, POST, PUT, etc.
    RawURL string // The raw URL given in the request.
    URL *URL // Parsed URL.
    Proto string // "HTTP/1.0"
    ProtoMajor int // 1 ProtoMajor int
    ProtoMinor int // 0 ProtoMinor int
   
    // A header maps request lines to their values.
    // If the header says
    //
    // accept-encoding: gzip, deflate
    // Accept-Language: en-us
    // Connection: keep-alive
    //
    // then
    //
    // Header = map[string]string{
    //          "Accept-Encoding": "gzip, deflate",
    //          "Accept-Language": "en-us",
    //          "Connection": "keep-alive",
    // }
    //
    // HTTP defines that header names are case-insensitive
    // The request parser implements this by canonicalizing
    // name, making the first character and any characters
    // following a hyphen uppercase and the rest lowercase.
    Header map[string]string
   
    // The message body.
    Body io.ReadCloser
    
    // ContentLength records the length of the associated content.
    // The value -1 indicates that the length is unknown.
    // Values >= 0 indicate that the given number of bytes may be read from Body.
    ContentLength int64
   
    // TransferEncoding lists the transfer encodings from outermost to innermost.
    // An empty list denotes the "identity" encoding.
    TransferEncoding []string
   
    // Whether to close the connection after replying to this request.
    Close bool
   
    // The host on which the URL is sought.
    // Per RFC 2616, this is either the value of the Host: header
    // or the host name given in the URL itself.
    Host string
   
    // The referring URL, if sent in the request.
    //
    // Referer is misspelled as in the request itself,
    // a mistake from the earliest days of HTTP. 
    // This value can also be fetched from the Header map
    // as Header["Referer"]; the benefit of making it
    // available as a structure field is that the compiler
    // can diagnose programs that use the alternate
    // (correct English) spelling req.Referrer but cannot
    // diagnose programs that use Header["Referrer"].
    Referer string
    
    // The User-Agent: header string, if sent in the request.
    UserAgent string
   
    // The parsed form. Only available after ParseForm is called.
    Form map[string][]string 
   
    // Trailer maps trailer keys to values. Like for Header, if the
    // response has multiple trailer lines with the same key, they will be
    // concatenated, delimited by commas.
    Trailer map[string]string
   }

请求中可以存放大量的信息,但你不需要填写所有的内容,只填必要的即可。最简单的使用默认值创建请求的方法如下:

request, err := http.NewRequest("GET", url.String(), nil) 

请求创建后,可以修改其内容字段(field)。比如,需指定只接受 UTF-8,可添加一个"Accept-Charset"字段:

request.Header.Add("Accept-Charset", "UTF-8;q=1, ISO-8859-1;q=0") 

(注意,若没有在列表中提及,则默认设置 ISO-8859-1 总是返回值 1).
如上所述,客户端设置字符集请求很简单。但对于服务器返回的字符集,发生的事情就比较复杂。返回的资源理应包含 Content-Type,用来指明内容的媒介类型,如:text/html。有些媒介类型应当声明字符集,如 text/html; charset=UTF-8。如果没有指明字符集,按照 HTTP规范就应当作为默认的 ISO8859-1 字符集处理。但是很多服务器并不符合此约定,因此 HTML4 规定此时不能做任何假设。
如果服务器的 Content-Type 指定了字符集,那么就认为它是正确的。如果未指定字符集,由于 50%的页面是 UTF-8 的,20%的页面是 ASCII 的,因此假设字符集是 UTF-8 的会比较安全,但仍然有 30%的页面可能会出错:-(。

客户端对象

向服务器发送一个请求并取得回复,最简单的方法是使用方便对象 Client。此对象可以管理多个请求,并处理一些问题,如与服务器间的 TCP 连接是否保持活动状态等。
下面的程序给出了示例:

/* ClientGet */

package main

import(
    "fmt"
    "net/http"
    "net/url"
    "os"
    "strings"
)
 
func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "http://host:port/page")
        os.Exit(1)
    }
    url, err := url.Parse(os.Args[1]) 
    checkError(err)
 
    client := &http.Client{}
 
    request, err := http.NewRequest("GET", url.String(), nil)
    // only accept UTF-8
    request.Header.Add("Accept-Charset", "UTF-8;q=1, ISO-8859-1;q=0")
    checkError(err)
 
    response, err := client.Do(request)
    if response.Status!= "200 OK"{
        fmt.Println(response.Status)
        os.Exit(2)
    }
 
    chSet := getCharset(response)
    fmt.Printf("got charset %s\n", chSet)
    if chSet != "UTF-8"{
        fmt.Println("Cannot handle", chSet)
        os.Exit(4)
    }
 
    var buf [512]byte
    reader := response.Body
    fmt.Println("got body")
    for{
        n, err := reader.Read(buf[0:])
        if err != nil {
            os.Exit(0) 
        }
        fmt.Print(string(buf[0:n]))
    }
 
    os.Exit(0)
}
 
func getCharset(response *http.Response) string {
    contentType := response.Header.Get("Content-Type")
    if contentType == ""{
        // guess
        return "UTF-8"
    }
    idx := strings.Index(contentType, "charset:")
    if idx == -1 {
        // guess
        return "UTF-8"
    }
    return strings.Trim(contentType[idx:], " ")
 }
 
func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
} 

代理处理

简单代理

HTTP 1.1 规定了 HTTP 应当如何通过代理工作。向代理服务器发送一个"GET"请求。但是请求 URL 必须是完整的目标地址。此外,设置代理的 HTTP 头应当包括"Host"字段。只要代理服务器设置为允许这样的请求通过,那么做这些就够了。
Go 把这看成 HTTP 传输层的一部分。可使用 Transport 类进行管理。可以使用函数将代理服务器的 URL 返回到它的一个字段。假设有一个代理服务器地址字符串 URL,相应的创建Transport 对象并交给 Client 对象的代码就是:

proxyURL, err := url.Parse(proxyString)
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client := &http.Client{Transport: transport}

客户端可以像之前一样继续使用
下面是程序范例:

/* ProxyGet */

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
)
 
func main() {
    if len(os.Args) != 3 {
        fmt.Println("Usage: ", os.Args[0], "http://proxy-host:port http://host:port/page")
        os.Exit(1)
    }
    proxyString := os.Args[1]
    proxyURL, err := url.Parse(proxyString)
    checkError(err)
    rawURL := os.Args[2] 
    url, err := url.Parse(rawURL)
    checkError(err)
 
    transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
    client := &http.Client{Transport: transport}
 
    request, err := http.NewRequest("GET", url.String(), nil)
 
    dump, _:= httputil.DumpRequest(request, false)
    fmt.Println(string(dump))
 
    response, err := client.Do(request)
 
    checkError(err)
    fmt.Println("Read ok")
 
    if response.Status!= "200 OK"{
        fmt.Println(response.Status)
        os.Exit(2)
    }
    fmt.Println("Reponse ok")
 
    var buf [512]byte
    reader := response.Body
    for{
        n, err := reader.Read(buf[0:])
        if err != nil {
            os.Exit(0)
        } 
        fmt.Print(string(buf[0:n]))
    }
 
    os.Exit(0)
}
 
func checkError(err error) {
    if err != nil {
        if err == io.EOF{
            return
        }
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

假设有一个代理服务器 XYZ.com,端口 8080,测试命令就是
go run ProxyGet.go http://XYZ.com:8080/ http://www.google.com
如果没有合适的代理服务器可供测试,也可以在自己的计算机上下载安装 Squid proxy。
上面的程序是将已知的代理服务器地址作为参数传入的。有很多办法可以将代理服务器的地址通知到应用程序。大多数浏览器可以通过配置菜单输入代理服务器的信息:但这些信息对Go 应用没有用。有些应用程序可以从网络中某处找到 autoproxy.pac 文件取得其中的代理服务器信息,但 Go(目前还)不能解析 JavaScript 文件,因此也不能使用。Gnome Linux 系统使用的配置系统 gconf 里可以存储代理服务器信息,但 Go 也访问不了。但是,如果在操作系统环境变量中设置代理服务器信息(如 HTTP_PROXYhttp_proxy),Go 可以通过以下函数访问到:

func ProxyFromEnvironment(req *Request) (*url.URL, error)

假如你的程序运行在这样的环境中,就可以使用此功能,而不用明确指定代理服务器参数。

身份验证代理

有些代理服务器要求通过用户名和密码进行身份验证才能传递请求。一般的方法是“基本身份验证”:将用户名和密码串联成一个字符串“user:password”,然后进行 Base64 编码,然后添加到 HTTP 请求头的“Proxy-Authorization”中,再发送到代理服务器
在前一个程序的基础上增加 Proxy-Authorization 头,示例如下:

/* ProxyAuthGet */

package main

import (
    "encoding/base64"
    "fmt"
    "io"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
)
 
const auth = "jannewmarch:mypassword"
 
func main() { 
    if len(os.Args) != 3 {
        fmt.Println("Usage: ", os.Args[0], "http://proxy-host:port http://host:port/page")
        os.Exit(1)
    }
    proxy := os.Args[1]
    proxyURL, err := url.Parse(proxy)
    checkError(err)
    rawURL := os.Args[2]
    url, err := url.Parse(rawURL)
    checkError(err)
 
    // encode the auth
    basic := "Basic "+ base64.StdEncoding.EncodeToString([]byte(auth))
 
    transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
    client := &http.Client{Transport: transport}
 
    request, err := http.NewRequest("GET", url.String(), nil)
 
    request.Header.Add("Proxy-Authorization", basic)
    dump, _:= httputil.DumpRequest(request, false)
    fmt.Println(string(dump))
 
    // send the request
    response, err := client.Do(request)
 
    checkError(err)
    fmt.Println("Read ok") 
 
    if response.Status!= "200 OK"{
        fmt.Println(response.Status)
        os.Exit(2)
    }
    fmt.Println("Reponse ok")
 
    var buf [512]byte
    reader := response.Body
    for{
        n, err := reader.Read(buf[0:])
        if err != nil {
            os.Exit(0)
        }
        fmt.Print(string(buf[0:n]))
    }
 
    os.Exit(0)
 } 
 
func checkError(err error) {
    if err != nil {
        if err == io.EOF{
            return
        }
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

客户端发起 HTTPS 连接

为保证连接的安全和加密,HTTP 使用其在安全性章节中说明的 TLS 技术。HTTP+TLS 的协议被称为 HTTPS,它使用 https://地址,而不是 http://地址。
服务器必须在客户端接受从其数据前返回有效的 X.509 证书。如果证书有效,Go 会在内部处理好所有的事情,而客户端会在使用 HTTPS 地址时和以前工作得一样出色。
许多网站都使用无效的证书。这些证书可能已经过期,或者是自行签名的,而没有让认可的证书颁发机构签名;又或者他们可能只是用错了(比如服务器名称不对)。浏览器(如 Firefox),会显示一个很大的警告通知,通知上放着“立即离开!”按钮,但你也可以仍然继续此风险- 很多人会这么做。
Go 目前在遇到证书错误时,会 bails out。对继续工作的支持非常谨慎,我还没有找到正确的方法。因此,目前也没有“继续此风险”任何示例 :-)。以后再说吧。

服务器

这边创建客户端,另一边 Web 服务器则需要处理 HTTP 请求。最早最简单的服务器只是返回文件的副本。然而,目前的服务器上,随便一个 URL 都可能触发任何计算。

文件服务器

我们从一个基本的文件服务器开始。Go 提供了一个 multi-plexer,即一个读取和解释请求的对象。它把请求交给运行在自己线程中的 handlers。这样,许多读取 HTTP 请求,解码并转移到合适功能上的工作都可以在各自的线程中进行。
对于文件服务器,Go 也提供了一个FileServer对象,它知道如何发布本地文件系统中的文件。它需要一个“root”目录,该目录是在本地系统中文件树的顶端;还有一个针对 URL 的匹配模式。最简单的模式是“/”,这是所有 URL 的顶部,可以匹配所有的 URL。
HTTP 服务器从本地文件系统中发布文件太简单了,让人都有点不好意思举例。如下:

/* File Server */

package main

import (
    "fmt"
    "net/http"
    "os"
)
 
func main() {
    // deliver files from the directory /var/www
    // fileServer := http.FileServer(http.Dir("/var/www"))
    fileServer := http.FileServer(http.Dir("/home/httpd/html/"))
 
    // register the handler and deliver requests to it
    err := http.ListenAndServe(":8000", fileServer)
    checkError(err)
    // That's it!
}
 
func checkError(err error) {
    if err != nil { 
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
} 

甚至当请求到一个不存在的文件资源时,这个服务器还提供了“404 未找到”的信息!

处理函数(Handler function)

上一个程序中,handler 被作为第二个参数传给 ListenAndServe。可以先注册任意多个 handlerHandlehandleFunc 使用。调用方式:

func Handle(pattern string, handler Handler)
func HandleFunc(pattern string, handler func(*Conn, *Request)) 

ListenAndServe第二个参数是可以是 nil,调用会被分派到所有已注册的 handler。每个对立对象都有不同的 URL 匹配模式。例如,可能文件 handler 的 URL 匹配模式是"/",而一个函数 handler 的 URL 匹配模式是"/cgi-bin"。这里具体的模式优先级高于一般的模式。
常见的 CGI 程序有 test-cgi(shell 程序)或 printenv(Perl 程序)用来打印环境变量的值。可以让handler 用类似的方式工作。

/* Print Env */

package main

import (
    "fmt"
    "net/http"
    "os"
)
 
func main() {
    // file handler for most files
    fileServer := http.FileServer(http.Dir("/var/www"))
    http.Handle("/", fileServer)

    //function handler for /cgi-bin/printenv
    http.HandleFunc("/cgi-bin/printenv", printEnv)

    //deliver requests to the handlers
    err := http.ListenAndServe(":8000", nil)
    checkError(err)
    // That's it!
}

func printEnv(writer http.ResponseWriter, req *http.Request) {
    env := os.Environ()
    writer.Write([]byte("<h1>Environment</h1>\n<pre>"))
    for _,v := range env {
        writer.Write([]byte(v + "\n"))
    }
    writer.Write([]byte("</pre>"))
}
 
func checkError(err error) {
    if err != nil { 
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
} 

注:为简单起见,本程序不提供完整的的 HTML。这里缺少 html、head 和 body 标签。
这个程序在使用 cgi-bin 目录时有点耍赖。其实它并没有调用外部的 CGI 脚本程序,而只是使用了一个 Go 的内部函数。Go 确实可以通过 os.ForkExec 调用外部的程序,但还不能支持像 Apache 的 mod_perl 这样的动态连接库

绕过默认的 multiplexer

Go 服务器接收到的 HTTP 请求通常是由一个 multiplexer 进行处理,检查 HTTP 请求的路径,然后调用合适的文件 handler 等等。你也可以定义自己的 handler。将一个匹配模式参数和一个函数作为参数,调用http.HandleFunc,可以将其注册为默认的 multiplexer。像** ListenAndServe这样的函数就可以使用 nil 作为 handler function。上一个例子就是这样做的。
如果你想扮演 multiplexer 的角色,那么你就可以
给一个非零函数作为 handler function**。这个函数将会全权负责管理请求和响应。
下面的例子非常简单,但它说明了如何使 multiplexer 对所有请求都只返回一个“204 No content”:

/* ServerHandler */

package main

import ( 
    "net/http"
)
 
func main() {
    myHandler := http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
        // Just return no content -arbitrary headers can be set, arbitrary body
        rw.WriteHeader(http.StatusNoContent)
    })
 
    http.ListenAndServe(":8080", myHandler)
} 

当然,也可以把它做成无所不能的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • iOS网络编程读书笔记 Facade Tester客户端门面模式的实例(被动版本化) 被动版本化,所以硬编码URL...
    melouverrr阅读 1,602评论 3 7
  • Getting Started Burp Suite 是用于攻击web 应用程序的集成平台。它包含了许多工具,并为...
    Eva_chenx阅读 28,661评论 0 14
  • Web 页面的实现 Web 基于 HTTP 协议通信 客户端(Client)的 Web 浏览器从 Web 服务器端...
    毛圈阅读 1,077评论 0 2
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,256评论 0 9