亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入了解Go的HttpClient超時(shí)機(jī)制

 更新時(shí)間:2022年11月16日 08:36:10   作者:捉蟲(chóng)大師  
在寫(xiě)?Go?的過(guò)程中經(jīng)常對(duì)比這Java和GO語(yǔ)言的特性,踩了不少坑,也發(fā)現(xiàn)了不少有意思的地方,今天就來(lái)聊聊?Go?自帶的?HttpClient?的超時(shí)機(jī)制

Java HttpClient 超時(shí)底層原理

在介紹 Go 的 HttpClient 超時(shí)機(jī)制之前,我們先看看 Java 是如何實(shí)現(xiàn)超時(shí)的。

寫(xiě)一個(gè) Java 原生的 HttpClient,設(shè)置連接超時(shí)、讀取超時(shí)時(shí)間分別對(duì)應(yīng)到底層的方法分別是:

再追溯到 JVM 源碼,發(fā)現(xiàn)是對(duì)系統(tǒng)調(diào)用的封裝,其實(shí)不光是 Java,大部分的編程語(yǔ)言都借助了操作系統(tǒng)提供的超時(shí)能力。

然而 Go 的 HttpClient 卻提供了另一種超時(shí)機(jī)制,挺有意思,我們來(lái)盤(pán)一盤(pán)。但在開(kāi)始之前,我們先了解一下 Go 的 Context。

Go Context 簡(jiǎn)介

Context 是什么

根據(jù) Go 源碼的注釋:

// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
// Context's methods may be called by multiple goroutines simultaneously.

Context 簡(jiǎn)單來(lái)說(shuō)是一個(gè)可以攜帶超時(shí)時(shí)間、取消信號(hào)和其他數(shù)據(jù)的接口,Context 的方法會(huì)被多個(gè)協(xié)程同時(shí)調(diào)用。

Context 有點(diǎn)類似 Java 的ThreadLocal,可以在線程中傳遞數(shù)據(jù),但又不完全相同,它是顯示傳遞,ThreadLocal 是隱式傳遞,除了傳遞數(shù)據(jù)之外,Context 還能攜帶超時(shí)時(shí)間、取消信號(hào)。

Context 只是定義了接口,具體的實(shí)現(xiàn)在 Go 中提供了幾個(gè):

  • Background :空的實(shí)現(xiàn),啥也沒(méi)做
  • TODO:還不知道用什么 Context,先用 TODO 代替,也是啥也沒(méi)做的空 Context
  • cancelCtx:可以取消的 Context
  • timerCtx:主動(dòng)超時(shí)的 Context

針對(duì) Context 的三個(gè)特性,可以通過(guò) Go 提供的 Context 實(shí)現(xiàn)以及源碼中的例子來(lái)進(jìn)一步了解下。

Context 三個(gè)特性例子

這部分的例子來(lái)源于 Go 的源碼,位于 src/context/example_test.go

攜帶數(shù)據(jù)

使用 context.WithValue 來(lái)攜帶,使用 Value 來(lái)取值,源碼中的例子如下:

// 來(lái)自 src/context/example_test.go
func ExampleWithValue() {
	type favContextKey string

	f := func(ctx context.Context, k favContextKey) {
		if v := ctx.Value(k); v != nil {
			fmt.Println("found value:", v)
			return
		}
		fmt.Println("key not found:", k)
	}

	k := favContextKey("language")
	ctx := context.WithValue(context.Background(), k, "Go")

	f(ctx, k)
	f(ctx, favContextKey("color"))

	// Output:
	// found value: Go
	// key not found: color
}

取消

先起一個(gè)協(xié)程執(zhí)行一個(gè)死循環(huán),不停地往 channel 中寫(xiě)數(shù)據(jù),同時(shí)監(jiān)聽(tīng) ctx.Done() 的事件

// 來(lái)自 src/context/example_test.go
gen := func(ctx context.Context) <-chan int {
		dst := make(chan int)
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done():
					return // returning not to leak the goroutine
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}

