您好,登錄后才能下訂單哦!
HTTP協(xié)議是基于TCP/IP協(xié)議棧的,并且是一個(gè)面向普通文本的協(xié)議。原則上,使用任何一個(gè)文本編輯器,都可以寫(xiě)出一個(gè)完整的HTTP請(qǐng)求報(bào)文。只要搞清楚了請(qǐng)求報(bào)文的頭部(header、請(qǐng)求頭)和主體(body、請(qǐng)求體)應(yīng)該包含的內(nèi)容。
如果只是訪問(wèn)基于HTTP協(xié)議的網(wǎng)絡(luò)服務(wù),那么使用net/http包中的程序?qū)嶓w會(huì)非常方便。
調(diào)用http.Get函數(shù),只需要傳遞給它一個(gè)URL即可:
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
resp, err := http.Get("http://baidu.com")
if err != nil {
fmt.Fprintf(os.Stderr, "request sending error: %v\n", err)
return
}
defer resp.Body.Close()
line := resp.Proto + " " + resp.Status
fmt.Println("返回的第一行的內(nèi)容:", line)
}
http.Get函數(shù)會(huì)返回兩個(gè)結(jié)果:
http.Get函數(shù)會(huì)在內(nèi)部使用缺省的HTTP客戶端,并且調(diào)用它的Get方法來(lái)完成功能。這個(gè)缺省的HTTP客戶端就是net/http包中的公開(kāi)變量DefaultClient,源碼中是這樣的:
// 源碼中提供的缺省的客戶端
var DefaultClient = &Client{}
// 使用缺省的客戶端調(diào)用Get方法
func Get(url string) () {
return DefaultClient.Get(url)
}
所以下面的這兩行代碼:
var httpClient http.Client
resp, err := httpClient.Get(utl)
與示例中的這一行代碼:
resp, err := http.Get(url)
是等價(jià)的。這里只是不使用DefaultClient而是自己創(chuàng)建了一個(gè)客戶端。
http.Client是一個(gè)結(jié)構(gòu)體,并且它包含的字段都是公開(kāi)的:
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
該類型是開(kāi)箱即用的,因?yàn)樗乃凶侄?,要么存在相?yīng)的缺省值,要么其零值直接就可以使用,并且代表著特定的含義。
主要看下Transport字段,該字段向網(wǎng)絡(luò)服務(wù)發(fā)送HTTP請(qǐng)求,并從網(wǎng)絡(luò)服務(wù)接收HTTP響應(yīng)。該字段的方法RoundTrip應(yīng)該實(shí)現(xiàn)單次HTTP事務(wù)(或者說(shuō)基于HTTP協(xié)議的單次交互)需要的所有步驟。這個(gè)字段是一個(gè)接口:
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
并且該字段有一個(gè)由http.DefaultTransport變量的缺省值:
func (c *Client) transport() RoundTripper {
if c.Transport != nil {
return c.Transport
}
return DefaultTransport
}
在初始化http.Client類型的時(shí)候,如果沒(méi)有顯式的為該字段賦值,這個(gè)Client字段就會(huì)直接使用DefaultTransport。
該字段是單次HTTP事務(wù)的超時(shí)時(shí)間,它是time.Duration類型。它的零值是可用的,用于表示沒(méi)有設(shè)置超時(shí)時(shí)間。
http.Transport類型是一個(gè)結(jié)構(gòu)體,該類型包含的字段很多。這里通過(guò)http.Client結(jié)構(gòu)體中的Transport字段的缺省值DefaultTransport,來(lái)深入了解一下。DefaultTransport是一個(gè)*http.Transport的結(jié)構(gòu)體,做了一些默認(rèn)的設(shè)置:
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
這里Transport結(jié)構(gòu)體的指針就是就是RoundTripper接口的默認(rèn)實(shí)現(xiàn):
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
return t.roundTrip(req)
}
這個(gè)類型是可以被復(fù)用的,并且也推薦被復(fù)用。同時(shí)它也是并發(fā)安全的。所以http.Client類型也是一樣,推薦復(fù)用,并且并發(fā)安全。
看上面的默認(rèn)設(shè)置,http.Transport類型,內(nèi)部的DialContext字段會(huì)使用net.Dialer類型的值,并且把Timeout設(shè)置為30秒。仔細(xì)看,該值是一個(gè)方法,這里把Dialer值的DialContext方法賦值給了DefaultTransport里的同名字段,并且已經(jīng)設(shè)置好了調(diào)用該方法時(shí)的結(jié)構(gòu)體。
http.Transport類型還包含了很多其他的字段,其中有一些字段是關(guān)于操作超時(shí)的:
TLS 是 Transport Layer Security 的縮寫(xiě),可以被翻譯為傳輸層安全。
此外,還有一些與IdleConnTimeout相關(guān)的字段值也值得關(guān)注:
MaxIdleConns
無(wú)論當(dāng)前訪問(wèn)了多少個(gè)網(wǎng)絡(luò)服務(wù),MaxIdleConns字段只會(huì)對(duì)空閑連接的總數(shù)做限定。
MaxIdleConnsPerHost
而MaxIdleConnsPerHost字段限定的是,每一個(gè)網(wǎng)絡(luò)服務(wù)的最大空閑連接數(shù)。每一個(gè)網(wǎng)絡(luò)服務(wù)都有自己的網(wǎng)絡(luò)地址,可能會(huì)使用不同的網(wǎng)絡(luò)協(xié)議,對(duì)于一些HTTP請(qǐng)求也可能會(huì)用到代理。地址、協(xié)議、代理,通脫這三個(gè)方面的具體情況來(lái)鑒別不同的網(wǎng)絡(luò)服務(wù)。
MaxIdleConnsPerHost是有缺省值的,由常量http.DefaultMaxIdleConnsPerHost表示,值為2:
const DefaultMaxIdleConnsPerHost = 2
func (t *Transport) maxIdleConnsPerHost() int {
if v := t.MaxIdleConnsPerHost; v != 0 {
return v
}
return DefaultMaxIdleConnsPerHost
}
在默認(rèn)情況下,每一個(gè)網(wǎng)絡(luò)服務(wù),它的空閑連接數(shù)最多只能由2個(gè)。
MaxConnsPerHost
MaxConnsPerHost字段限制針對(duì)每一個(gè)網(wǎng)絡(luò)服務(wù)的最大連接數(shù),不論這些鏈接是否是空閑的。并且,該字段沒(méi)有相應(yīng)的缺省值,零值就是不做限制。
小結(jié)
不限制連接數(shù),默認(rèn)也不限制每一個(gè)網(wǎng)絡(luò)服務(wù)的連接數(shù)。要限制整體的空閑連接數(shù)以及嚴(yán)格限制對(duì)每一個(gè)網(wǎng)絡(luò)服務(wù)的空閑連接數(shù)。
簡(jiǎn)單說(shuō)明一下,為什么會(huì)出現(xiàn)空閑的連接。
HTTP協(xié)議的請(qǐng)求頭里有一個(gè)Connection。在HTTP協(xié)議的1.1版本中,默認(rèn)值是“keep-alive”。在這種情況下的網(wǎng)絡(luò)連接是持久連接的,它們會(huì)在當(dāng)前的HTTP事務(wù)完成后仍然保持著連通性,因此是可以被復(fù)用的。
既然連接可以被復(fù)用,就會(huì)有兩種可能:
另外,如果分配給某一個(gè)網(wǎng)絡(luò)服務(wù)的連接過(guò)多的話,也可能會(huì)導(dǎo)致空閑連接的產(chǎn)生。因?yàn)闆](méi)一個(gè)HTTP請(qǐng)求只會(huì)使用一個(gè)空閑的連接。所以,在大多數(shù)情況下,都需要限制空閑連接數(shù)。
關(guān)閉keep-alive
另外,請(qǐng)求頭的Connection還可以設(shè)置為“close”,這樣就徹徹底杜絕了空閑連接的生成。這會(huì)告訴網(wǎng)絡(luò)服務(wù),這個(gè)網(wǎng)絡(luò)連接不必保持,當(dāng)前的HTTP事務(wù)完成后就可以斷開(kāi)它了。做法是在初始化Transport值的時(shí)候,將DisableKeepAlives字段設(shè)置為true。
這么做的話,每次提交HTTP請(qǐng)求,就會(huì)產(chǎn)生一個(gè)新的網(wǎng)絡(luò)連接。這樣會(huì)明顯的加重網(wǎng)絡(luò)服務(wù)以及客戶端的負(fù)載,并會(huì)讓每個(gè)HTTP事務(wù)都耗費(fèi)更多的時(shí)間。所以默認(rèn)不設(shè)置這個(gè)DisableKeepAlives字段。
http.Transport類型,內(nèi)部的DialContext字段會(huì)使用net.Dialer類型的值。在net.Dialer類型中,也有一個(gè)KeepAlive字段。該字段是直接作用在底層的socket上的。
它的背后是一種針對(duì)網(wǎng)絡(luò)連接(更確切的是說(shuō),是TCP連接)的存活探測(cè)機(jī)制。它的值用于表示每間隔多長(zhǎng)時(shí)間發(fā)送一次探測(cè)包。當(dāng)該值不大于0是,則表示不開(kāi)啟這種機(jī)制。
DefaultTransport會(huì)把這個(gè)字段設(shè)置為30秒。
自定義Client和Transport使用的示例:
package main
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"sync"
"time"
)
var domains = []string{
"baidu.com",
"sina.com.cn",
"www.baidu.com",
"www.sina.com.cn",
"tieba.baidu.com",
"news.baidu.com",
"news.sina.com.cn",
}
func main() {
myTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 15 * time.Second,
DualStack: true,
}).DialContext,
MaxConnsPerHost: 2,
MaxIdleConns: 10,
MaxIdleConnsPerHost: 2,
IdleConnTimeout: 30 * time.Second,
ResponseHeaderTimeout: 0,
ExpectContinueTimeout: 1 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
myClient := http.Client{
Transport: myTransport,
Timeout: 20 * time.Second,
}
var wg sync.WaitGroup
for _, domain := range domains {
wg.Add(1)
go func(domain string) {
var logBuf strings.Builder
var diff time.Duration
defer func() {
logBuf.WriteString(fmt.Sprintf("持續(xù)時(shí)間: %s\n", diff))
fmt.Println(logBuf.String())
wg.Done()
}()
url := "https://" + domain
logBuf.WriteString(fmt.Sprintf("發(fā)送請(qǐng)求: %s\n", url))
tStart := time.Now()
resp, err := myClient.Get(url)
diff = time.Now().Sub(tStart)
if err != nil {
logBuf.WriteString(fmt.Sprintf("request get error: %v\n", err))
return
}
defer resp.Body.Close()
line := resp.Proto + " " + resp.Status
logBuf.WriteString(fmt.Sprintf("response: %s\n", line))
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
logBuf.WriteString(fmt.Sprintf("get data error: %v\n", err))
return
}
index1 := strings.Index(string(data), "<title>")
index2 := strings.Index(string(data), "</title>")
if index1 > 0 && index2 > 0 {
logBuf.WriteString(fmt.Sprintf("title: %s\n", string(data)[index1+len("<title>"):index2]))
}
}(domain)
}
wg.Wait()
fmt.Println("All Done")
}
http.Server類型與http.Client是相對(duì)應(yīng)的。http.Server代表的是基于HTTP協(xié)議的服務(wù)端,或者說(shuō)網(wǎng)絡(luò)服務(wù)。
http.Server類型的ListenAndServe方法的功能是:監(jiān)聽(tīng)一個(gè)基于TCP協(xié)議的網(wǎng)絡(luò)地址,并對(duì)接收到的HTTP請(qǐng)求進(jìn)行處理。這個(gè)方法會(huì)默認(rèn)開(kāi)啟針對(duì)網(wǎng)絡(luò)連接的存活探測(cè)機(jī)制,以保證連接是持久的。同時(shí),該方法會(huì)一直執(zhí)行,直到有嚴(yán)重的錯(cuò)誤發(fā)生或者被外界關(guān)掉。當(dāng)被外界關(guān)掉時(shí),它會(huì)返回一個(gè)由http.ErrServerClosed變量代表的錯(cuò)誤值。
這個(gè)ListenAndServe方法主要會(huì)做以下幾件事情:
這里又牽出兩個(gè)問(wèn)題:
net.Listen函數(shù)的作用:
再往下深入的話,就會(huì)涉及到net.socket函數(shù)以及相關(guān)的socket知識(shí)。就此打住。
在一個(gè)for循環(huán)中,網(wǎng)絡(luò)監(jiān)聽(tīng)器Accept方法會(huì)不斷地調(diào)用,該方法的源碼如下:
type tcpKeepAliveListener struct {
*net.TCPListener
}
func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
tc, err := ln.AcceptTCP()
if err != nil {
return nil, err
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
Accept方法會(huì)返回兩個(gè)結(jié)果值:
當(dāng)錯(cuò)誤值不為nil時(shí),如果此時(shí)是一個(gè)暫時(shí)性的錯(cuò)誤,那么循環(huán)的下一次迭代將會(huì)在一段時(shí)間之后開(kāi)始執(zhí)行。否則,循環(huán)會(huì)被終止。
如果沒(méi)有錯(cuò)誤,返回的錯(cuò)誤值就是nil。那么這里的程序?qū)?huì)把它的第一個(gè)結(jié)果值包裝成一個(gè)*http.conn類型的值,然后通過(guò)在新的goroutine中調(diào)用這個(gè)conn值的serve方法,來(lái)對(duì)當(dāng)前的HTTP請(qǐng)求進(jìn)行處理。
上面最后說(shuō)的處理的細(xì)節(jié)還是很多的:
這些都沒(méi)有一一說(shuō)明,建議去看下源碼。
在下面的示例中,啟動(dòng)了3個(gè)Server。啟動(dòng)后,可以用瀏覽器訪問(wèn)進(jìn)行驗(yàn)證:
package main
import (
"fmt"
"net/http"
"os"
"sync"
)
var wg sync.WaitGroup
// 一般沒(méi)有這么用的,http.Server的Handler字段
// 要么是nil,就用包里的http.DefaultServeMux
// 要么用NewServeMux()來(lái)創(chuàng)建一個(gè)*http.ServeMux
// 我這里按照http.Handler接口的要求實(shí)現(xiàn)了一個(gè),賦值給Handler字段
// 這個(gè)自定義的Handler不支持路由
func startServer1() {
defer wg.Done()
var httpServer http.Server
httpServer.Addr = "127.0.0.1:8001"
httpServer.Handler = http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
fmt.Println(*r)
fmt.Fprint(w, "Hello World")
},
)
fmt.Println("啟動(dòng)服務(wù),訪問(wèn): http://127.0.0.1:8001")
if err := httpServer.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
fmt.Println("HTTP Server1 Closed.")
} else {
fmt.Fprintf(os.Stderr, "HTTP Server1 Error: %v\n", err)
}
}
}
// 這個(gè)最簡(jiǎn)單,都是調(diào)用http包里的函數(shù)。本質(zhì)上還是要調(diào)用方法的,都會(huì)用默認(rèn)的或是零值
func startServer2() {
defer wg.Done()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World\nThis is Server2")
})
fmt.Println("啟動(dòng)服務(wù),訪問(wèn): http://127.0.0.1:8002")
// 第二個(gè)參數(shù)傳nil,就是用包里的http.DefaultServeMux,或者也可以自己創(chuàng)建一個(gè)傳給第二個(gè)參數(shù)
if err := http.ListenAndServe("127.0.0.1:8002", nil); err != nil {
if err == http.ErrServerClosed {
fmt.Println("HTTP Server2 Closed.")
} else {
fmt.Fprintf(os.Stderr, "HTTP Server2 Error: %v\n", err)
}
}
}
// 這個(gè)例子里用到了解析Get請(qǐng)求的參數(shù),并且還設(shè)置了2個(gè)路由
func startServer3() {
defer wg.Done()
mux := http.NewServeMux()
mux.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/hi" {
// 這個(gè)分支應(yīng)該是進(jìn)不來(lái)的,因?yàn)橐M(jìn)入這個(gè)分支,路徑應(yīng)該必須是"/hi"
fmt.Println("Server3 hi 404")
http.NotFound(w, r)
return
}
name := r.FormValue("name")
if name == "" {
fmt.Fprint(w, "Hi!")
} else {
fmt.Fprintf(w, "Hi, %s!", name)
}
})
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World\nThis is Server3")
})
// 如果只是定義http.Server的下面2個(gè)字段,完全可以使用http.ListenAndServe函數(shù)來(lái)啟動(dòng)服務(wù)
// 這樣的用法可以對(duì)http.Server里更多的字段進(jìn)行自定義
httpServer := http.Server{
Addr: "127.0.0.1:8003",
Handler: mux,
}
fmt.Println("啟動(dòng)服務(wù),訪問(wèn): http://127.0.0.1:8003/hi?name=Adam")
if err := httpServer.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
fmt.Println("HTTP Server3 Closed.")
} else {
fmt.Fprintf(os.Stderr, "HTTP Server3 Error: %v\n", err)
}
}
}
func main() {
wg.Add(1)
go startServer1()
wg.Add(1)
go startServer2()
wg.Add(1)
go startServer3()
wg.Wait()
}
包里還提供了一個(gè)Shutdown方法,可以優(yōu)雅的停止HTTP服務(wù):
func (srv *Server) Shutdown(ctx context.Context) error {
// 內(nèi)容省略
}
我們要做的就是在需要的時(shí)候,可以調(diào)用該Shutdown方法。
這里的問(wèn)題是,調(diào)用了ListenAndServe方法之后,就進(jìn)入了無(wú)限循環(huán)的流程。這里最好是用一個(gè)goroutine來(lái)啟動(dòng)ListenAndServe方法,在goroutine外聲明http.Server。然后在主線程里等待一個(gè)信號(hào),比如是從通道接收值。這樣就可以在主線程里調(diào)用這個(gè)Shutdown方法執(zhí)行了。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。