Mari kita bincangkan tentang mekanisme tamat masa HttpClient Golang sendiri

青灯夜游
Lepaskan: 2022-11-18 20:25:45
ke hadapan
2097 orang telah melayarinya

Dalam proses menulis Go, saya sering membandingkan ciri-ciri dua bahasa yang telah saya pijak dan menemui banyak tempat menarik Dalam artikel ini, saya akan bercakap tentang mekanisme tamat masa HttpClient yang disertakan dengan Go. Saya harap Bermanfaat kepada semua orang.

Mari kita bincangkan tentang mekanisme tamat masa HttpClient Golang sendiri

Prinsip asas tamat masa Java HttpClient

Sebelum memperkenalkan mekanisme tamat masa HttpClient Go, mari kita lihat dahulu Java Bagaimana untuk melaksanakan tamat masa. [Cadangan berkaitan: Pergi tutorial video]

Tulis HttpClient asli Java, dan tetapkan tamat masa sambungan dan baca tamat masa yang sepadan dengan kaedah asas:

Berbalik kepada kod sumber JVM, saya mendapati bahawa ia adalah enkapsulasi panggilan sistem Malah, bukan sahaja Java, kebanyakan bahasa pengaturcaraan​​menggunakan keupayaan tamat masa yang disediakan oleh sistem pengendalian.

Namun, Go's HttpClient menyediakan satu lagi mekanisme tamat masa, yang agak menarik Mari kita lihat. Tetapi sebelum kita bermula, mari kita fahami Konteks Go terlebih dahulu.

Pengenalan kepada Konteks Go

Apakah Konteks?

Menurut ulasan dalam kod sumber Go:

// Konteks membawa tarikh akhir, isyarat pembatalan dan nilai lain di seluruh // Sempadan API. // Kaedah konteks mungkin dipanggil oleh berbilang goroutin secara serentak.

Konteks hanyalah antara muka yang boleh membawa tamat masa, isyarat pembatalan dan data lain akan dipanggil oleh berbilang goroutin secara serentak.

Konteks agak serupa dengan ThreadLocal Java Ia boleh menghantar data dalam urutan, tetapi ia tidak sama persis, manakala ThreadLocal ialah pemindahan tersirat juga boleh menjalankan tamat masa, isyarat Batal.

Konteks hanya mentakrifkan antara muka dan beberapa pelaksanaan khusus disediakan dalam Go:

  • Latar belakang: pelaksanaan kosong, tidak melakukan apa-apa
  • TODO: Saya tidak tahu apa Konteks untuk digunakan lagi, jadi saya akan menggunakan TODO sebaliknya, yang merupakan Konteks kosong yang tidak melakukan apa-apa
  • Untuk tiga ciri Konteks, anda boleh mengetahui lebih lanjut tentang pelaksanaan Konteks yang disediakan oleh Go dan contoh dalam sumber. kod.
  • Konteks Tiga contoh ciri

Contoh dalam bahagian ini datang daripada kod sumber Go, terletak di Untuk membawa data

src/context/example_test.go gunakan

untuk membawanya dan gunakan

untuk mendapatkan nilai Contoh dalam kod sumber adalah seperti berikut:

Batalcontext.WithValue Value

Mula-mula, mulakan coroutine untuk melaksanakan gelung tak terhingga, tulis data secara berterusan ke saluran dan pada masa yang sama dengar
// 来自 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
}
Salin selepas log masuk
acara

dan kemudian jana acara boleh dibatalkan melalui

Konteks, masukkan kaedah

, sehingga ctx.Done() mengembalikan 5, panggil

untuk membatalkan pelaksanaan kaedah
// 来自 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
	}
Salin selepas log masuk
.

context.WithCancelgenNampaknya ia boleh difahami secara ringkas sebagai membenamkan bendera akhir dalam gelung satu coroutine, dan coroutine lain menetapkan bendera akhir. gencancelgenTamat masa

// 来自 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
Salin selepas log masuk

Dengan bayangan awal pembatalan, tamat masa mudah difahami Pembatalan adalah pembatalan manual dan tamat masa adalah pembatalan automatik selagi coroutine yang dijadualkan dimulakan , Hanya laksanakan batal selepas masa.

Terdapat dua cara untuk menetapkan tamat masa: dan , WithTimeout ditetapkan selepas tempoh masa, WithDeadline ditetapkan kepada tarikh akhir dan WithTimeout akhirnya akan ditukar kepada WithDeadline.

Satu lagi mekanisme tamat masa Go HttpClient context.WithTimeoutcontext.WithDeadline

Berdasarkan Konteks, anda boleh menetapkan mekanisme tamat masa untuk pelaksanaan mana-mana segmen kod, dan anda boleh mereka bentuk keupayaan yang tidak bergantung pada sistem operasi Minta keupayaan tamat masa.
// 来自 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
}
Salin selepas log masuk

Pengenalan kepada mekanisme tamat masa

Lihat arahan konfigurasi tamat masa HttpClient Go: Terjemah ulasan: Termasuk masa untuk menyambung, mengubah hala dan membaca data Pemasa akan mengganggu bacaan data selepas masa tamat masa Jika ditetapkan kepada 0, tiada had tamat masa.

