当前位置: 代码网 > it编程>前端脚本>Golang > golang中http请求的context传递到异步任务的坑及解决

golang中http请求的context传递到异步任务的坑及解决

2024年05月15日 Golang 我要评论
前言在golang中,context.context可以用来用来设置截止日期、同步信号,传递请求相关值的结构体。 与 goroutine 有比较密切的关系。在web程序中,每个request都需要开启

前言

在golang中,context.context可以用来用来设置截止日期、同步信号,传递请求相关值的结构体。 与 goroutine 有比较密切的关系。

在web程序中,每个request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的 goroutine去访问后端资源,比如数据库、rpc服务等,它们需要访问一些共享的资源,比如用户身份信息、认证token、请求截止时间等 这时候可以通过context,来跟踪这些goroutine,并且通过context来控制它们, 这就是go语言为我们提供的context,中文可以理解为“上下文”。

简单看一下context结构

type context interface {
    deadline() (deadline time.time, ok bool)
    done() <-chan struct{}
    err() error
    value(key interface{}) interface{}
}
  • deadline方法是获取设置的截止时间的意思,第一个返回值是截止时间,到了这个时间点,context会自动发起取消请求; 第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数(canclefunc)进行取消。
  • done方法返回一个只读的chan,类型为struct{},在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求, 我们通过done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。之后,err 方法会返回一个错误,告知为什么 context 被取消。
  • err方法返回取消的错误原因,context被取消的原因。
  • value方法获取该context上绑定的值,是一个键值对,通过一个key才可以获取对应的值,这个值一般是线程安全的。

常用的

// 传递一个父context作为参数,返回子context,以及一个取消函数用来取消context。
func withcancel(parent context) (ctx context, cancel cancelfunc)
// 和withcancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消context,
// 当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
func withdeadline(parent context, deadline time.time) (context, cancelfunc)

// withtimeout和withdeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消context的意思
func withtimeout(parent context, timeout time.duration) (context, cancelfunc)

//withvalue函数和取消context无关,它是为了生成一个绑定了一个键值对数据的context,
// 绑定的数据可以通过context.value方法访问到,这是我们实际用经常要用到的技巧,一般我们想要通过上下文来传递数据时,可以通过这个方法,
// 如我们需要tarce追踪系统调用栈的时候。
func withvalue(parent context, key, val interface{}) context

http请求的context传递到异步任务的坑

看下面例子

我们将http的context传递到goroutine 中:

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func indexhandler(resp http.responsewriter, req *http.request) {
	ctx := req.context()
	go func(ctx context.context) {
		for {
			select {
			case <-ctx.done():
				fmt.println("gorountine off,the err is: ", ctx.err())
				return
			default:
				fmt.println(333)
			}
		}
	}(ctx)

	time.sleep(1000)
	resp.write([]byte{1})
}
func main() {

	http.handlefunc("/test1", indexhandler)
	http.listenandserve("127.0.0.1:8080", nil)
}

结果:

从上面结果来看,在http请求返回之后,传入gorountine的context被cancel掉了,如果不巧,你在gorountine中进行一些http调用或者rpc调用传入了这个context,那么对应的请求也将会被cancel掉。

因此,在http请求中异步任务出去时,如果这个异步任务中需要进行一些rpc类请求,那么就不要直接使用或者继承http的context,否则将会被cancel。

纠其原因

http请求再结束后,将会cancel掉这个context,所以异步出去的请求中收到的context是被cancel掉的。

下面来看下源代码:

listenandserve–>server:server方法中有一个大的for循环,这个for循环中,针对每个请求,都会起一个协程进行处理。

serve方法处理一个连接中的请求,并在一个请求serverhandler{c.server}.servehttp(w, w.req)结束后cancel掉对应的context:

