HTTP

Introduction

万维网是最主要的分布式系统,有上亿的用户。一个站点可能是运行在一个HTTP服务器上。万维网的客户端通常是一个浏览器。也可能是其它的用户代理,比如网络爬虫,应用程序客户端等。

Web是建立在HTTP协议之上,而HTTP又是建立在TCP之上。HTTP有三个版本,最新的为1.1, 它是现在最常用的一个版本。

在这一章,我们大概看看什么是HTTP. 之后讲解Go语言中的http连接API.

Overview of HTTP

URLs and resource

URLs用来指定资源的位置。一个资源通常是一个静态文件。比如HTML文档,图片,声音文件,但越来越多的是一个动态产生的对像,它是基于存储在数据库的信息。

当一个用户代理请求一个资源,它并不只是返回资源本身,而是这个资源的代表(representation)。比如,如果资源是一个静态文件,则发送给代理的是这个文件的副本.

多个URLs可以指向同一个资源,一个HTTP服务器将会为每一个URL返回相应的资源代表。比如,一个公司提供的产品信息分为内部和外部的,内部的产品表像包括的信息包括内部供应商联系方式。而外部的产品表像包括商店的位置。

虽然一个HTTP服务器可以非常的复杂,但从资源的观点上来看,HTTP协议非常简单明了.HTTP将用户代理的请求传递给服务器,然后返回一个字节流。当然在服务器上会对请求进行处理。

HTTP characteristics

http是无状态,非持久连接的可靠协议。每一个http请求,都是建立在一个TCP连接上,所以如果有多个资源请求(比如html文件中的图片), 则在短时间内,有许多TCP连接创建和销毁。

http存在以下3个版本

  • Version 0.9 - 完全过时
  • Version 1.0 - 完全过时
  • Version 1.1 - Current

每一个版本都必须知道之前的版本的请求和响应.

HTTP 0.9

请求格式

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

响应格式

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

HTTP 1.0

这个版本向请求和响应中添加了更多的信息。

请求格式

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.

A Request-Line的格式如下

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

方法可以为

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

e.g.

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

e.g.

HTTP/1.0 200 OK

Status-Code有以下几个格式

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-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修补了HTTP1.0的许多问题,但也更复杂。这个版本是通过扩展和改进1.0.

  • 多了更多的命令TRACE and CONNECT
  • 你应该使用决对路径。特别是对于代理来说 e.g.
    GET http://www.w3.org/index.html HTTP/1.1
    
  • 添加了更多的属性,比如If-Modified-Since, 也可以用于代理

HTTP1.1改进了以下内容

  • 主机名标识(允许是虚拟主机)
  • 内容协商(多语言)
  • 持久连接(降低TCP开销)
  • 分块传输
  • byte range(可以请求部分文档)
  • 代理支持

Simple user-agents

用户代理,比如浏览器,可以发起请求和接收响应。响应的类型如下

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
}

我们将通过一个例子来看看这个struct. 最简单的一个请求是"HEAD", 它请求一个关于资源与服务器的信息,不返回资源的数据类容

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

响应的状态是在response结构中的字段Status中获得。而Header字段是一个map类型。


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)

    }
    fmt.Printf("%#v", response)
    os.Exit(0)
}

运行Head http://www.golang.com,将打印以下的信息

200 OK
Content-Type: text/html; charset=utf-8
Date: Tue, 14 Sep 2010 05:34:29 GMT
Cache-Control: public, max-age=3600
Expires: Tue, 14 Sep 2010 06:34:29 GMT
Server: Google Frontend

通常,我们想要获得的是一个资源,而不是获得关于它的信息。"GET"就可以用于这样的目的

func Get(url string) (resp *Response, err error)

响应的内容在字段Body中,它的类型为io.ReadCloser. 以下这个程序将打印出Body中的内容


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.StatusCode != 200 {
        fmt.Println(response.Status)
        os.Exit(2)
    }

    b, _ := httputil.DumpResponse(response, false) //第二个参数表示输出Body部分
    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 {
    for _, cType := range contentTypes {
        cType = strings.ToUpper(cType)
        if strings.Index(cType, "UTF-8") != -1 {
            return true
        }
    }
    return false
}

字符的问题我们在上一章中讨论过了。服务器会使用字符集对内容进行编码后在传递。通常这个编码是由用户代理和服务器进行协商。但是简单的Get命令中,没有包含用户代理的协商。所以这就取决于服务器。

在第一次这本书的时候,我还在中国,当我们打开这个程序访问www.google.com, Google服务器会猜测我的地址,然后使用Big5编码。如何告诉服务器使用字符编码,我们在之后讨论

Configuring HTTP requests

Go也支持最底层的用户代理接口,与服务器进行通信。如你所期望的,这些接口不仅仅给你更多的控制客户端的请求. 但是,这也需要你花更多的努力来构建请求。但困难也只是增加一点点。

以下是Request的结构

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
    ProtoMinor int // 0
    // 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 the
    // 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的最简单的方法是使用默认值

//func NewRequest(method, urlStr string, body io.Reader) (*Request, error)
request, err := http.NewRequest("GET", url.String(), nil)