然后通過(guò) context.WithCancel 生成一個(gè)可取消的 Context,傳入 gen 方法,直到 gen 返回 5 時(shí),調(diào)用 cancel 取消 gen 方法的執(zhí)行。

// 來(lái)自 src/context/example_test.go
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
	fmt.Println(n)
	if n == 5 {
		break
	}
}
// Output:
// 1
// 2
// 3
// 4
// 5

這么看起來(lái),可以簡(jiǎn)單理解為在一個(gè)協(xié)程的循環(huán)中埋入結(jié)束標(biāo)志,另一個(gè)協(xié)程去設(shè)置這個(gè)結(jié)束標(biāo)志。

超時(shí)

有了 cancel 的鋪墊,超時(shí)就好理解了,cancel 是手動(dòng)取消,超時(shí)是自動(dòng)取消,只要起一個(gè)定時(shí)的協(xié)程,到時(shí)間后執(zhí)行 cancel 即可。

設(shè)置超時(shí)時(shí)間有2種方式:context.WithTimeout 與 context.WithDeadline,WithTimeout 是設(shè)置一段時(shí)間后,WithDeadline 是設(shè)置一個(gè)截止時(shí)間點(diǎn),WithTimeout 最終也會(huì)轉(zhuǎn)換為 WithDeadline。

// 來(lái)自 src/context/example_test.go
func ExampleWithTimeout() {
	// Pass a context with a timeout to tell a blocking function that it
	// should abandon its work after the timeout elapses.
	ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err()) // prints "context deadline exceeded"
	}

	// Output:
	// context deadline exceeded
}

Go HttpClient 的另一種超時(shí)機(jī)制

基于 Context 可以設(shè)置任意代碼段執(zhí)行的超時(shí)機(jī)制,就可以設(shè)計(jì)一種脫離操作系統(tǒng)能力的請(qǐng)求超時(shí)能力。

超時(shí)機(jī)制簡(jiǎn)介

看一下 Go 的 HttpClient 超時(shí)配置說(shuō)明:

	client := http.Client{
		Timeout: 10 * time.Second,
	}
	
	// 來(lái)自 src/net/http/client.go
	type Client struct {
	// ... 省略其他字段
	// Timeout specifies a time limit for requests made by this
	// Client. The timeout includes connection time, any
	// redirects, and reading the response body. The timer remains
	// running after Get, Head, Post, or Do return and will
	// interrupt reading of the Response.Body.
	//
	// A Timeout of zero means no timeout.
	//
	// The Client cancels requests to the underlying Transport
	// as if the Request's Context ended.
	//
	// For compatibility, the Client will also use the deprecated
	// CancelRequest method on Transport if found. New
	// RoundTripper implementations should use the Request's Context
	// for cancellation instead of implementing CancelRequest.
	Timeout time.Duration
}

翻譯一下這個(gè)注釋:Timeout 包括了連接、redirect、讀取數(shù)據(jù)的時(shí)間,定時(shí)器會(huì)在 Timeout 時(shí)間后打斷數(shù)據(jù)的讀取,設(shè)為0則沒(méi)有超時(shí)限制。

也就是說(shuō)這個(gè)超時(shí)是一個(gè)請(qǐng)求的總體超時(shí)時(shí)間,而不必再分別去設(shè)置連接超時(shí)、讀取超時(shí)等等。

這對(duì)于使用者來(lái)說(shuō)可能是一個(gè)更好的選擇,大部分場(chǎng)景,使用者不必關(guān)心到底是哪部分導(dǎo)致的超時(shí),而只是想這個(gè) HTTP 請(qǐng)求整體什么時(shí)候能返回。

超時(shí)機(jī)制底層原理

以一個(gè)最簡(jiǎn)單的例子來(lái)闡述超時(shí)機(jī)制的底層原理。