// serve a new connection.
func (c *conn) serve(ctx context.context) {
	c.remoteaddr = c.rwc.remoteaddr().string()
	ctx = context.withvalue(ctx, localaddrcontextkey, c.rwc.localaddr())
	defer func() {
		if err := recover(); err != nil && err != erraborthandler {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.stack(buf, false)]
			c.server.logf("http: panic serving %v: %v\n%s", c.remoteaddr, err, buf)
		}
		if !c.hijacked() {
			c.close()
			c.setstate(c.rwc, stateclosed, runhooks)
		}
	}()

	if tlsconn, ok := c.rwc.(*tls.conn); ok {
		if d := c.server.readtimeout; d != 0 {
			c.rwc.setreaddeadline(time.now().add(d))
		}
		if d := c.server.writetimeout; d != 0 {
			c.rwc.setwritedeadline(time.now().add(d))
		}
		if err := tlsconn.handshake(); err != nil {
			// if the handshake failed due to the client not speaking
			// tls, assume they're speaking plaintext http and write a
			// 400 response on the tls conn's underlying net.conn.
			if re, ok := err.(tls.recordheadererror); ok && re.conn != nil && tlsrecordheaderlookslikehttp(re.recordheader) {
				io.writestring(re.conn, "http/1.0 400 bad request\r\n\r\nclient sent an http request to an https server.\n")
				re.conn.close()
				return
			}
			c.server.logf("http: tls handshake error from %s: %v", c.rwc.remoteaddr(), err)
			return
		}
		c.tlsstate = new(tls.connectionstate)
		*c.tlsstate = tlsconn.connectionstate()
		if proto := c.tlsstate.negotiatedprotocol; validnextproto(proto) {
			if fn := c.server.tlsnextproto[proto]; fn != nil {
				h := initalpnrequest{ctx, tlsconn, serverhandler{c.server}}
				// mark freshly created http/2 as active and prevent any server state hooks
				// from being run on these connections. this prevents closeidleconns from
				// closing such connections. see issue https://golang.org/issue/39776.
				c.setstate(c.rwc, stateactive, skiphooks)
				fn(c.server, tlsconn, h)
			}
			return
		}
	}

	// http/1.x from here on.

	ctx, cancelctx := context.withcancel(ctx)
	c.cancelctx = cancelctx
	defer cancelctx()

	c.r = &connreader{conn: c}
	c.bufr = newbufioreader(c.r)
	c.bufw = newbufiowritersize(checkconnerrorwriter{c}, 4<<10)

	for {
		// 从连接中读取请求
		w, err := c.readrequest(ctx)
		if c.r.remain != c.server.initialreadlimitsize() {
			// if we read any bytes off the wire, we're active.
			c.setstate(c.rwc, stateactive, runhooks)
		}
		.....
		.....
		// expect 100 continue support
		req := w.req
		if req.expectscontinue() {
			if req.protoatleast(1, 1) && req.contentlength != 0 {
				// wrap the body reader with one that replies on the connection
				req.body = &expectcontinuereader{readcloser: req.body, resp: w}
				w.canwritecontinue.settrue()
			}
		} else if req.header.get("expect") != "" {
			w.sendexpectationfailed()
			return
		}

		c.curreq.store(w)
		
		// 启动协程后台读取连接
		if requestbodyremains(req.body) {
			registeronhiteof(req.body, w.conn.r.startbackgroundread)
		} else {
			w.conn.r.startbackgroundread() 
		}

		// http cannot have multiple simultaneous active requests.[*]
		// until the server replies to this request, it can't read another,
		// so we might as well run the handler in this goroutine.
		// [*] not strictly true: http pipelining. we could let them all process
		// in parallel even if their responses need to be serialized.
		// but we're not going to implement http pipelining because it
		// was never deployed in the wild and the answer is http/2.
		serverhandler{c.server}.servehttp(w, w.req)
		/**
		* 重点在这儿,处理完请求后将会调用w.cancelctx()方法cancel掉context
		**/
		w.cancelctx()
		if c.hijacked() {
			return
		}
		w.finishrequest()
		if !w.shouldreuseconnection() {
			if w.requestbodylimithit || w.closedrequestbodyearly() {
				c.closewriteandwait()
			}
			return
		}
		c.setstate(c.rwc, stateidle, runhooks)
		c.curreq.store((*response)(nil))

		if !w.conn.server.dokeepalives() {
			// we're in shutdown mode. we might've replied
			// to the user without "connection: close" and
			// they might think they can send another
			// request, but such is life with http/1.1.
			return
		}

		if d := c.server.idletimeout(); d != 0 {
			c.rwc.setreaddeadline(time.now().add(d))
			if _, err := c.bufr.peek(4); err != nil {
				return
			}
		}
		c.rwc.setreaddeadline(time.time{})
	}
}

至此,我们知道,http请求在正常结束后将会主动cancel掉context。

此外,在请求异常时候也会主动cancel掉context(cancel目的就是为了快速失败),具体可见w.conn.r.startbackgroundread() 其中的实现。

在日常开发中,我们知道有时候会存在客户端超时情况,和ctx相关的原因可归纳如下:

  • 服务端收到的请求的request context被cancel掉。
  • 客户端本身收到context deadline exceeded错误
  • 服务端业务业务使用了http的context,但没有用于做rpc等需要建立连接的任务,那么客户端即使收到了context canceled的错误,服务端实际上还是在继续执行业务代码。
  • 服务端业务业务使用了http的context,并用于做rpc等需要建立连接的任务,那么客户端收到context canceled错误,并且服务端也会在对应的rpc等建立连接任务处返回context cancled的错误。

最后,如果context cancel掉了,但是业务又在继续执行,有时候并不是我们想要的结果,因为这会占用资源,因此我们可以主动在业务中通过监听context done的信号来做context canceled的处理,从而可以达到快速失败,节约资源的目的。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com