一时请求创建之后,你可以修改它的字段,比如你想要接受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,则可以设置为text/html;charset=utf-8; 如果没有指定字符集,则依据HTTP的规范,默认设置为ISO8859-1。但是很多服务器不支持HTML4的规定,所以你不能做任何的假设。

The Client object

为了将请求发送给服务器,并获得反馈。Client对像是最简单的方法。这个对像可以管理多个请求和处理各种问题,比如是否与服务器保持TCP持久连接。

以下用程序来说明这一点


func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "http://host:port/page")
        os.Exit(0)
    }

    url, err := url.Parse(os.Args[1])
    checkError(err)

    client := &http.Client{}

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

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

    response, err := client.Do(request)

    if response.StatusCode != 200 {
        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("Get 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 == "" {
        return "UTF-8"
    }
    idx := strings.Index(contentType, "charset:")
    if idx == -1 {
        return "UTF-8"
    }
    return strings.TrimSpace(contentType[idx:])
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error:", err.Error())
        os.Exit(1)
    }
}

Proxy handling

Simple proxy

HTTP1.1中列出了HTTP如何通过代理也能工作。A "GET"请求可以发送给一个代理proxy. 但是URL必须是完整的目的地URL. 除此以外,HTTP header中应当包含"Host"字段,但需要在代理中配置了这些请求可以通过。

Go语言认为,这是HTTP传输层的部分。为了管理它,有一个类Transport. 它包含一个字段,这个字段的值可以设置为一个函数,这个函数返回一个代理服务器的URL。如果我们获得了一个代理的URL的字符串,则相应的传输对像(transport)就会创建,然后将这个transport对像传递给client object.

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

然后client可以继续向之前那样

以下是详细的代码说明

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)

    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.StatusCode != 200 {
        fmt.Println(response.Status)
        os.Exit(2)

    }
    fmt.Println("Response 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 {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

如果没有代理服务器,可以下载安装Squid代理

上面的程序是能过第一参数来指定代理服务器。这里有很多的方法来设置代理服务器。许多的浏览器是通过配置菜单中输入代理服务器信息。但这些信息对于Go应用程序来说是无效的。有的应用程序是从autoproxy.pac文件中获得代理信息。但Go也不支持,因为它不知道如何解析这些Javascript文件,所以也不能使用它们。Linux系统使用Gnome,它有一个配置文件称为gconf,这里存放了代理信息。Go语言不能访问它,但是如果操作系统中有HTTP_PROXY或者http_proxy,则可以使用以下这个函数找到代理信息

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

Servers

与客户端对应的是建立一个http server, 用来处理http请求。 最简单的服务器是仅仅返回一个文件的副本。

File Server

我们从一个基础的文件服务器开始。Go语言提供了多路复用器,即,一个对像可以读又可以解释请求。它将请求转手到一个handlers,handlers运行在它们自已的线程中。因此读取HTTP请求,解码他们和分配合适的函数都在它们自已的线程中完成.

对于一个文件服务器,Go也提供了FileServer对像, 它用于传递文件。它接受一个"root"文件夹。


func main() {
    //http.Dir是一个字符串类型,在这里强制转换
    fileServer := http.FileServer(http.Dir("e:/golang"))
    err := http.ListenAndServe(":8000", fileServer)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

Handler functions

在最后一个程序中,handler会作为第二个参数传递给ListenAndServe. 可以通过Handle和handleFunc, 注册任意数量的handlers。

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

HandleAndServe的第二个参数可以为nil. 然后所有的调用可以分发给所有注册的handlers. 每一个Handler应该有一个不同的URL。举个例子,文件处理函数handlers的URL模式为"/", 而另一个处理函数可能为"/cgi-bin". 详细的URL要比通用的URL的优先级更高。

常见的CGI程序test-cgi(written in the shell) or printenv(written in Perl)会打印出环境变量的值。以下是一个类似的程序

func main() {
    fileServer := http.FileServer(http.Dir("e:/golang"))

    http.Handle("/", fileServer)

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

    err := http.ListenAndServe(":8000", nil)
    checkError(err)
}

func printEnv(writer http.ResponseWriter, req *http.Request) {
    env := os.Environ()
    writer.Write([]byte("<h1>Enviroment<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)
    }
}

在这里的cgi-bin/printenv处理函数并没有调用一个外部函数,比如CGI角本,仅仅是调用了一个Go函数。Golang中也可以通过os.ForkExec调用一个外部程序,但是也不支持动态链接模块的调用,比如Apache的mod_perl

Bypassing the default multiplexer

Go写的服务器在接受HTTP请求后,通常是通过多路复用器multiplexer来测试HTTP请求中的路径,并调用相应的文件处理函数。你也可以定义自己的处理器handle, 它可以通过http.HandleFunc注册到默认的multiplexer.而ListenAndServe的第二个参数则为空。如上面例子所示。

如果你想替换默认的multiplexer, 你可以给ListenAndServe的handler函数传递一个非空的函数。这个函数将负责管理所有的请求和响应.

以下这个例子向所有的请求返回一个"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)
}

当然,你可以建立更复杂的行为

low-level servers

Go支持更底层的服务器接口。但在次说明,这意味着程序员需要做更多的工作,你首先需要创建一个TCP服务器,然后包装一个ServerConn, 然后你可以读取Request和写Response.