Maksudnya, tamat masa ini ialah

tamat masa keseluruhan permintaan
	client := http.Client{
		Timeout: 10 * time.Second,
	}
	
	// 来自 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&#39;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&#39;s Context
	// for cancellation instead of implementing CancelRequest.
	Timeout time.Duration
}
Salin selepas log masuk
, tanpa perlu menetapkan tamat masa sambungan, tamat masa baca, dsb. secara berasingan.

TimeoutIni mungkin pilihan yang lebih baik untuk pengguna Dalam kebanyakan senario, pengguna tidak perlu mengambil berat tentang bahagian mana yang menyebabkan tamat masa, tetapi hanya ingin tahu bila permintaan HTTP secara keseluruhan boleh dikembalikan.

Prinsip asas mekanisme tamat masa

Menggunakan contoh paling mudah untuk menggambarkan prinsip asas mekanisme tamat masa.

这里我起了一个本地服务,用 Go HttpClient 去请求,超时时间设置为 10 分钟,建议使 Debug 时设置长一点,否则可能超时导致无法走完全流程。

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

1. 根据 timeout 计算出超时的时间点

// 来自 src/net/http/client.go
deadline = c.deadline()
Salin selepas log masuk

2. 设置请求的 cancel

// 来自 src/net/http/client.go
stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
Salin selepas log masuk

这里返回的 stopTimer 就是可以手动 cancel 的方法,didTimeout 是判断是否超时的方法。这两个可以理解为回调方法,调用 stopTimer() 可以手动 cancel,调用 didTimeout() 可以返回是否超时。

设置的主要代码其实就是将请求的 Context 替换为 cancelCtx,后续所有的操作都将携带这个 cancelCtx:

// 来自 src/net/http/client.go
var cancelCtx func()
if oldCtx := req.Context(); timeBeforeContextDeadline(deadline, oldCtx) {
	req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
}
Salin selepas log masuk

同时,再起一个定时器,当超时时间到了之后,将 timedOut 设置为 true,再调用 doCancel(),doCancel() 是调用真正 RoundTripper (代表一个 HTTP 请求事务)的 CancelRequest,也就是取消请求,这个跟实现有关。

// 来自 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()
	}
}()
Salin selepas log masuk

Go 默认 RoundTripper CancelRequest 实现是关闭这个连接

// 位于 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)
}
Salin selepas log masuk

3. 获取连接

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

	// ...
	pconn, err := t.getConn(treq, cm)
	// ...
}
Salin selepas log masuk

代码的开头监听 ctx.Done,如果超时则直接返回,使用 for 循环主要是为了请求的重试。

后续的 getConn 是阻塞的,代码比较长,挑重点说,先看看有没有空闲连接,如果有则直接返回

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

如果没有空闲连接,起个协程去异步建立,建立成功再通知主协程

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

再接着是一个 select 等待连接建立成功、超时或者主动取消,这就实现了在连接过程中的超时

// 位于 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
}
Salin selepas log masuk

4. 读写数据

在上一条连接建立的时候,每个链接还偷偷起了两个协程,一个负责往连接中写入数据,另一个负责读数据,他们都监听了相应的 channel。

// 位于 src/net/http/transport.go
go pconn.readLoop()
go pconn.writeLoop()
Salin selepas log masuk

其中 wirteLoop 监听来自主协程的数据,并往连接中写入

// 位于 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
		}
	}
}
Salin selepas log masuk

同理,readLoop 读取响应数据,并写回主协程。读与写的过程中如果超时了,连接将被关闭,报错退出。

超时机制小结

Go 的这种请求超时机制,可随时终止请求,可设置整个请求的超时时间。其实现主要依赖协程、channel、select 机制的配合。总结出套路是:

  • 主协程生成 cancelCtx,传递给子协程,主协程与子协程之间用 channel 通信
  • 主协程 select channel 和 cancelCtx.Done,子协程完成或取消则 return
  • 循环任务:子协程起一个循环处理,每次循环开始都 select cancelCtx.Done,如果完成或取消则退出
  • 阻塞任务:子协程 select 阻塞任务与 cancelCtx.Done,阻塞任务处理完或取消则退出

以循环任务为例

Java 能实现这种超时机制吗

直接说结论:暂时不行。

首先 Java 的线程太重,像 Go 这样一次请求开了这么多协程,换成线程性能会大打折扣。

其次 Go 的 channel 虽然和 Java 的阻塞队列类似,但 Go 的 select 是多路复用机制,Java 暂时无法实现,即无法监听多个队列是否有数据到达。所以综合来看 Java 暂时无法实现类似机制。

总结

本文介绍了 Go 另类且有趣的 HTTP 超时机制,并且分析了底层实现原理,归纳出了这种机制的套路,如果我们写 Go 代码,也可以如此模仿,让代码更 Go。

原文地址:https://juejin.cn/post/7166201276198289445

更多编程相关知识,请访问:编程视频!!

Atas ialah kandungan terperinci Mari kita bincangkan tentang mekanisme tamat masa HttpClient Golang sendiri. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:juejin.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan