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.