位置: IT常识 - 正文

有趣的 Go HttpClient 超时机制

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

推荐整理分享有趣的 Go HttpClient 超时机制,希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:,内容如对您有帮助,希望把文章链接给更多的朋友!

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 代替,也是啥也没做的空 ContextcancelCtx:可以取消的 ContexttimerCtx:主动超时的 Context

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

Context 三个特性例子

这部分的例子来源于 Go 的源码,位于 src/context/example_test.go

携带数据

使用 context.WithValue 来携带,使用 Value 来取值,源码中的例子如下:

// 来自 src/context/example_test.gofunc ExampleWithValue() {type favContextKey stringf := 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.gogen := func(ctx context.Context) <-chan int {dst := make(chan int)n := 1go func() {for {select {case <-ctx.Done():return // returning not to leak the goroutinecase dst <- n:n++}}}()return dst}

然后通过 context.WithCancel 生成一个可取消的 Context,传入 gen 方法,直到 gen 返回 5 时,调用 cancel 取消 gen 方法的执行。

// 来自 src/context/example_test.goctx, cancel := context.WithCancel(context.Background())defer cancel() // cancel when we are finished consuming integersfor n := range gen(ctx) {fmt.Println(n)if n == 5 {break}}// Output:// 1// 2// 3// 4// 5

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

超时

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

设置超时时间有2种方式:context.WithTimeout 与 context.WithDeadline,WithTimeout 是设置一段时间后,WithDeadline 是设置一个截止时间点,WithTimeout 最终也会转换为 WithDeadline。

// 来自 src/context/example_test.gofunc 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 超时机制

看一下 Go 的 HttpClient 超时配置说明:

client := http.Client{Timeout: 10 * time.Second,}// 来自 src/net/http/client.gotype 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.godeadline = c.deadline()2. 设置请求的 cancel// 来自 src/net/http/client.gostopTimer, didTimeout := setRequestCancel(req, rt, deadline)

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

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

// 来自 src/net/http/client.govar 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.gotimer := time.NewTimer(time.Until(deadline))var timedOut atomicBoolgo 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.gofor {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.errcase <-req.Cancel:return nil, errRequestCanceledConncase <-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.gogo pconn.readLoop()go pconn.writeLoop()

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

// 位于 src/net/http/transport.gofunc (pc *persistConn) writeLoop() {defer close(pc.writeLoopDone)for {select {case wr := <-pc.writech:startBytesWritten := pc.nwriteerr := 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 底层原理第一期,求个 赞、在看、分享,我们下期再见。

搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。
本文链接地址:https://www.jiuchutong.com/zhishi/311827.html 转载请保留说明!

上一篇:Java实现邮件发送(java 邮件)

