Let's talk about Golang's own HttpClient timeout mechanism

青灯夜游
Release: 2022-11-18 20:25:45
forward
2098 people have browsed it

In the process of writing Go, I often compare the characteristics of these two languages. I have stepped on many pitfalls and found many interesting places. In this article, I will talk about the timeout mechanism of HttpClient that comes with Go. I hope Helpful to everyone.

Let's talk about Golang's own HttpClient timeout mechanism

The underlying principle of Java HttpClient timeout

Before introducing Go’s HttpClient timeout mechanism, let’s first take a look at Java’s How to implement timeout. [Related recommendations: Go Video Tutorial]

Write a Java native HttpClient, and set the connection timeout and read timeout corresponding to the underlying methods:

Going back to the JVM source code, I found that it is an encapsulation of system calls. In fact, not only Java, most programming languages ​​​​make use of the timeout capability provided by the operating system.

However, Go's HttpClient provides another timeout mechanism, which is quite interesting. Let's take a look. But before we get started, let’s first understand Go’s Context.

Go Context Introduction

What is Context?

According to the comments in the Go source code:

// 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 is simply an interface that can carry timeouts, cancellation signals and other data. Context's methods will be called by multiple goroutines simultaneously. transfer.

Context is somewhat similar to Java's ThreadLocal. It can transfer data in threads, but it is not exactly the same. It is an explicit transfer, while ThreadLocal is an implicit transfer. In addition to passing data, Context can also carry timeout, Cancel signal.

Context only defines the interface, and several specific implementations are provided in Go:

  • Background: empty implementation, does nothing
  • TODO: I don’t know what Context to use yet, so I’ll use TODO instead, which is also an empty Context that does nothing.
  • cancelCtx: Cancelable Context
  • timerCtx: Active timeout Context

For the three characteristics of Context, you can learn more about the Context implementation provided by Go and the examples in the source code.

Context Three feature examples

The examples in this part come from the source code of Go, located at src/context/example_test.go

Carry data

Use context.WithValue to carry it, use Value to get the value, in the source code The example is as follows:

// 来自 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
}
Copy after login

Cancel

First, start a coroutine to execute an infinite loop, continuously write data to the channel, and listen to ctx at the same time. The event

// 来自 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
	}
Copy after login

of Done()

then generates a cancelable Context through context.WithCancel and passes in the gen method until gen When 5 is returned, call cancel to cancel the execution of the gen method.

// 来自 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
Copy after login

It seems that it can be simply understood as burying the end flag in the loop of one coroutine, and another coroutine sets the end flag.

Timeout

With the foreshadowing of cancel, timeout is easy to understand. Cancel is manual cancellation, and timeout is automatic cancellation. As long as a scheduled coroutine is started, Just execute cancel after the time.

There are two ways to set the timeout: context.WithTimeout and context.WithDeadline. WithTimeout is to set a period of time, WithDeadline is to set a deadline, and WithTimeout It will also eventually be converted to 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
}
Copy after login

Another timeout mechanism of Go HttpClient

Based on Context, you can set the timeout mechanism for the execution of any code segment, and you can design a request that is independent of the operating system's capabilities. Timeout capability.

Introduction to the timeout mechanism

Look at Go’s HttpClient timeout configuration instructions:

	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
}
Copy after login

Translate the comments: Timeout includes the time for connecting, redirecting, and reading data. The timer will interrupt the reading of data after the Timeout time. If it is set to 0, there will be no timeout limit.

That is to say, this timeout is the overall timeout period of a request, without having to set the connection timeout, read timeout, etc. separately.

This may be a better choice for users. In most scenarios, users do not need to care about which part causes the timeout, but just want to know when the HTTP request as a whole can be returned.

The underlying principle of the timeout mechanism

The simplest example is used to illustrate the underlying principle of the timeout mechanism.

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

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

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

// 来自 src/net/http/client.go
deadline = c.deadline()
Copy after login

2. 设置请求的 cancel

// 来自 src/net/http/client.go
stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
Copy after login

这里返回的 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)
}
Copy after login

同时,再起一个定时器,当超时时间到了之后,将 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()
	}
}()
Copy after login

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)
}
Copy after login

3. 获取连接

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

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

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

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

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

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

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

再接着是一个 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
}
Copy after login

4. 读写数据

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

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

其中 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
		}
	}
}
Copy after login

同理,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

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

The above is the detailed content of Let's talk about Golang's own HttpClient timeout mechanism. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:juejin.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template