這里我起了一個(gè)本地服務(wù),用 Go HttpClient 去請(qǐng)求,超時(shí)時(shí)間設(shè)置為 10 分鐘,建議使 Debug 時(shí)設(shè)置長(zhǎng)一點(diǎn),否則可能超時(shí)導(dǎo)致無(wú)法走完全流程。

	client := http.Client{
		Timeout: 10 * time.Minute,
	}
	resp, err := client.Get("http://127.0.0.1:81/hello")

1. 根據(jù) timeout 計(jì)算出超時(shí)的時(shí)間點(diǎn)

// 來(lái)自 src/net/http/client.go
deadline = c.deadline()

2. 設(shè)置請(qǐng)求的 cancel

// 來(lái)自 src/net/http/client.go
stopTimer, didTimeout := setRequestCancel(req, rt, deadline)

這里返回的 stopTimer 就是可以手動(dòng) cancel 的方法,didTimeout 是判斷是否超時(shí)的方法。這兩個(gè)可以理解為回調(diào)方法,調(diào)用 stopTimer() 可以手動(dòng) cancel,調(diào)用 didTimeout() 可以返回是否超時(shí)。

設(shè)置的主要代碼其實(shí)就是將請(qǐng)求的 Context 替換為 cancelCtx,后續(xù)所有的操作都將攜帶這個(gè) cancelCtx:

// 來(lái)自 src/net/http/client.go
var cancelCtx func()
if oldCtx := req.Context(); timeBeforeContextDeadline(deadline, oldCtx) {
	req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
}

同時(shí),再起一個(gè)定時(shí)器,當(dāng)超時(shí)時(shí)間到了之后,將 timedOut 設(shè)置為 true,再調(diào)用 doCancel(),doCancel() 是調(diào)用真正 RoundTripper (代表一個(gè) HTTP 請(qǐng)求事務(wù))的 CancelRequest,也就是取消請(qǐng)求,這個(gè)跟實(shí)現(xiàn)有關(guān)。

// 來(lái)自 src/net/http/client.go
timer := time.NewTimer(time.Until(deadline))
var timedOut atomicBool

go func() {
	select {
	case <-initialReqCancel:
		doCancel()
		timer.Stop()
	case <-timer.C:
		timedOut.setTrue()
		doCancel()
	case <-stopTimerCh:
		timer.Stop()
	}
}()

Go 默認(rèn) RoundTripper CancelRequest 實(shí)現(xiàn)是關(guān)閉這個(gè)連接

// 位于 src/net/http/transport.go
// CancelRequest cancels an in-flight request by closing its connection.
// CancelRequest should only be called after RoundTrip has returned.
func (t *Transport) CancelRequest(req *Request) {
	t.cancelRequest(cancelKey{req}, errRequestCanceled)
}

3. 獲取連接

// 位于 src/net/http/transport.go
for {
	select {
	case <-ctx.Done():
		req.closeBody()
		return nil, ctx.Err()
	default:
	}

	// ...
	pconn, err := t.getConn(treq, cm)
	// ...
}

代碼的開(kāi)頭監(jiān)聽(tīng) ctx.Done,如果超時(shí)則直接返回,使用 for 循環(huán)主要是為了請(qǐng)求的重試。

后續(xù)的 getConn 是阻塞的,代碼比較長(zhǎng),挑重點(diǎn)說(shuō),先看看有沒(méi)有空閑連接,如果有則直接返回

// 位于 src/net/http/transport.go
// Queue for idle connection.
if delivered := t.queueForIdleConn(w); delivered {
	// ...
	return pc, nil
}

如果沒(méi)有空閑連接,起個(gè)協(xié)程去異步建立,建立成功再通知主協(xié)程

// 位于 src/net/http/transport.go
// Queue for permission to dial.
t.queueForDial(w)

再接著是一個(gè) select 等待連接建立成功、超時(shí)或者主動(dòng)取消,這就實(shí)現(xiàn)了在連接過(guò)程中的超時(shí)

// 位于 src/net/http/transport.go
// Wait for completion or cancellation.
select {
case <-w.ready:
	// ...
	return w.pc, w.err
case <-req.Cancel:
	return nil, errRequestCanceledConn
case <-req.Context().Done():
	return nil, req.Context().Err()
case err := <-cancelc:
	if err == errRequestCanceled {
		err = errRequestCanceledConn
	}
	return nil, err
}

