有趣的 Go HttpClient 超时机制

news2024/10/2 20:35:16

hello,大家好呀,我是既写 Java 又写 Go 的小楼,在写 Go 的过程中经常对比这两种语言的特性,踩了不少坑,也发现了不少有意思的地方,今天就来聊聊 Go 自带的 HttpClient 的超时机制。

Java HttpClient 超时底层原理

在介绍 Go 的 HttpClient 超时机制之前,我们先看看 Java 是如何实现超时的。

写一个 Java 原生的 HttpClient,设置连接超时、读取超时时间分别对应到底层的方法分别是:

再追溯到 JVM 源码,发现是对系统调用的封装,其实不光是 Java,大部分的编程语言都借助了操作系统提供的超时能力。

然而 Go 的 HttpClient 却提供了另一种超时机制,挺有意思,我们来盘一盘。但在开始之前,我们先了解一下 Go 的 Context。

Go Context 简介

Context 是什么?

根据 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 简单来说是一个可以携带超时时间、取消信号和其他数据的接口,Context 的方法会被多个协程同时调用。

Context 有点类似 Java 的ThreadLocal,可以在线程中传递数据,但又不完全相同,它是显示传递,ThreadLocal 是隐式传递,除了传递数据之外,Context 还能携带超时时间、取消信号。

Context 只是定义了接口,具体的实现在 Go 中提供了几个:

  • Background :空的实现,啥也没做
  • TODO:还不知道用什么 Context,先用 TODO 代替,也是啥也没做的空 Context
  • cancelCtx:可以取消的 Context
  • timerCtx:主动超时的 Context

针对 Context 的三个特性,可以通过 Go 提供的 Context 实现以及源码中的例子来进一步了解下。

Context 三个特性例子

这部分的例子来源于 Go 的源码,位于 src/context/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
}

取消

先起一个协程执行一个死循环,不停地往 channel 中写数据,同时监听 ctx.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 生成一个可取消的 Context,传入 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

这么看起来,可以简单理解为在一个协程的循环中埋入结束标志,另一个协程去设置这个结束标志。

超时

有了 cancel 的铺垫,超时就好理解了,cancel 是手动取消,超时是自动取消,只要起一个定时的协程,到时间后执行 cancel 即可。

设置超时时间有2种方式:context.WithTimeoutcontext.WithDeadline,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 的另一种超时机制

基于 Context 可以设置任意代码段执行的超时机制,就可以设计一种脱离操作系统能力的请求超时能力。

超时机制简介

看一下 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 包括了连接、redirect、读取数据的时间,定时器会在 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")

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

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

2. 设置请求的 cancel

// 来自 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)
}

3. 获取连接

// 位于 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
}

4. 读写数据

在上一条连接建立的时候,每个链接还偷偷起了两个协程,一个负责往连接中写入数据,另一个负责读数据,他们都监听了相应的 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 机制的配合。总结出套路是:

  • 主协程生成 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。这期是我写的 Go 底层原理第一期,求个 在看分享,我们下期再见。


  • 搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7387.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu22.04中root用户下依然权限不够,执行不了可执行文件

文章目录先看现象解决方法什么情况下会遇到这样的错误先看现象 provider是一个C语言编译得到的可执行文件。 开始&#xff0c;我直接运行它&#xff0c;告诉我权限不够然后我加上sudo运行&#xff0c;告诉我找不到命令最后我进入root用户运行&#xff0c;竟然还告诉我权限不够…

PraNet: Parallel Reverse Attention Networkfor Polyp Segmentation

Tittle:用于息肉分割的并行反向注意力网络 摘要 准确的息肉分割主要面临着两个难点&#xff1a;1&#xff09;相同类型的息肉有不同的大小&#xff0c;颜色和纹理。2&#xff09;息肉与周围粘膜的边界模糊不清晰。 为了解决这些挑战本文提出了一种并行反向注意网络ParNet。具…

10、MySQL——子查询

目录 一、子查询 1、子查询出现的位置 2、子查询结果集的形式 二、实例演示 1、查询工资高于JONES的员工 1.1 分析 1.2 代码 2、查询与SCOTT同一部门的员工 2.1 分析 2.2 代码 3、工资高于30号部门所有人的员工信息 3.1 分析 3.2 代码 4、查询工作和工资…

mannose-OH|甘露糖-羟基|mannose-PEG-OH|甘露糖-聚乙二醇-羟基

mannose-OH|甘露糖-羟基|mannose-PEG-OH|甘露糖-聚乙二醇-羟基 羟基&#xff08;oxhydryl&#xff09;是一种常见的极性基团&#xff0c;化学式为-OH。羟基与水有某些相似的性质&#xff0c;羟基是典型的极性基团&#xff0c;与水可形成氢键&#xff0c;在无机化合物水溶液中以…

钙尔奇30周年以行动,力挺中国骨骼健康发展

启动行动力赋能新旅程 近日&#xff0c;2022年度西普会于中国海南博鳌盛大举行。以“构筑患者价值同心圆——二元发展驱动健康产业新增长”为主题&#xff0c;本届西普会的会议内容和参会主体全面升维&#xff0c;从全球视野到中国特色聚合优质资源、拓宽产业边界&#xff0c;…

vue项目中实际构建echarts拓扑关系图业务

vue项目中实际构建echarts拓扑关系图业务前言一、关系拓扑是什么&#xff1f;二、需求梳理三、封装关系图组件1.父组件引用2.测试数据引入3.封装关系子组件4.关系组件完整代码总结前言 由于现在echarts的利用率增强&#xff0c;需要用到拓扑图的设计&#xff0c;如果单纯针对e…

