Go を書く過程で、この 2 つの言語の特徴を比較することがよくありますが、何度も落とし穴を踏んだり、興味深い箇所を見つけたりしましたが、今回は Go に付属している HttpClient のタイムアウト機構についてお話します。皆様のお役に立てれば幸いです。
Go の HttpClient タイムアウト メカニズムを紹介する前に、まず見てみましょう。 Java のタイムアウトを実装する方法。 [関連する推奨事項: Go ビデオ チュートリアル ]
Java ネイティブ HttpClient を作成し、基礎となるメソッドに対応する接続タイムアウトと読み取りタイムアウトを設定します:
JVM のソース コードに戻ると、これはシステム コールのカプセル化であることがわかりましたが、実際には Java に限らず、ほとんどのプログラミング言語はオペレーティング システムが提供するタイムアウト機能を利用しています。
ただし、Go の HttpClient は別のタイムアウト メカニズムを提供しており、これは非常に興味深いものです。しかし、始める前に、まず Go のコンテキストを理解しましょう。
Context とは何ですか?
Go ソース コードのコメントによると:
// コンテキストは期限、キャンセル信号、その他の値を伝達します。 // API の境界。 // Context のメソッドは複数の goroutines によって同時に呼び出される場合があります。
Context は、タイムアウト、キャンセル信号、その他のデータを送信できるインターフェイスです。Context のメソッドは複数の goroutines によって同時に呼び出されます。
Context は Java の ThreadLocal に似ています。スレッド内でデータを転送できますが、まったく同じではありません。これは明示的な転送ですが、ThreadLocal は暗黙的な転送です。Context はデータを渡すだけでなく、次のこともできます。タイムアウト、キャンセル信号もキャリーします。
コンテキストはインターフェイスを定義するだけで、Go ではいくつかの特定の実装が提供されます。
Context の 3 つの特徴については、Go が提供する Context 実装とソース コードの例を参照してください。
コンテキスト 3 つの機能の例
このパートの例は、src/context/ にある Go のソース コードから取得しています。 example_test.go
それを運ぶには context.WithValue
を使用し、取得するには Value
を使用しますソース コード内の値。 例は次のとおりです。
// 来自 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 }
まず、コルーチンを開始して無限ループを実行し、チャネルにデータを継続的に書き込みます。 Done() のイベント
// 来自 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 }
context.WithCancel を通じてキャンセル可能なコンテキストを生成し、## を渡します。
gen# まで #gen メソッド ## 5 が返された場合は、
cancel を呼び出して
gen メソッドの実行をキャンセルします。
// 来自 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
単純に、あるコルーチンのループ内に終了フラグを埋め込み、別のコルーチンが終了フラグを立てると理解できそうです。
タイムアウト
context.WithTimeout
とcontext.WithDeadline の 2 つの方法があります。WithTimeout は期間を設定し、WithDeadline は期限を設定します。 、および WithTimeout これも最終的には WithDeadline に変換されます。
// 来自 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 の別のタイムアウト メカニズムGo の HttpClient タイムアウト設定手順を参照してください:
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'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 }
Timeout
には、接続、リダイレクト、およびデータの読み取りにかかる時間が含まれます。タイマーは、タイムアウト時間が経過するとデータの読み取りを中断します。0 に設定すると、タイムアウト制限はありません。つまり、このタイムアウトは、接続タイムアウトや読み取りタイムアウトなどを個別に設定する必要がなく、リクエストの
全体のタイムアウト期間
これはユーザーにとってより良い選択かもしれません。ほとんどのシナリオでは、ユーザーはタイムアウトの原因となっている部分を気にする必要はありませんが、HTTP リクエスト全体がいつ返されるかを知りたいだけです。
タイムアウト メカニズムの基礎となる原理最も単純な例を使用して、タイムアウト メカニズムの基礎となる原理を説明します。
这里我起了一个本地服务,用 Go HttpClient 去请求,超时时间设置为 10 分钟,建议使 Debug 时设置长一点,否则可能超时导致无法走完全流程。
client := http.Client{ Timeout: 10 * time.Minute, } resp, err := client.Get("http://127.0.0.1:81/hello")
// 来自 src/net/http/client.go deadline = c.deadline()
// 来自 src/net/http/client.go stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
这里返回的 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) }
同时,再起一个定时器,当超时时间到了之后,将 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() } }()
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) }
// 位于 src/net/http/transport.go for { select { case <-ctx.Done(): req.closeBody() return nil, ctx.Err() default: } // ... pconn, err := t.getConn(treq, cm) // ... }
代码的开头监听 ctx.Done,如果超时则直接返回,使用 for 循环主要是为了请求的重试。
后续的 getConn 是阻塞的,代码比较长,挑重点说,先看看有没有空闲连接,如果有则直接返回
// 位于 src/net/http/transport.go // Queue for idle connection. if delivered := t.queueForIdleConn(w); delivered { // ... return pc, nil }
如果没有空闲连接,起个协程去异步建立,建立成功再通知主协程
// 位于 src/net/http/transport.go // Queue for permission to dial. t.queueForDial(w)
再接着是一个 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 }
在上一条连接建立的时候,每个链接还偷偷起了两个协程,一个负责往连接中写入数据,另一个负责读数据,他们都监听了相应的 channel。
// 位于 src/net/http/transport.go go pconn.readLoop() go pconn.writeLoop()
其中 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 } } }
同理,readLoop 读取响应数据,并写回主协程。读与写的过程中如果超时了,连接将被关闭,报错退出。
超时机制小结
Go 的这种请求超时机制,可随时终止请求,可设置整个请求的超时时间。其实现主要依赖协程、channel、select 机制的配合。总结出套路是:
以循环任务为例
Java 能实现这种超时机制吗
直接说结论:暂时不行。
首先 Java 的线程太重,像 Go 这样一次请求开了这么多协程,换成线程性能会大打折扣。
其次 Go 的 channel 虽然和 Java 的阻塞队列类似,但 Go 的 select 是多路复用机制,Java 暂时无法实现,即无法监听多个队列是否有数据到达。所以综合来看 Java 暂时无法实现类似机制。
本文介绍了 Go 另类且有趣的 HTTP 超时机制,并且分析了底层实现原理,归纳出了这种机制的套路,如果我们写 Go 代码,也可以如此模仿,让代码更 Go。
原文地址:https://juejin.cn/post/7166201276198289445
更多编程相关知识,请访问:编程视频!!
以上がGolang 独自の HttpClient タイムアウト メカニズムについて話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。