4. 讀寫(xiě)數(shù)據(jù)

在上一條連接建立的時(shí)候,每個(gè)鏈接還偷偷起了兩個(gè)協(xié)程,一個(gè)負(fù)責(zé)往連接中寫(xiě)入數(shù)據(jù),另一個(gè)負(fù)責(zé)讀數(shù)據(jù),他們都監(jiān)聽(tīng)了相應(yīng)的 channel。

// 位于 src/net/http/transport.go
go pconn.readLoop()
go pconn.writeLoop()

其中 wirteLoop 監(jiān)聽(tīng)來(lái)自主協(xié)程的數(shù)據(jù),并往連接中寫(xiě)入

// 位于 src/net/http/transport.go
func (pc *persistConn) writeLoop() {
	defer close(pc.writeLoopDone)
	for {
		select {
		case wr := <-pc.writech:
			startBytesWritten := pc.nwrite
			err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
			// ... 
			if err != nil {
				pc.close(err)
				return
			}
		case <-pc.closech:
			return
		}
	}
}

同理,readLoop 讀取響應(yīng)數(shù)據(jù),并寫(xiě)回主協(xié)程。讀與寫(xiě)的過(guò)程中如果超時(shí)了,連接將被關(guān)閉,報(bào)錯(cuò)退出。

超時(shí)機(jī)制小結(jié)

Go 的這種請(qǐng)求超時(shí)機(jī)制,可隨時(shí)終止請(qǐng)求,可設(shè)置整個(gè)請(qǐng)求的超時(shí)時(shí)間。其實(shí)現(xiàn)主要依賴協(xié)程、channel、select 機(jī)制的配合??偨Y(jié)出套路是:

  • 主協(xié)程生成 cancelCtx,傳遞給子協(xié)程,主協(xié)程與子協(xié)程之間用 channel 通信
  • 主協(xié)程 select channel 和 cancelCtx.Done,子協(xié)程完成或取消則 return
  • 循環(huán)任務(wù):子協(xié)程起一個(gè)循環(huán)處理,每次循環(huán)開(kāi)始都 select cancelCtx.Done,如果完成或取消則退出
  • 阻塞任務(wù):子協(xié)程 select 阻塞任務(wù)與 cancelCtx.Done,阻塞任務(wù)處理完或取消則退出

以循環(huán)任務(wù)為例

Java 能實(shí)現(xiàn)這種超時(shí)機(jī)制嗎

直接說(shuō)結(jié)論:暫時(shí)不行。

首先 Java 的線程太重,像 Go 這樣一次請(qǐng)求開(kāi)了這么多協(xié)程,換成線程性能會(huì)大打折扣。

其次 Go 的 channel 雖然和 Java 的阻塞隊(duì)列類似,但 Go 的 select 是多路復(fù)用機(jī)制,Java 暫時(shí)無(wú)法實(shí)現(xiàn),即無(wú)法監(jiān)聽(tīng)多個(gè)隊(duì)列是否有數(shù)據(jù)到達(dá)。所以綜合來(lái)看 Java 暫時(shí)無(wú)法實(shí)現(xiàn)類似機(jī)制。

總結(jié)

本文介紹了 Go 另類且有趣的 HTTP 超時(shí)機(jī)制,并且分析了底層實(shí)現(xiàn)原理,歸納出了這種機(jī)制的套路,如果我們寫(xiě) Go 代碼,也可以如此模仿,讓代碼更 Go。

以上就是深入了解Go的HttpClient超時(shí)機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Go HttpClient超時(shí)機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go語(yǔ)言之自定義集合Set

    Go語(yǔ)言之自定義集合Set

    本文主要介紹的是Go語(yǔ)言的自定義集合Set,文中介紹的很詳細(xì),有需要的可以參考學(xué)習(xí)。
    2016-08-08
  • 深入探索Go?1.21中的?maps工具庫(kù)

    深入探索Go?1.21中的?maps工具庫(kù)

    隨著?Go?1.21.0?版本的發(fā)布,新增了兩個(gè)實(shí)用的泛型工具庫(kù):maps?和?slices,下面小編就帶大家一起學(xué)習(xí)一下?maps?工具庫(kù)的相關(guān)知識(shí)吧
    2023-08-08
  • 利用Golang實(shí)現(xiàn)對(duì)配置文件加密

    利用Golang實(shí)現(xiàn)對(duì)配置文件加密

    在實(shí)際的應(yīng)用中,配置文件通常包含了一些敏感的信息,如數(shù)據(jù)庫(kù)密碼、API密鑰等,為了保護(hù)這些敏感信息不被惡意獲取,我們可以對(duì)配置文件進(jìn)行加密,本文將介紹如何使用Go語(yǔ)言實(shí)現(xiàn)對(duì)配置文件的加密,需要的朋友可以參考下
    2023-10-10
  • go語(yǔ)言LeetCode題解1030距離順序排列矩陣單元格

    go語(yǔ)言LeetCode題解1030距離順序排列矩陣單元格

    這篇文章主要為大家介紹了go語(yǔ)言LeetCode題解1030距離順序排列矩陣單元格,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • go中Excelize處理excel表實(shí)現(xiàn)帶數(shù)據(jù)校驗(yàn)的文件導(dǎo)出

    go中Excelize處理excel表實(shí)現(xiàn)帶數(shù)據(jù)校驗(yàn)的文件導(dǎo)出

    本文主要介紹了go中Excelize處理excel表實(shí)現(xiàn)帶數(shù)據(jù)校驗(yàn)的文件導(dǎo)出,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • golang中字符串和數(shù)字轉(zhuǎn)換方法

    golang中字符串和數(shù)字轉(zhuǎn)換方法

    在Golang中,可以使用strconv包中的Itoa()和Atoi()函數(shù)進(jìn)行字符串與數(shù)字之間的轉(zhuǎn)換,Itoa()用于將數(shù)字轉(zhuǎn)換為字符串,Atoi()則用于將字符串轉(zhuǎn)換回?cái)?shù)字,本文介紹golang中字符串和數(shù)字轉(zhuǎn)換方法,感興趣的朋友一起看看吧
    2024-09-09
  • Golang匯編命令解讀及使用

    Golang匯編命令解讀及使用

    這篇文章主要介紹了Golang匯編命令解讀及命令使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-04-04
  • 詳解為什么說(shuō)Golang中的字符串類型不能修改

    詳解為什么說(shuō)Golang中的字符串類型不能修改

    在接觸Go這么語(yǔ)言,可能你經(jīng)常會(huì)聽(tīng)到這樣一句話。對(duì)于字符串不能修改,可能你很納悶,日常開(kāi)發(fā)中我們對(duì)字符串進(jìn)行修改也是很正常的,為什么又說(shuō)Go中的字符串不能進(jìn)行修改呢?本文就來(lái)通過(guò)實(shí)際案例給大家演示一下
    2023-03-03
  • 從淺入深帶你掌握Golang數(shù)據(jù)結(jié)構(gòu)map

    從淺入深帶你掌握Golang數(shù)據(jù)結(jié)構(gòu)map

    在?Go?語(yǔ)言中,map?是一種非常常見(jiàn)的數(shù)據(jù)類型,它可以用于快速地檢索數(shù)據(jù)。本篇文章將介紹?Go?語(yǔ)言中的?map,包括?map?的定義、初始化、操作和優(yōu)化,需要的可以參考一下
    2023-04-04
  • 探索分析Go?HTTP?GET請(qǐng)求發(fā)送body

    探索分析Go?HTTP?GET請(qǐng)求發(fā)送body

    這篇文章主要為大家介紹了探索分析Go?HTTP?GET請(qǐng)求發(fā)送body,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11

最新評(píng)論