5.1 Socket 編程
在Go語言中編寫網絡程序時,咱們將看不到傳統的編碼形式。之前咱們使用Socket編程時,
會按照以下步驟展開。
(1) 創建Socket:使用socket()函數。
(2) 綁定Socket:使用bind()函數。
(3) 監聽:使用listen()函數。或者鏈接:使用connect()函數。
(4) 接受鏈接:使用accept()函數。
(5) 接收:使用receive()函數。或者發送:使用send()函數。
Go語言標準庫對此過程進行了抽象和封裝。不管咱們指望使用什麼協議創建什麼形式的連
接,都只須要調用net.Dial()便可。
5.1.1 Dial()函數
Dial()函數的原型以下:
func Dial(net, addr string) (Conn, error)
其中net參數是網絡協議的名字,addr參數是IP地址或域名,而端口號以「:」的形式跟隨在地址
或域名的後面,端口號可選。若是鏈接成功,返回鏈接對象,不然返回error。
咱們來看一下幾種常見協議的調用方式。
TCP連接:
conn, err := net.Dial("tcp", "192.168.0.10:2100")
UDP連接:
conn, err := net.Dial("udp", "192.168.0.12:975")
ICMP連接(使用協議名稱):
第5章
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.1 Socket 編程 119
1
2
3
4
5
9
6
7
8
8
conn, err := net.Dial("ip4:icmp", "www.baidu.com")
ICMP連接(使用協議編號):
conn, err := net.Dial("ip4:1", "10.0.0.3")
這裏咱們能夠經過如下連接查看協議編號的含義:http://www.iana.org/assignments/protocol-numbers/
protocol-numbers.xml。
目前,Dial()函數支持以下幾種網絡協議:"tcp"、"tcp4"(僅限IPv4)、"tcp6"(僅限
IPv6)、"udp"、"udp4"(僅限IPv4)、"udp6"(僅限IPv6)、"ip"、"ip4"(僅限IPv4)和"ip6"
(僅限IPv6)。
在成功創建鏈接後,咱們就能夠進行數據的發送和接收。發送數據時,使用conn的Write()
成員方法,接收數據時使用Read()方法。
5.1.2 ICMP示例程序
下面咱們實現這樣一個例子:咱們使用ICMP協議向在線的主機發送一個問候,並等待主機
返回,具體代碼如代碼清單5-1所示。
代碼清單5-1 icmptest.go
package main
import (
"net"
"os"
"bytes"
"fmt"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host")
os.Exit(1)
}
service := os.Args[1]
conn, err := net.Dial("ip4:icmp", service)
checkError(err)
var msg [512]byte
msg[0] = 8 // echo
msg[1] = 0 // code 0
msg[2] = 0 // checksum
msg[3] = 0 // checksum
msg[4] = 0 // identifier[0]
msg[5] = 13 //identifier[1]
msg[6] = 0 // sequence[0]
msg[7] = 37 // sequence[1]
len := 8
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
120 第5 章 網絡編程
check := checkSum(msg[0:len])
msg[2] = byte(check >> 8)
msg[3] = byte(check & 255)
_, err = conn.Write(msg[0:len])
checkError(err)
_, err = conn.Read(msg[0:])
checkError(err)
fmt.Println("Got response")
if msg[5] == 13 {
fmt.Println("Identifier matches")
}
if msg[7] == 37 {
fmt.Println("Sequence matches")
}
os.Exit(0)
}
func checkSum(msg []byte) uint16 {
sum := 0
// 先假設爲偶數
for n := 1; n <len(msg)-1; n += 2 {
sum += int(msg[n])*256 + int(msg[n+1])
}
sum = (sum >> 16) + (sum & 0xffff)
sum += (sum >> 16)
var answer uint16 = uint16(^sum)
return answer
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
func readFully(conn net.Conn) ([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
result.Write(buf[0:n])
if err != nil {
if err == io.EOF {
break
}
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.1 Socket 編程 121
1
2
3
4
5
9
6
7
8
8
return nil, err
}
}
return result.Bytes(), nil
}
執行結果以下:
$ go build icmptest.go
$ ./icmptest www.baidu.com
Got response
Identifier matches
Sequence matches
5.1.3 TCP示例程序
下面咱們創建TCP連接來實現初步的HTTP協議,經過向網絡主機發送HTTP Head請求,讀
取網絡主機返回的信息,具體代碼如代碼清單5-2所示。
代碼清單5-2 simplehttp.go
package main
import (
"net"
"os"
"bytes"
"fmt"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
conn, err := net.Dial("tcp", service)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := readFully(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
122 第5 章 網絡編程
os.Exit(1)
}
}
func readFully(conn net.Conn) ([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
result.Write(buf[0:n])
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
}
return result.Bytes(), nil
}
執行這段程序並查看執行結果:
$ go build simplehttp.go
$ ./simplehttp qbox.me:80
HTTP/1.1 301 Moved Permanently
Server: nginx/1.0.14
Date: Mon, 21 May 2012 03:15:08 GMT
Content-Type: text/html
Content-Length: 184
Connection: close
Location: https://qbox.me/
5.1.4 更豐富的網絡通訊
實際上,Dial()函數是對DialTCP()、DialUDP()、DialIP()和DialUnix()的封裝。我
們也能夠直接調用這些函數,它們的功能是一致的。這些函數的原型以下:
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err error)
func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)
func DialUnix(net string, laddr, raddr *UnixAddr) (c *UnixConn, err error)
以前基於TCP發送HTTP請求,讀取服務器返回的HTTP Head的整個流程也可使用代碼清
單5-3所示的實現方式。
代碼清單5-3 simplehttp2.go
package main
import (
"net"
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.1 Socket 編程 123
1
2
3
4
5
9
6
7
8
8
"os"
"fmt"
"io/ioutil"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
與以前使用Dail()的例子相比,這裏有兩個不一樣:
net.ResolveTCPAddr(),用於解析地址和端口號;
net.DialTCP(),用於創建連接。
這兩個函數在Dial()中都獲得了封裝。
此外,net包中還包含了一系列的工具函數,合理地使用這些函數能夠更好地保障程序的
質量。
驗證IP地址有效性的代碼以下:
func net.ParseIP()
建立子網掩碼的代碼以下:
func IPv4Mask(a, b, c, d byte) IPMask
獲取默認子網掩碼的代碼以下:
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
124 第5 章 網絡編程
func (ip IP) DefaultMask() IPMask
根據域名查找IP的代碼以下:
func ResolveIPAddr(net, addr string) (*IPAddr, error)
func LookupHost(name string) (cname string, addrs []string, err error);
5.2 HTTP 編程
HTTP(HyperText Transfer Protocol,超文本傳輸協議)是互聯網上應用最爲普遍的一種網絡
協議,定義了客戶端和服務端之間請求與響應的傳輸標準。
Go語言標準庫內建提供了net/http包,涵蓋了HTTP客戶端和服務端的具體實現。使用
net/http包,咱們能夠很方便地編寫HTTP客戶端或服務端的程序。
閱讀本節內容,讀者須要具有以下知識點:
瞭解 HTTP 基礎知識
瞭解 Go 語言中接口的用法
5.2.1 HTTP客戶端
Go內置的net/http包提供了最簡潔的HTTP客戶端實現,咱們無需藉助第三方網絡通訊庫
(好比libcurl)就能夠直接使用HTTP中用得最多的GET和POST方式請求數據。
1. 基本方法
net/http包的Client類型提供了以下幾個方法,讓咱們能夠用最簡潔的方式實現 HTTP
請求:
func (c *Client) Get(url string) (r *Response, err error)
func (c *Client) Post(url string, bodyType string, body io.Reader) (r *Response, err
error)
func (c *Client) PostForm(url string, data url.Values) (r *Response, err error)
func (c *Client) Head(url string) (r *Response, err error)
func (c *Client) Do(req *Request) (resp *Response, err error)
下面概要介紹這幾個方法。
http.Get()
要請求一個資源,只需調用http.Get()方法(等價於http.DefaultClient.Get())即
可,示例代碼以下:
resp, err := http.Get("http://example.com/")
if err != nil {
// 處理錯誤 ...
return
}
defer resp.Body.close()
io.Copy(os.Stdout, resp.Body)
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.2 HTTP 編程 125
1
2
3
4
5
9
6
7
8
8
上面這段代碼請求一個網站首頁,並將其網頁內容打印到標準輸出流中。
http.Post()
要以POST的方式發送數據,也很簡單,只需調用http.Post()方法並依次傳遞下面的3個
參數便可:
請求的目標 URL
將要 POST 數據的資源類型(MIMEType)
數據的比特流([]byte形式)
下面的示例代碼演示瞭如何上傳一張圖片:
resp, err := http.Post("http://example.com/upload", "image/jpeg", &imageDataBuf)
if err != nil {
// 處理錯誤
return
}
if resp.StatusCode != http.StatusOK {
// 處理錯誤
return
}
// ...
http.PostForm()
http.PostForm()方法實現了標準編碼格式爲application/x-www-form-urlencoded
的表單提交。下面的示例代碼模擬HTML表單提交一篇新文章:
resp, err := http.PostForm("http://example.com/posts", url.Values{"title":
{"article title"}, "content": {"article body"}})
if err != nil {
// 處理錯誤
return
}
// ...
http.Head()
HTTP 中的 Head 請求方式代表只請求目標 URL 的頭部信息,即 HTTP Header 而不返回 HTTP
Body。Go 內置的 net/http 包一樣也提供了 http.Head() 方法,該方法同 http.Get() 方法一
樣,只需傳入目標 URL 一個參數便可。下面的示例代碼請求一個網站首頁的 HTTP Header信息:
resp, err := http.Head("http://example.com/")
(*http.Client).Do()
在多數狀況下,http.Get()和http.PostForm() 就能夠知足需求,可是若是咱們發起的
HTTP 請求須要更多的定製信息,咱們但願設定一些自定義的 Http Header 字段,好比:
設定自定義的"User-Agent",而不是默認的 "Go http package"
傳遞 Cookie
此時可使用net/http包http.Client對象的Do()方法來實現:
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
126 第5 章 網絡編程
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("User-Agent", "Gobook Custom User-Agent")
// ...
client := &http.Client{ //... }
resp, err := client.Do(req)
// ...
2. 高級封裝
除了以前介紹的基本HTTP操做,Go語言標準庫也暴露了比較底層的HTTP相關庫,讓開發
者能夠基於這些庫靈活定製HTTP服務器和使用HTTP服務。
自定義http.Client
前面咱們使用的http.Get()、http.Post()、http.PostForm()和http.Head()方法其
實都是在http.DefaultClient的基礎上進行調用的,好比http.Get()等價於http.Default-
Client.Get(),依次類推。
http.DefaultClient 在字面上就向咱們傳達了一個信息,既然存在默認的 Client,那麼
HTTP Client 大概是能夠自定義的。實際上確實如此,在net/http包中,的確提供了Client類
型。讓咱們來看一看http.Client類型的結構:
type Client struct {
// Transport用於肯定HTTP請求的建立機制。
// 若是爲空,將會使用DefaultTransport
Transport RoundTripper
// CheckRedirect定義重定向策略。
// 若是CheckRedirect不爲空,客戶端將在跟蹤HTTP重定向前調用該函數。
// 兩個參數req和via分別爲即將發起的請求和已經發起的全部請求,最先的
// 已發起請求在最前面。
// 若是CheckRedirect返回錯誤,客戶端將直接返回錯誤,不會再發起該請求。
// 若是CheckRedirect爲空,Client將採用一種確認策略,將在10個連續
// 請求後終止
CheckRedirect func(req *Request, via []*Request) error
// 若是Jar爲空,Cookie將不會在請求中發送,並會
// 在響應中被忽略
Jar CookieJar
}
在Go語言標準庫中,http.Client類型包含了3個公開數據成員:
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
其中Transport類型必須實現http.RoundTripper接口。Transport指定了執行一個
HTTP 請求的運行機制,假若不指定具體的Transport,默認會使用http.DefaultTransport,
這意味着http.Transport也是能夠自定義的。net/http包中的http.Transport類型實現了
http.RoundTripper接口。
CheckRedirect函數指定處理重定向的策略。當使用 HTTP Client 的Get()或者是Head()
方法發送 HTTP 請求時,若響應返回的狀態碼爲 30x (好比 301 / 302 / 303 / 307),HTTP Client 會
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.2 HTTP 編程 127
1
2
3
4
5
9
6
7
8
8
在遵循跳轉規則以前先調用這個CheckRedirect函數。
Jar可用於在 HTTP Client 中設定 Cookie,Jar的類型必須實現了 http.CookieJar 接口,
該接口預約義了 SetCookies()和Cookies()兩個方法。若是 HTTP Client 中沒有設定 Jar,
Cookie將被忽略而不會發送到客戶端。實際上,咱們通常都用 http.SetCookie() 方法來設定
Cookie。
使用自定義的http.Client及其Do()方法,咱們能夠很是靈活地控制 HTTP 請求,好比發
送自定義 HTTP Header 或是改寫重定向策略等。建立自定義的 HTTP Client 很是簡單,具體代碼
以下:
client := &http.Client {
CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("User-Agent", "Our Custom User-Agent")
req.Header.Add("If-None-Match", `W/"TheFileEtag"`)
resp, err := client.Do(req)
// ...
自定義 http.Transport
在http.Client 類型的結構定義中,咱們看到的第一個數據成員就是一個 http.Transport
對象,該對象指定執行一個 HTTP 請求時的運行規則。下面咱們來看看 http.Transport 類型
的具體結構:
type Transport struct {
// Proxy指定用於針對特定請求返回代理的函數。
// 若是該函數返回一個非空的錯誤,請求將終止並返回該錯誤。
// 若是Proxy爲空或者返回一個空的URL指針,將不使用代理
Proxy func(*Request) (*url.URL, error)
// Dial指定用於建立TCP鏈接的dail()函數。
// 若是Dial爲空,將默認使用net.Dial()函數
Dial func(net, addr string) (c net.Conn, err error)
// TLSClientConfig指定用於tls.Client的TLS配置。
// 若是爲空則使用默認配置
TLSClientConfig *tls.Config
DisableKeepAlives bool
DisableCompression bool
// 若是MaxIdleConnsPerHost爲非零值,它用於控制每一個host所須要
// 保持的最大空閒鏈接數。若是該值爲空,則使用DefaultMaxIdleConnsPerHost
MaxIdleConnsPerHost int
// ...
}
在上面的代碼中,咱們定義了 http.Transport 類型中的公開數據成員,下面詳細說明其
中的各行代碼。
Proxy func(*Request) (*url.URL, error)
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
128 第5 章 網絡編程
Proxy 指定了一個代理方法,該方法接受一個 *Request 類型的請求實例做爲參數並返回
一個最終的 HTTP 代理。若是 Proxy 未指定或者返回的 *URL 爲零值,將不會有代理被啓用。
Dial func(net, addr string) (c net.Conn, err error)
Dial 指定具體的dial()方法來建立 TCP 鏈接。若是不指定,默認將使用 net.Dial() 方法。
TLSClientConfig *tls.Config
SSL鏈接專用,TLSClientConfig 指定 tls.Client 所用的 TLS 配置信息,若是不指定,
也會使用默認的配置。
DisableKeepAlives bool
是否取消長鏈接,默認值爲 false,即啓用長鏈接。
DisableCompression bool
是否取消壓縮(GZip),默認值爲 false,即啓用壓縮。
MaxIdleConnsPerHost int
指定與每一個請求的目標主機之間的最大非活躍鏈接(keep-alive)數量。若是不指定,默認使
用 DefaultMaxIdleConnsPerHost 的常量值。
除了 http.Transport 類型中定義的公開數據成員之外,它同時還提供了幾個公開的成員
方法。
func(t *Transport) CloseIdleConnections()。該方法用於關閉全部非活躍的
鏈接。
func(t *Transport) RegisterProtocol(scheme string, rt RoundTripper)。
該方法可用於註冊並啓用一個新的傳輸協議,好比 WebSocket 的傳輸協議標準(ws),或
者 FTP、File 協議等。
func(t *Transport) RoundTrip(req *Request) (resp *Response, err error)。
用於實現 http.RoundTripper 接口。
自定義http.Transport也很簡單,以下列代碼所示:
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: pool},
DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
Client和Transport在執行多個 goroutine 的併發過程當中都是安全的,但出於性能考慮,應
當建立一次後反覆使用。
靈活的 http.RoundTripper 接口
在前面的兩小節中,咱們知道 HTTP Client 是能夠自定義的,而 http.Client 定義的第一
個公開成員就是一個 http.Transport 類型的實例,且該成員所對應的類型必須實現
http.RoundTripper 接口。下面咱們來看看 http.RoundTripper 接口的具體定義:
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.2 HTTP 編程 129
1
2
3
4
5
9
6
7
8
8
type RoundTripper interface {
// RoundTrip執行一個單一的HTTP事務,返回相應的響應信息。
// RoundTrip函數的實現不該試圖去理解響應的內容。若是RoundTrip獲得一個響應,
// 不管該響應的HTTP狀態碼如何,都應將返回的err設置爲nil。非空的err
// 只意味着沒有成功獲取到響應。
// 相似地,RoundTrip也不該試圖處理更高級別的協議,好比重定向、認證和
// Cookie等。
//
// RoundTrip不該修改請求內容, 除非了是爲了理解Body內容。每個請求
// 的URL和Header域都應被正確初始化
RoundTrip(*Request) (*Response, error)
}
從上述代碼中能夠看到,http.RoundTripper接口很簡單,只定義了一個名爲RoundTrip
的方法。任何實現了 RoundTrip() 方法的類型便可實現http.RoundTripper接口。前面咱們
看到的http.Transport類型正是實現了 RoundTrip() 方法繼而實現了該接口。
http.RoundTripper 接口定義的 RoundTrip() 方法用於執行一個獨立的 HTTP 事務,接
受傳入的 \*Request 請求值做爲參數並返回對應的 \*Response 響應值,以及一個 error 值。
在實現具體的 RoundTrip() 方法時,不該該試圖在該函數裏邊解析 HTTP 響應信息。若響應成
功,error 的值必須爲nil,而與返回的 HTTP 狀態碼無關。若不能成功獲得服務端的響應,error
必須爲非零值。相似地,也不該該試圖在 RoundTrip() 中處理協議層面的相關細節,好比重定
向、認證或是 cookie 等。
非必要狀況下,不該該在 RoundTrip() 中改寫傳入的請求體(\*Request),請求體的內
容(好比 URL 和 Header 等)必須在傳入 RoundTrip() 以前就已組織好並完成初始化。
一般,咱們能夠在默認的 http.Transport 之上包一層 Transport 並實現 RoundTrip()
方法,如代碼清單5-4所示。
代碼清單5-4 customtrans.go
package main
import(
"net/http"
)
type OurCustomTransport struct {
Transport http.RoundTripper
}
func (t *OurCustomTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
func (t *OurCustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// 處理一些事情 ...
// 發起HTTP請求
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
130 第5 章 網絡編程
// 添加一些域到req.Header中
return t.transport().RoundTrip(req)
}
func (t *OurCustomTransport) Client() *http.Client {
return &http.Client{Transport: t}
}
func main() {
t := &OurCustomTransport{
//...
}
c := t.Client()
resp, err := c.Get("http://example.com")
// ...
}
由於實現了http.RoundTripper 接口的代碼一般須要在多個 goroutine中併發執行,所以我
們必須確保實現代碼的線程安全性。
設計優雅的 HTTP Client
綜上示例講解能夠看到,Go語言標準庫提供的 HTTP Client 是至關優雅的。一方面提供了極
其簡單的使用方式,另外一方面又具有極大的靈活性。
Go語言標準庫提供的HTTP Client 被設計成上下兩層結構。一層是上述提到的 http.Client
類及其封裝的基礎方法,咱們不妨將其稱爲「業務層」。之因此稱爲業務層,是由於調用方一般
只須要關心請求的業務邏輯自己,而無需關心非業務相關的技術細節,這些細節包括:
HTTP 底層傳輸細節
HTTP 代理
gzip 壓縮
鏈接池及其管理
認證(SSL或其餘認證方式)
之因此HTTP Client 能夠作到這麼好的封裝性, 是由於HTTP Client 在底層抽象了
http.RoundTripper 接口,而http.Transport 實現了該接口,從而可以處理更多的細節,我
們不妨將其稱爲「傳輸層」。HTTP Client 在業務層初始化 HTTP Method、目標URL、請求參數、
請求內容等重要信息後,通過「傳輸層」,「傳輸層」在業務層處理的基礎上補充其餘細節,而後
再發起 HTTP 請求,接收服務端返回的 HTTP 響應。
5.2.2 HTTP服務端
本節咱們將介紹HTTP服務端技術,包括如何處理HTTP請求和HTTPS請求。
1. 處理HTTP請求
使用 net/http 包提供的 http.ListenAndServe() 方法,能夠在指定的地址進行監聽,
開啓一個HTTP,服務端該方法的原型以下:
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.2 HTTP 編程 131
1
2
3
4
5
9
6
7
8
8
func ListenAndServe(addr string, handler Handler) error
該方法用於在指定的 TCP 網絡地址 addr 進行監聽,而後調用服務端處理程序來處理傳入的連
接請求。該方法有兩個參數:第一個參數 addr 即監聽地址;第二個參數表示服務端處理程序,
一般爲空,這意味着服務端調用 http.DefaultServeMux 進行處理,而服務端編寫的業務邏
輯處理程序 http.Handle() 或 http.HandleFunc() 默認注入 http.DefaultServeMux 中,
具體代碼以下:
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
若是想更多地控制服務端的行爲,能夠自定義 http.Server,代碼以下:
s := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
2. 處理HTTPS請求
net/http 包還提供 http.ListenAndServeTLS() 方法,用於處理 HTTPS 鏈接請求:
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler)
error
ListenAndServeTLS() 和 ListenAndServe()的行爲一致,區別在於只處理HTTPS請求。
此外,服務器上必須存在包含證書和與之匹配的私鑰的相關文件,好比certFile對應SSL證書
文件存放路徑,keyFile對應證書私鑰文件路徑。若是證書是由證書頒發機構簽署的,certFile
參數指定的路徑必須是存放在服務器上的經由CA認證過的SSL證書。
開啓 SSL 監聽服務也很簡單,以下列代碼所示:
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil))
或者是:
ss := &http.Server{
Addr: ":10443",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(ss.ListenAndServeTLS("cert.pem", "key.pem"))
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
132 第5 章 網絡編程
5.3 RPC 編程
RPC(Remote Procedure Call,遠程過程調用)是一種經過網絡從遠程計算機程序上請求服
務,而不須要了解底層網絡細節的應用程序通訊協議。RPC協議構建於TCP或UDP,或者是 HTTP
之上,容許開發者直接調用另外一臺計算機上的程序,而開發者無需額外地爲這個調用過程編寫網
絡通訊相關代碼,使得開發包括網絡分佈式程序在內的應用程序更加容易。
RPC 採用客戶端—服務器(Client/Server)的工做模式。請求程序就是一個客戶端(Client),
而服務提供程序就是一個服務器(Server)。當執行一個遠程過程調用時,客戶端程序首先發送一
個帶有參數的調用信息到服務端,而後等待服務端響應。在服務端,服務進程保持睡眠狀態直到
客戶端的調用信息到達爲止。當一個調用信息到達時,服務端得到進程參數,計算出結果,並向
客戶端發送應答信息,而後等待下一個調用。最後,客戶端接收來自服務端的應答信息,得到進
程結果,而後調用執行並繼續進行。
5.3.1 Go語言中的RPC支持與處理
在Go中,標準庫提供的net/rpc 包實現了 RPC 協議須要的相關細節,開發者能夠很方便地
使用該包編寫 RPC 的服務端和客戶端程序,這使得用 Go 語言開發的多個進程之間的通訊變得非
常簡單。
net/rpc包容許 RPC 客戶端程序經過網絡或是其餘 I/O 鏈接調用一個遠端對象的公開方法
(必須是大寫字母開頭、可外部調用的)。在 RPC 服務端,可將一個對象註冊爲可訪問的服務,
以後該對象的公開方法就可以以遠程的方式提供訪問。一個 RPC 服務端能夠註冊多個不一樣類型
的對象,但不容許註冊同一類型的多個對象。
一個對象中只有知足以下這些條件的方法,才能被 RPC 服務端設置爲可供遠程訪問:
必須是在對象外部可公開調用的方法(首字母大寫);
必須有兩個參數,且參數的類型都必須是包外部能夠訪問的類型或者是Go內建支持的類
型;
第二個參數必須是一個指針;
方法必須返回一個error類型的值。
以上4個條件,能夠簡單地用以下一行代碼表示:
func (t *T) MethodName(argType T1, replyType *T2) error
在上面這行代碼中,類型T、T1 和 T2 默認會使用 Go 內置的 encoding/gob 包進行編碼解碼。
關於encoding/gob 包的內容,稍後咱們將會對其進行介紹。
該方法(MethodName)的第一個參數表示由 RPC 客戶端傳入的參數,第二個參數表示要返
回給RPC客戶端的結果,該方法最後返回一個 error 類型的值。
RPC 服務端能夠經過調用 rpc.ServeConn 處理單個鏈接請求。多數狀況下,經過 TCP 或
是 HTTP 在某個網絡地址上進行監聽來建立該服務是個不錯的選擇。
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.3 RPC 編程 133
1
2
3
4
5
9
6
7
8
8
在 RPC 客戶端,Go 的 net/rpc 包提供了便利的 rpc.Dial() 和 rpc.DialHTTP() 方法
來與指定的 RPC 服務端創建鏈接。在創建鏈接以後,Go 的 net/rpc 包容許咱們使用同步或者
異步的方式接收 RPC 服務端的處理結果。調用 RPC 客戶端的 Call() 方法則進行同步處理,這
時候客戶端程序按順序執行,只有接收完 RPC 服務端的處理結果以後才能夠繼續執行後面的程
序。當調用 RPC 客戶端的 Go() 方法時,則能夠進行異步處理,RPC 客戶端程序無需等待服務
端的結果便可執行後面的程序,而當接收到 RPC 服務端的處理結果時,再對其進行相應的處理。
不管是調用 RPC 客戶端的 Call() 或者是 Go() 方法,都必須指定要調用的服務及其方法名稱,
以及一個客戶端傳入參數的引用,還有一個用於接收處理結果參數的指針。
若是沒有明確指定 RPC 傳輸過程當中使用何種編碼解碼器,默認將使用 Go 標準庫提供的
encoding/gob 包進行數據傳輸。
接下來,咱們來看一組 RPC 服務端和客戶端交互的示例程序。代碼清單5-5是RPC服務端程序。
代碼清單5-5 rpcserver.go
package server
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
註冊服務對象並開啓該 RPC 服務的代碼以下:
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
go http.Serve(l, nil)
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
134 第5 章 網絡編程
此時,RPC 服務端註冊了一個Arith類型的對象及其公開方法Arith.Multiply()和
Arith.Divide()供 RPC 客戶端調用。RPC 在調用服務端提供的方法以前,必須先與 RPC 服務
端創建鏈接,以下列代碼所示:
client, err := rpc.DialHTTP("tcp", serverAddress + ":1234")
if err != nil {
log.Fatal("dialing:", err)
}
在創建鏈接以後,RPC 客戶端能夠調用服務端提供的方法。首先,咱們來看同步調用程序順
序執行的方式:
args := &server.Args{7,8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d", args.A, args.B, reply)
此外,還能夠以異步方式進行調用,具體代碼以下:
quotient := new(Quotient)
divCall := client.Go("Arith.Divide", args, "ient, nil)
replyCall := <-divCall.Done
5.3.2 Gob簡介
Gob 是 Go 的一個序列化數據結構的編碼解碼工具,在 Go 標準庫中內置encoding/gob包
以供使用。一個數據結構使用 Gob 進行序列化以後,可以用於網絡傳輸。與 JSON 或 XML 這種
基於文本描述的數據交換語言不一樣,Gob 是二進制編碼的數據流,而且 Gob 流是能夠自解釋的,
它在保證高效率的同時,也具有完整的表達能力。
做爲針對 Go 的數據結構進行編碼和解碼的專用序列化方法,這意味着 Gob 沒法跨語言使
用。在 Go 的net/rpc包中,傳輸數據所須要用到的編碼解碼器,默認就是 Gob。因爲 Gob 僅局
限於使用 Go 語言開發的程序,這意味着咱們只能用 Go 的 RPC 實現進程間通訊。然而,大多數
時候,咱們用 Go 編寫的 RPC 服務端(或客戶端),可能更但願它是通用的,與語言無關的,無
論是Python 、 Java 或其餘編程語言實現的 RPC 客戶端,都可與之通訊。
5.3.3 設計優雅的RPC接口
Go 的net/rpc很靈活,它在數據傳輸先後實現了編碼解碼器的接口定義。這意味着,開發
者能夠自定義數據的傳輸方式以及 RPC 服務端和客戶端之間的交互行爲。
RPC 提供的編碼解碼器接口以下:
type ClientCodec interface {
WriteRequest(*Request, interface{}) error
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.4 JSON 處理 135
1
2
3
4
5
9
6
7
8
8
ReadResponseHeader(*Response) error
ReadResponseBody(interface{}) error
Close() error
}
type ServerCodec interface {
ReadRequestHeader(*Request) error
ReadRequestBody(interface{}) error
WriteResponse(*Response, interface{}) error
Close() error
}
接口ClientCodec定義了 RPC 客戶端如何在一個 RPC 會話中發送請求和讀取響應。客戶端程
序經過 WriteRequest() 方法將一個請求寫入到 RPC 鏈接中,並經過 ReadResponseHeader()
和 ReadResponseBody() 讀取服務端的響應信息。當整個過程執行完畢後,再經過 Close() 方
法來關閉該鏈接。
接口ServerCodec定義了 RPC 服務端如何在一個 RPC 會話中接收請求併發送響應。服務端
程序經過 ReadRequestHeader() 和 ReadRequestBody() 方法從一個 RPC 鏈接中讀取請求
信息,而後再經過 WriteResponse() 方法向該鏈接中的 RPC 客戶端發送響應。當完成該過程
後,經過 Close() 方法來關閉鏈接。
經過實現上述接口,咱們能夠自定義數據傳輸先後的編碼解碼方式,而不只僅侷限於 Gob。
一樣,能夠自定義RPC 服務端和客戶端的交互行爲。實際上,Go 標準庫提供的net/rpc/json
包,就是一套實現了rpc.ClientCodec和rpc.ServerCodec接口的 JSON-RPC 模塊。
5.4 JSON 處理
JSON (JavaScript Object Notation)是一種比XML更輕量級的數據交換格式,在易於人們閱
讀和編寫的同時,也易於程序解析和生成。儘管JSON是JavaScript的一個子集,但JSON採用徹底
獨立於編程語言的文本格式,且表現爲鍵/值對集合的文本描述形式(相似一些編程語言中的字
典結構),這使它成爲較爲理想的、跨平臺、跨語言的數據交換語言。
開發者能夠用 JSON 傳輸簡單的字符串、數字、布爾值,也能夠傳輸一個數組,或者一個更
複雜的複合結構。在 Web 開發領域中,JSON被普遍應用於 Web 服務端程序和客戶端之間的數據
通訊,但也不只僅侷限於此,其應用範圍很是廣闊,好比做爲Web Services API輸出的標準格式,
又或是用做程序網絡通訊中的遠程過程調用(RPC)等。
關於JSON的更多信息,請訪問JSON官方網站 http://json.org/ 查閱。
Go語言內建對JSON的支持。使用Go語言內置的encoding/json 標準庫,開發者能夠輕鬆
使用Go程序生成和解析JSON格式的數據。在Go語言實現JSON的編碼和解碼時,遵循RFC4627
協議標準。
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
136 第5 章 網絡編程
5.4.1 編碼爲JSON格式
使用json.Marshal()函數能夠對一組數據進行JSON格式的編碼。json.Marshal()函數
的聲明以下:
func Marshal(v interface{}) ([]byte, error)
假若有以下一個Book類型的結構體:
type Book struct {
Title string
Authors []string
Publisher string
IsPublished bool
Price float
}
而且有以下一個 Book 類型的實例對象:
gobook := Book{
"Go語言編程",
["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"ituring.com.cn",
true,
9.99
}
而後,咱們可使用 json.Marshal() 函數將gobook實例生成一段JSON格式的文本:
b, err := json.Marshal(gobook)
若是編碼成功,err 將賦於零值 nil,變量b 將會是一個進行JSON格式化以後的[]byte
類型:
b == []byte(`{
"Title": "Go語言編程",
"Authors": ["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"Publisher": "ituring.com.cn",
"IsPublished": true,
"Price": 9.99
}`)
當咱們調用json.Marshal(gobook)語句時,會遞歸遍歷gobook對象,若是發現gobook這個
數據結構實現了json.Marshaler接口且包含有效的值,Marshal()就會調用其MarshalJSON()
方法將該數據結構生成 JSON 格式的文本。
Go語言的大多數數據類型均可以轉化爲有效的JSON文本,但channel、complex和函數這幾種
類型除外。
若是轉化前的數據結構中出現指針,那麼將會轉化指針所指向的值,若是指針指向的是零值,
那麼null將做爲轉化後的結果輸出。
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.4 html
在Go中,JSON轉化先後的數據類型映射以下。
布爾值轉化爲JSON後仍是布爾類型。
浮點數和整型會被轉化爲JSON裏邊的常規數字。
字符串將以UTF-8編碼轉化輸出爲Unicode字符集的字符串,特殊字符好比<將會被轉義爲
\u003c。
數組和切片會轉化爲JSON裏邊的數組,但[]byte類型的值將會被轉化爲 Base64 編碼後
的字符串,slice類型的零值會被轉化爲 null。
結構體會轉化爲JSON對象,而且只有結構體裏邊以大寫字母開頭的可被導出的字段纔會
被轉化輸出,而這些可導出的字段會做爲JSON對象的字符串索引。
轉化一個map類型的數據結構時,該數據的類型必須是 map[string]T(T能夠是
encoding/json 包支持的任意數據類型)。
5.4.2 解碼JSON數據
可使用json.Unmarshal()函數將JSON格式的文本解碼爲Go裏邊預期的數據結構。
json.Unmarshal()函數的原型以下:
func Unmarshal(data []byte, v interface{}) error
該函數的第一個參數是輸入,即JSON格式的文本(比特序列),第二個參數表示目標輸出容器,
用於存放解碼後的值。
要解碼一段JSON數據,首先須要在Go中建立一個目標類型的實例對象,用於存放解碼後
的值:
var book Book
而後調用 json.Unmarshal() 函數,將 []byte 類型的JSON數據做爲第一個參數傳入,將 book
實例變量的指針做爲第二個參數傳入:
err := json.Unmarshal(b, &book)
若是 b 是一個有效的JSON數據並能和 book 結構對應起來,那麼JSON解碼後的值將會一一
存放到book結構體中。解碼成功後的 book 數據以下:
book := Book{
"Go語言編程",
["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"ituring.com.cn",
true,
9.99
}
咱們不由好奇,Go是如何將JSON數據解碼後的值一一準確無誤地關聯到一個數據結構中的
相應字段呢?
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
138 第5 章 網絡編程
實際上,json.Unmarshal()函數會根據一個約定的順序查找目標結構中的字段,若是找到
一個即發生匹配。假設一個JSON對象有個名爲"Foo"的索引,要將"Foo"所對應的值填充到目標
結構體的目標字段上,json.Unmarshal()將會遵循以下順序進行查找匹配:
(1) 一個包含Foo標籤的字段;
(2) 一個名爲Foo的字段;
(3) 一個名爲Foo或者Foo或者除了首字母其餘字母不區分大小寫的名爲Foo的字段。
這些字段在類型聲明中必須都是以大寫字母開頭、可被導出的字段。
可是當JSON數據裏邊的結構和Go裏邊的目標類型的結構對不上時,會發生什麼呢?示例代
碼以下:
b := []byte(`{"Title": "Go語言編程", "Sales": 1000000}`)
var gobook Book
err := json.Unmarshal(b, &gobook)
若是JSON中的字段在Go目標類型中不存在,json.Unmarshal()函數在解碼過程當中會丟棄
該字段。在上面的示例代碼中,因爲Sales字段並無在Book類型中定義,因此會被忽略,只有
Title這個字段的值纔會被填充到gobook.Title中。
這個特性讓咱們能夠從同一段JSON數據中篩選指定的值填充到多個Go語言類型中。固然,
前提是已知JSON數據的字段結構。這也一樣意味着,目標類型中不可被導出的私有字段(非首
字母大寫)將不會受到解碼轉化的影響。
但若是JSON的數據結構是未知的,應該如何處理呢?
5.4.3 解碼未知結構的JSON數據
咱們已經知道,Go語言支持接口。在Go語言裏,接口是一組預約義方法的組合,任何一個
類型都可經過實現接口預約義的方法來實現,且無需顯示聲明,因此沒有任何方法的空接口能夠
表明任何類型。換句話說,每個類型其實都至少實現了一個空接口。
Go內建這樣靈活的類型系統,向咱們傳達了一個頗有價值的信息:空接口是通用類型。如
果要解碼一段未知結構的JSON,只需將這段JSON數據解碼輸出到一個空接口便可。在解碼JSON
數據的過程當中,JSON數據裏邊的元素類型將作以下轉換:
JSON中的布爾值將會轉換爲Go中的bool類型;
數值會被轉換爲Go中的float64類型;
字符串轉換後仍是string類型;
JSON數組會轉換爲[]interface{}類型;
JSON對象會轉換爲map[string]interface{}類型;
null值會轉換爲nil。
在Go的標準庫encoding/json包中,容許使用map[string]interface{}和[]interface{}
類型的值來分別存放未知結構的JSON對象或數組,示例代碼以下:
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.4 JSON 處理 139
1
2
3
4
5
9
6
7
8
8
b := []byte(`{
"Title": "Go語言編程",
"Authors": ["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"Publisher": "ituring.com.cn",
"IsPublished": true,
"Price": 9.99,
"Sales": 1000000
}`)
var r interface{}
err := json.Unmarshal(b, &r)
在上述代碼中,r被定義爲一個空接口。json.Unmarshal() 函數將一個JSON對象解碼到
空接口r中,最終r將會是一個鍵值對的 map[string]interface{} 結構:
map[string]interface{}{
"Title": "Go語言編程",
"Authors": ["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"Publisher": "ituring.com.cn",
"IsPublished": true,
"Price": 9.99,
"Sales": 1000000
}
要訪問解碼後的數據結構,須要先判斷目標結構是否爲預期的數據類型:
gobook, ok := r.(map[string]interface{})
而後,咱們能夠經過for循環搭配range語句一一訪問解碼後的目標數據:
if ok {
for k, v := range gobook {
switch v2 := v.(type) {
case string:
fmt.Println(k, "is string", v2)
case int:
fmt.Println(k, "is int", v2)
case bool:
fmt.Println(k, "is bool", v2)
case []interface{}:
fmt.Println(k, "is an array:")
for i, iv := range v2 {
fmt.Println(i, iv)
}
default:
fmt.Println(k, "is another type not handle yet")
}
}
}
雖然有些煩瑣,但的確是一種解碼未知結構的JSON數據的安全方式。
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
140 第5 章 網絡編程
5.4.4 JSON的流式讀寫
Go內建的encoding/json 包還提供Decoder和Encoder兩個類型,用於支持JSON數據的
流式讀寫,並提供NewDecoder()和NewEncoder()兩個函數來便於具體實現:
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
代碼清單5-6從標準輸入流中讀取JSON數據,而後將其解碼,但只保留Title字段(書名),
再寫入到標準輸出流中。
代碼清單5-6 jsondemo.go
package main
import (
"encoding/json"
"log"
"os"
)
func main() {
dec := json.NewDecoder(os.Stdin)
enc := json.NewEncoder(os.Stdout)
for {
var v map[string]interface{}
if err := dec.Decode(&v); err != nil {
log.Println(err)
return
}
for k := range v {
if k != "Title" {
v[k] = nil, false
}
}
if err := enc.Encode(&v); err != nil {
log.Println(err)
}
}
}
使用Decoder 和Encoder對數據流進行處理能夠應用得更爲普遍些,好比讀寫 HTTP 鏈接、
WebSocket或文件等,Go的標準庫net/rpc/jsonrpc就是一個應用了Decoder和Encoder的實
際例子。
5.5 網站開發
在這一節中,咱們將向你按部就班地講解怎樣使用Go進行Web開發。本節將圍繞一個簡單的
相冊程序進行,儘管示例程序比較簡單,但體現的都是使用Go開發網站的幾處關鍵環節,旨在
讓你係統瞭解基於原生的Go語言開發Web應用程序的基本思路及其相關細節的具體實現。
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.5 網站開發 141
1
2
3
4
5
9
6
7
8
8
5.5.1 最簡單的網站程序
讓咱們從最簡單的網站程序入手。
還記得第1章中編寫的那個最簡單的Hello world示例程序嗎?如今稍微調整幾行代碼,將其
改形成一個可用瀏覽器打開並能在網頁中顯示「Hello, world!」的小程序。打開你最喜好的編輯
器,編寫如代碼清單5-7所示的幾行代碼(示例中筆者使用Vim編輯器並將其存盤爲 hello.go)。
代碼清單5-7 hello.go
package main
import (
"io"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello, world!")
}
func main() {
http.HandleFunc("/hello", helloHandler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
咱們引入了Go語言標準庫中的 net/http 包,主要用於提供Web服務,響應並處理客戶端
(瀏覽器)的HTTP請求。同時,使用io包而不是fmt包來輸出字符串,這樣源文件編譯成可執行
文件後,體積要小不少,運行起來也更省資源。
接下來,讓咱們簡單地瞭解Go語言的http包在上述示例中所作的工做。
5.5.2 net/http包簡介
能夠看到,咱們在main()方法中調用了http.HandleFunc(),該方法用於分發請求,即針
對某一路徑請求將其映射到指定的業務邏輯處理方法中。若是你有其餘編程語言(好比Ruby、
Python或者PHP等)的Web開發經驗,能夠將其形象地理解爲提供相似URL路由或者URL映射之
類的功能。在hello.go中,http.HandleFunc()方法接受兩個參數,第一個參數是HTTP請求的
目標路徑"/hello",該參數值能夠是字符串,也能夠是字符串形式的正則表達式,第二個參數指定
具體的回調方法,好比helloHandler。當咱們的程序運行起來後,訪問http://localhost:8080/hello ,
程序就會去調用helloHandler()方法中的業務邏輯程序。
在上述例子中, helloHandler() 方法是http.HandlerFunc 類型的實例, 並傳入
http.ResponseWriter和http.Request做爲其必要的兩個參數。http.ResponseWriter 類
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
142 第5 章 網絡編程
型的對象用於包裝處理HTTP服務端的響應信息。咱們將字符串"Hello, world! "寫入類型爲
http.ResponseWriter的w實例中,便可將該字符串數據發送到HTTP客戶端。第二個參數
r *http.Request表示的是這次HTTP請求的一個數據結構體,即表明一個客戶端,不過該示例
中咱們還沒有用到它。
咱們還看到,在main()方法中調用了http.ListenAndServe(),該方法用於在示例中監
聽 8080 端口,接受並調用內部程序來處理鏈接到此端口的請求。若是端口監聽失敗,會調用
log.Fatal()方法輸出異常出錯信息。
正如你所見,main()方法中的短短兩行即開啓了一個HTTP服務,使用Go語言的net/http
包搭建一個Web是如此簡單!固然,net/http包的做用遠不止這些,咱們只用到其功能的一小
部分。
試着編譯並運行當前的這份 hello.go 源文件:
$ go run hello.go
而後訪問 http://localhost:8080/hello,看會發生什麼。
5.5.3 開發一個簡單的相冊網站
本節咱們將綜合以前介紹的網站開發相關知識,一步步介紹如何開發一個雖然簡單但五臟俱
全的相冊網站。
1. 新建工程
首先建立一個用於存放工程源代碼的目錄並切換到該目錄中去,隨後建立一個名爲
photoweb.go 的文件,用於後面編輯咱們的代碼:
$ mkdir -p photoweb/uploads
$ cd photoweb
$ touch photoweb.go
咱們的示例程序不是再造一個Flickr那樣的網站或者比其更強大的圖片分享網站,雖然咱們
可能很想這麼玩。不過仍是先讓咱們快速開發一個簡單的網站小程序,暫且只實現如下最基本的
幾個功能:
支持圖片上傳;
在網頁中能夠查看已上傳的圖片;
能看到全部上傳的圖片列表;
能夠刪除指定的圖片。
功能很少,也很簡單。在大概瞭解以前的網頁輸出Hello world示例後,想必你已經知道能夠
引入net/http包來提供更多的路由分派並編寫與之對應的業務邏輯處理方法,只不過會比輸出
一行Hello, world!多一些環節,還有些細節須要關注和處理。
2. 使用net/http包提供網絡服務
接下來,咱們繼續使用Go標準庫中的net/http包來一步步構建整個相冊程序的網絡服務。
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.5 網站開發 143
1
2
3
4
5
9
6
7
8
8
上傳圖片
先從最基本的圖片上傳着手,具體代碼如代碼清單5-8所示。
代碼清單5-8 photoweb.go
package main
import (
"io"
"log"
"net/http"
)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
io.WriteString(w, "<form method=\"POST\" action=\"/upload\" "+
" enctype=\"multipart/form-data\">"+
"Choose an image to upload: <input name=\"image\" type=\"file\" />"+
"<input type=\"submit\" value=\"Upload\" />"+
"</form>")
return
}
}
func main() {
http.HandleFunc("/upload", uploadHandler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
能夠看到,結合main()和uploadHandler()方法,針對 HTTP GET 方式請求 /upload 路徑,
程序將會往http.ResponseWriter類型的實例對象w中寫入一段HTML文本,即輸出一個HTML
上傳表單。若是咱們使用瀏覽器訪問這個地址,那麼網頁上將會是一個能夠上傳文件的表單。
光有上傳表單還不能完成圖片上傳,服務端程序還必須有接收上傳圖片的相關處理。針對上
傳表單提交過來的文件,咱們對uploadHandler()方法再添加些業務邏輯程序:
const (
UPLOAD_DIR = "./uploads"
)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
io.WriteString(w, "<form method=\"POST\" action=\"/upload\" "+
" enctype=\"multipart/form-data\">"+
"Choose an image to upload: <input name=\"image\" type=\"file\" />"+
"<input type=\"submit\" value=\"Upload\" />"+
"</form>")
return
}
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
144 第5 章 網絡編程
if r.Method == "POST" {
f, h, err := r.FormFile("image")
if err != nil {
http.Error(w, err.Error(),
http.StatusInternalServerError)
return
}
filename := h.Filename
defer f.Close()
t, err := os.Create(UPLOAD_DIR + "/" + filename)
if err != nil {
http.Error(w, err.Error(),
http.StatusInternalServerError)
return
}
defer t.Close()
if _, err := io.Copy(t, f); err != nil {
http.Error(w, err.Error(),
http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view?id="+filename,
http.StatusFound)
}
}
若是是客戶端發起的HTTP POST 請求,那麼首先從表單提交過來的字段尋找名爲 image 的文
件域並對其接值,調用r.FormFile()方法會返回3個值,各個值的類型分別是multipart.File、
*multipart.FileHeader和error。
若是上傳的圖片接收不成功,那麼在示例程序中返回一個HTTP服務端的內部錯誤給客戶端。
若是上傳的圖片接收成功,則將該圖片的內容複製到一個臨時文件裏。若是臨時文件建立失
敗,或者圖片副本保存失敗,都將觸發服務端內部錯誤。
若是臨時文件建立成功而且圖片副本保存成功,即表示圖片上傳成功,就跳轉到查看圖片頁
面。此外,咱們還定義了兩個defer語句,不管圖片上傳成功仍是失敗,當uploadHandler()
方法執行結束時,都會先關閉臨時文件句柄,繼而關閉圖片上傳到服務器文件流的句柄。
別忘了在程序開頭引入io/ioutil這個包,由於示例程序中用到了ioutil.TempFile()這
個方法。
當圖片上傳成功後,咱們便可在網頁上查看這張圖片,順便確認圖片是否真正上傳到了服務
端。接下來在網頁中呈現這張圖片。
在網頁上顯示圖片
要在網頁中顯示圖片,必須有一個能夠訪問到該圖片的網址。在前面的示例代碼中,圖片上
傳成功後會跳轉到/view?id=<ImageId>這樣的網址,所以咱們的程序要可以將對 /view 路徑的
訪問映射到某個具體的業務邏輯處理方法。
首先,在photoweb程序中新增一個名爲viewHanlder()的方法,其代碼以下:
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.5 網站開發 145
1
2
3
4
5
9
6
7
8
8
func viewHandler(w http.ResponseWriter, r *http.Request) {
imageId = r.FormValue("id")
imagePath = UPLOAD_DIR + "/" + imageId
w.Header().Set("Content-Type", "image")
http.ServeFile(w, r, imagePath)
}
在上述代碼中,咱們首先從客戶端請求中對參數進行接值。r.FormValue("id")便可獲得
客戶端請求傳遞的圖片惟一ID,而後咱們將圖片ID結合以前保存圖片用的目錄進行組裝,便可得
到文件在服務器上的存放路徑。接着,調用http.ServeFile()方法將該路徑下的文件從磁盤中
讀取並做爲服務端的返回信息輸出給客戶端。同時,也將HTTP響應頭輸出格式預設爲image類
型。這是一種比較簡單的示意寫法,實際上應該嚴謹些,準確解析出文件的MimeType並將其做
爲Content-Type進行輸出,具體可參考Go語言標準庫中的http.DetectContentType()方法
和mime包提供的相關方法。
完成viewHandler()的業務邏輯後,咱們將該方法註冊到程序的main()方法中,與/view
路徑訪問造成映射關聯。main()方法的代碼以下:
func main() {
http.HandleFunc("/view", viewHandler)
http.HandleFunc("/upload", uploadHandler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
這樣當客戶端(瀏覽器)訪問/view路徑並傳遞id參數時,便可直接以HTTP形式看到圖片的
內容。在網頁上,將會呈現一張可視化的圖片。
處理不存在的圖片訪問
理論上,只要是uploads/ 目錄下有的圖片,都可以訪問到,但咱們仍是假設有意外狀況,比
如網址中傳入的圖片ID在 uploads/ 沒有對應的文件,這時,咱們的viewHandler()方法就顯得
很脆弱了。無論是給出友好的錯誤提示仍是返回404頁面,都應該對這種狀況做相應處理。咱們
不妨先以最簡單有效的方式對其進行處理,修改viewHandler()方法,具體以下:
func viewHandler(w http.ResponseWriter, r *http.Request) {
imageId = r.FormValue("id")
imagePath = UPLOAD_DIR + "/" + imageId
if exists := isExists(imagePath);!exists {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "image")
http.ServeFile(w, r, imagePath)
}
func isExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
146 第5 章 網絡編程
return true
}
return os.IsExist(err)
}
同時,咱們增長了isExists()輔助函數,用於檢查文件是否真的存在。
列出全部已上傳圖片
應該有個入口,能夠看到全部已上傳的圖片。對於全部列出的這些圖片,咱們能夠選擇進行
查看或者刪除等操做。下面假設在訪問首頁時列出全部上傳的圖片。
因爲咱們將客戶端上傳的圖片所有保存在工程的./uploads目錄下,因此程序中應該有個名叫
listHandler()的方法,用於在網頁上列出該目錄下存放的全部文件。暫時咱們不考慮以縮略
圖的形式列出全部已上傳圖片,只需列出可供訪問的文件名稱便可。下面咱們就來實現這個
listHandler()方法:
func listHandler(w http.ResponseWriter, r *http.Request) {
fileInfoArr, err := ioutil.ReadDir("./uploads")
if err != nil {
http.Error(w, err.Error(),
http.StatusInternalServerError)
return
}
var listHtml string
for _, fileInfo := range fileInfoArr {
imgid := fileInfo.Name
listHtml += "<li><a href=\"/view?id="+imgid+"\">imgid</a></li>"
}
io.WriteString(w, "<ol>"+listHtml+"</ol>")
}
從上面的listHandler()方法中能夠看到,程序先從./uploads目錄中遍歷獲得全部文件並賦
值到fileInfoArr 變量裏。fileInfoArr 是一個數組,其中的每個元素都是一個文件對象。
而後,程序遍歷fileInfoArr數組並從中獲得圖片的名稱,用於在後續的HTML片斷中顯示文件
名和傳入的參數內容。listHtml變量用於在for循序中將圖片名稱一一串聯起來生成一段
HTML,最後調用io.WriteString()方法將這段HTML輸出返回給客戶端。
而後在photoweb. go程序的main()方法中,咱們將對首頁的訪問映射到listHandler()方
法。main()方法的代碼以下:
func main() {
http.HandleFunc("/", listHandler)
http.HandleFunc("/view", viewHandler)
http.HandleFunc("/upload", uploadHandler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
圖靈社區會員 soooldier(soooldier@live.com) 專享 尊重版權
5.5
nginx