Ubuntu虚拟机安装

文章目录VMware添加虚拟机等待开机&#xff08;需要一些时间安装系统&#xff09;检查网络环境设置 Ubuntu 中文支持一些基础设置VMware添加虚拟机 文件——>新建虚拟机 下一步&#xff1a;安装程序光盘映像文件&#xff08;iso&#xff09; 设置主机名&#xff0c;用户名及…

reportportal 集成 robotframework 自动化执行及结果可视化

最近领导想了个需求&#xff0c;想把目前组内在linux平台上执行的自动化脚本搞成可视化&#xff0c;如果是web站点相关日志可视化倒是简单了&#xff0c;ELK就是不错的选择&#xff0c;大部分可视化项目这种的&#xff0c;可以做的开起来很炫。 我们这边是自己写的脚本&#x…

机器学习西瓜书-1-2章

学习目标&#xff1a; 概览机器学习西瓜书 1、2章 学习内容&#xff1a; 第一章 绪论 1.1 基本术语 1.2 假设空间 1.3 归纳偏好 1.4 发展历程 第二章 模型评估与选择 2.1 经验误差与过拟合 2.2 评估方法 2.3 性能度量 学习时间&#xff1a; 两天 学习产出&#xff1a; 第…

爱了爱了,20个好用到爆的 Python 函数

大家好&#xff0c;今天分享20个日常工作中必不可少的Python函数&#xff0c;这些函数平时看到的不多&#xff0c;但是它们使用起来倒是非常的方便&#xff0c;它们可以大幅度地提高工作效率。内容较长&#xff0c;欢迎收藏学习&#xff0c;喜欢点赞支持。 文章目录技术提升isi…

你以为的Java面试只是背答案?跳槽涨薪不还是得靠自己的技术

前言 Java面试当然不能只靠背答案&#xff0c;为了应付面试背完答案拿到offer只是进入了这个行业&#xff0c;后面的实操还是得看自己的技术&#xff01;技术的挂钩当然和技术底层也是挂钩的。 这是我在工作、面试中学习并总结到的一些知识点&#xff0c;都是一些比较典型的、…

Kafka 消息过期策略(时间相关参数)

Kafka 消息过期策略&#xff08;时间相关参数&#xff09; 标记delete时效 (CDH配置项)log.retention.ms &#xff08;Kafka offset配置&#xff09;retention.ms 标记delete的真删底层文件 delete.delay.ms log.segmetn.delete 背景&#xff1a;在不需要重启kafka的情况下&a…

QA特辑|剪得断,理不乱,一场直播解开关联网络与反团伙欺诈谜团

11月 10 日下午15:00 第九期《关联网络技术在业务安全中的应用》正式开讲。顶象数据科学家翼龙详尽的介绍了关联网络在反团伙欺诈中的作用&#xff0c;深度剖析了关联网络的技术框架、关联网络的图谱构建以及关联网络的复杂算法&#xff0c;为反团伙欺诈提供了重要的参考。 直播…

6.jeecg的pom结构

1.父pom parent为springboot 1.子模块部分 包括base-core、demo、system三个 air为自己新建 2.maven仓库部分 3.dependencies 这个是主动引入的依赖&#xff0c;子pom会继承 4.dependencyManagement 这个有点多&#xff0c;主要作用是先把依赖添加进来&#xff0c;但是…

python安装 learn2learn库 || 在线安装方式或者本地安装

文章目录1. 去github下载完整安装包&#xff08;或本文的百度网盘&#xff09;2. 安装C依赖库3 本地安装4 在线安装&#xff08;不想本地安装 看这儿&#xff01;&#xff09;1. 去github下载完整安装包&#xff08;或本文的百度网盘&#xff09; github连接 https://github.c…

死磕宠物食品安全,京东你凭什么?

出品| 大力财经 文 | 魏力 京东第一个站出来&#xff0c;破局宠物食品行业乱象。 济南女孩刘小姐很在乎宠物的饮食安全&#xff0c;每次换的新狗粮或者买的新零食&#xff0c;她都要先尝一尝&#xff0c;鉴定一下是否符合自家狗狗食用。随着家人式养宠的到来&#xff0c;宠物…

deb包格式实例详解

本文简介及包格式部分内容节选自&#xff1a; deb_百度百科 一、简介 DEB是Debian软件包格式的文件扩展名&#xff0c;跟Debian的命名一样&#xff0c;DEB也是因Debra Murdock而得名&#xff0c;她是Debian创始人Ian Murdock的太太。 Debian包是Unixar的标准归档&#xff0…

LeetCode第 91 场双周赛题解

目录2465. 不同的平均值数目2466. 统计构造好字符串的方案数2467. 树上最大得分和路径2468. 根据限制分割消息2465. 不同的平均值数目 模拟一下即可 class Solution { public:int distinctAverages(vector<int>& nums) {set<double>st;sort(nums.begin(),nums…

Intellij各个功能小件的样子

文章目录资料ControlsButtonBuilt-in buttonSplit buttonCheckboxCombo boxDescription textDrop-down listGot It tooltipGroup headerInput fieldLinkNotificationsBallonBannerProgress indicatorsLoaderProgress barProgress textRadio buttonScrollbarSearch fieldTableTa…

CSAPP实验记录(2)--------- Bomb

实验简介 本实验需要拆除一个“二进制炸弹”&#xff0c;“二进制炸弹”是一个可执行目标程序。运行时&#xff0c;它会提示用户键入6个不同的字符串。如果其中任何一个错误&#xff0c;炸弹就会“爆炸”。必须通过逆向工程和汇编语言知识&#xff0c;推导出六个字符串分别是什…