下一篇:PHPCMS上传错误413?(http上传错误)

  • 微信过期红包金额怎么查看(微信过期红包金额)

    微信过期红包金额怎么查看(微信过期红包金额)

  • 手机nfc功能如何打开(手机NFC功能如何读取)

    手机nfc功能如何打开(手机NFC功能如何读取)

  • 红米note键盘出不来了(红米note打字键盘震动怎么关)

    红米note键盘出不来了(红米note打字键盘震动怎么关)

  • 该数字是文本类型可能导致计算结果出错(该数字是文本类型怎么转换为数字)

    该数字是文本类型可能导致计算结果出错(该数字是文本类型怎么转换为数字)

  • b站草稿保存哪里去了(b站存草稿的视频怎么将它放进相册)

    b站草稿保存哪里去了(b站存草稿的视频怎么将它放进相册)

  • 手机换了电池还是耗电快怎么办(手机换了电池还是耗电快)

    手机换了电池还是耗电快怎么办(手机换了电池还是耗电快)

  • pciex1接口能接固态吗(pcie接口能接什么固态硬盘)

    pciex1接口能接固态吗(pcie接口能接什么固态硬盘)

  • 发起群聊不说话删除退出别人知道吗(发起群聊不说话然后移除群成员)

    发起群聊不说话删除退出别人知道吗(发起群聊不说话然后移除群成员)

  • 被邀请共享相册自己的照片能被看到吗(被邀请共享相册拒绝了还会共享吗)

    被邀请共享相册自己的照片能被看到吗(被邀请共享相册拒绝了还会共享吗)

  • 拼多多没开通拼小圈好友能看见吗(拼多多没开通拼小圈)

    拼多多没开通拼小圈好友能看见吗(拼多多没开通拼小圈)

  • word下方字数统计没了(word下面字数统计那一栏怎么看)

    word下方字数统计没了(word下面字数统计那一栏怎么看)

  • 苹果手机是什么电池(苹果手机是什么软件版本)

    苹果手机是什么电池(苹果手机是什么软件版本)

  • 华为nova4语音助手在哪(华为nova4语音助手叫什么名字)

    华为nova4语音助手在哪(华为nova4语音助手叫什么名字)

  • ios怎么把两张照片合成一张(ios怎么把两张照片p成一张)

    ios怎么把两张照片合成一张(ios怎么把两张照片p成一张)

  • 单反相机如何开机(单反相机如何开始拍照)

    单反相机如何开机(单反相机如何开始拍照)

  • 大麦买票需要填2个人吗(大麦买票填错电话号码怎么办)

    大麦买票需要填2个人吗(大麦买票填错电话号码怎么办)

  • 华为怎么打开尺子(华为自带尺)

    华为怎么打开尺子(华为自带尺)

  • 魅蓝note5换电池教程(魅蓝note5换电池后密码不对了)

    魅蓝note5换电池教程(魅蓝note5换电池后密码不对了)

  • 麒麟980比970强很多么(麒麟980与麒麟970差别有多大)

    麒麟980比970强很多么(麒麟980与麒麟970差别有多大)

  • 小q画笔为什么黑屏(小q画笔有软件代替吗)

    小q画笔为什么黑屏(小q画笔有软件代替吗)

  • PS怎么处理全景照片的尺寸(ps怎么处理全景图)

    PS怎么处理全景照片的尺寸(ps怎么处理全景图)

  • miui10如何看使用记录(miui查看使用记录)

    miui10如何看使用记录(miui查看使用记录)

  • vivox27有面部识别怎么设置(vivox27有面部解锁功能吗)

    vivox27有面部识别怎么设置(vivox27有面部解锁功能吗)

  • 华为p30如何设置铃声(华为p30如何设置24小时制)

    华为p30如何设置铃声(华为p30如何设置24小时制)

  • 手机不支持红外遥控功能怎么办(手机不支持红外设备怎么开空调)

    手机不支持红外遥控功能怎么办(手机不支持红外设备怎么开空调)

  • 抖音自动定位城市有误(抖音的自动定位准不准?)

    抖音自动定位城市有误(抖音的自动定位准不准?)

  • Win11如何将开始菜单从中间移到左边(win11如何将开始菜单里的软件移到桌面)

    Win11如何将开始菜单从中间移到左边(win11如何将开始菜单里的软件移到桌面)

  • 如何在你的电脑上完成whisper的简单部署(如何在你的电脑上截图)

    如何在你的电脑上完成whisper的简单部署(如何在你的电脑上截图)

  • IDEA从零到精通(24)之lombok插件的安装与使用(idea实用技巧)

    IDEA从零到精通(24)之lombok插件的安装与使用(idea实用技巧)

  • 免税肉类主要品种目录
  • 所得税季度预缴纳税申报表
  • 还有什么技术
  • 投资收益可以计入营业收入吗
  • 坏账已核销
  • 费用退回现金如何入账
  • 施工企业确认收入的方法有哪两种
  • 小规模纳税人进材料怎么做账
  • 投资款多投了怎么办
  • 筹备费用怎么扣税的
  • 个人投资理财需要注意什么
  • 利润表中企业所得税
  • 增值税税控系统技术维护费怎么抵扣
  • 什么叫项目所在地
  • 个税什么情况可以不用汇算
  • 销售额是含税价还是不含税
  • 为什么红字信息查不到
  • 小区保安有收物业费的权利和义务吗?
  • 个人从价计征房产税
  • 固定资产清理借贷方向增减变动
  • 免征的增值税怎么做账
  • 企业所得税汇算清缴操作流程
  • win11如何设置开机自启动软件
  • php的数组函数
  • 小规模纳税人销售使用过的固定资产
  • 房地产开发企业涉及哪些税
  • vue的安装命令
  • 税控系统维护费抵扣申报表怎么填
  • 企业出租房屋怎么做账
  • ms 高级应用
  • 代码简单图案
  • 微信小程序基于什么技术
  • win11更新22468
  • 应付账款转入营业外收入的证明
  • qt 5.15 编译
  • 织梦的css样式在哪
  • 法人如何提取公积金
  • 小企业会计准则和企业会计准则的区别
  • 所得税申报表的营业成本包括哪些
  • 水利建设专项基金土增税清算能扣除吗
  • 追索权定义
  • 原材料用于在建工程要进项转出吗
  • 应付账款挂账
  • 将现金存入银行编制什么凭证
  • 如何冲其他应付款发票
  • 车贷管理费合理吗
  • 工程款项目的发包人是谁
  • 哪些商业保险可以抵扣个税
  • 车船税开在备注栏相关文件
  • 待转销项税额对应科目
  • 投标保证金的计算
  • 会计准则应收账款计提坏账
  • 私营企业主的现金流特征
  • 深入理解中国式现代化论文
  • 安装mysql提示one or more
  • 如何设置linux
  • centos6.9开启ssh服务
  • vmwarevmx进程是干嘛的
  • onekey driver version是什么软件
  • 打开linux系统
  • explorer.exe是什么指令
  • WIN10系统中WPS字体颜色浅
  • win7任务栏透明调不了
  • windows10的操作中心
  • linux的安装教程
  • cocos2dx4.0教程
  • 怎么用javascript
  • dos命令批处理
  • linux中makefile怎么写
  • 仿京东首页html5
  • inputchange
  • android开发框架
  • python的web框架
  • 吉林市无犯罪记录证明网上申请流程
  • 广东增值税电子专用发票
  • 水电费的增值税专用税可抵扣进项
  • 打印发票需要什么信息
  • 税务系统个人业务自传
  • 什么是深入调研工作
  • ssr服务器地址端口密码
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设