Context请求控制器

添加上下文 设置超时时间


从主流程中我们知道(第三层关键结论),HTTP 服务会为每个请求创建一个 Goroutine 进行服务处理
在服务处理的过程中,有可能就在本地执行业务逻辑,也有可能再去下游服务获取数据

逻辑链条中,每个本地处理逻辑,或者下游服务请求节点,都有可能存在超时问题。而对于 HTTP 服务而言,超时往往是造成服务不可用、甚至系统瘫痪的罪魁祸首
系统瘫痪也就是我们俗称的雪崩,某个服务的不可用引发了其他服务的不可用

如果服务超时,导致请求处理缓慢甚至不可用,加剧了 Goroutine 堆积,同时也造成了服务的请求堆积,Goroutine 堆积,瞬时请求数加大,
导致服务都不可用,整个系统瘫痪。
最有效的方法就是从源头上控制一个请求的“最大处理时长”,所以,对于一个 Web 框架而言,“超时控制”能力是必备的。

观察官方库提供的函数

go doc context | grep "^func"

// 创建退出 Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 创建有超时时间的 Context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
// 创建有截止时间的 Context
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithCancel 直接创建可以操作退出的子节点,
WithTimeout 为子节点设置了超时时间(还有多少时间结束),
WithDeadline 为子节点设置了结束时间线(在什么时间结束)。
通过定时器来自动触发终结通知”,WithTimeout 设置若干秒后通知触发终结,WithDeadline 设置未来某个时间点触发终结。
为一个父节点生成一个带有 Done 方法的子节点,并且返回子节点的 CancelFunc 函数句柄

Context数据结构分析

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	// 当 Context 被取消或者到了 deadline,返回一个被关闭的 channel
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

//函数句柄
type CancelFunc func()

context 标准库设计思路

在整个树形逻辑链条中,用上下文控制器 Context,实现每个节点的信息传递和共享
用 Context 定时器为整个链条设置超时时间,时间一到,结束事件被触发,链条中正在处理的服务逻辑会监听到,从而结束整个逻辑链条,让后续操作不再进行。

一个节点其实有两个角色:一是下游树的管理者;二是上游树的被管理者
一个是能让整个下游树结束的能力,也就是函数句柄 CancelFunc;
另外一个是在上游树结束的时候被通知的能力,也就是 Done() 方法。同时因为通知是需要不断监听的,所以 Done() 方法需要通过 channel 作为返回值让使用方进行监听
CancelFunc 是主动让下游结束,而 Done 是被上游通知结束。

context是怎样产生得

Context 在哪里产生?它的上下游逻辑是什么?
每个连接的 Context 最终是放在 request 结构体中的
Context 已经有多层父节点。因为,在代码中,每执行一次 WithCancel、WithValue,就封装了一层 Context
Context 从哪里产生这个问题,我们就解决了,但是如果我们想要对 Context 进行必要的修改,还要从上下游逻辑中,找到它的修改点在哪里。
生成最终的 Context 的流程中,net/http 设计了两处可以注入修改的地方,都在 Server 结构里面,一处是 BaseContext,另一处是 ConnContext。

BaseContext 是整个 Context 生成的源头,如果我们不希望使用默认的 context.Backgroud(),可以替换这个源头。
而在每个连接生成自己要使用的 Context 时,会调用 ConnContext ,第二个参数是 net.Conn,能让我们对某些特定连接进行设置,比如要针对性设置某个调用 IP。

要自定义,不是想直接使用标准库的 Context,因为它完全是标准库 Context 接口的实现,只能控制链条结束,封装性并不够。但是只有先搞清楚了 context 标准库的设计思路,才能精准确定自己能怎么改、改到什么程度合适
type Server struct {
  
	  // BaseContext 用来为整个链条创建初始化 Context
	  // 如果没有设置的话,默认使用 context.Background()
	BaseContext func(net.Listener) context.Context{}
  
	  // ConnContext 用来为每个连接封装 Context
	  // 参数中的 context.Context 是从 BaseContext 继承来的
	ConnContext func(ctx context.Context, c net.Conn) context.Context{}

  }

简单的网络获取解析

返回查询字符串和请求实体

//解析所有请求数据
 r.ParseForm()
//获取所有请求数据
 r.Form()
//是一个包含所有请求数据的字典类型(map),包含 URL 查询字符串和 POST 请求数据

//request 对象上的 Form 可以获取所有请求参数,包括查询字符串和请求实体,并且不限请求类型。如果你想要进一步要获取指定的参数值,可以以索引方式获取指定参数对应的值,也可以通过 Form 提供的 Get 方法,就像我们从一个普通字典类型获取键值一样
id1 := r.Form["id"]		//字符串切片
id2 := r.Form.Get("id")	//字符串值

只想获得请求实体(只包含了 POST 表单请求数据,不包含 URL 查询字符串)

func EditPost(w http.ResponseWriter, r *http.Request)  {
	r.ParseForm()
    fmt.Println("form data:", r.PostForm)
}

获取用户请求数据

FormValue/PostFormValue
func EditPost(w http.ResponseWriter, r *http.Request)  {
    fmt.Println("post id:", r.FormValue("id"))
    fmt.Println("post title:", r.PostFormValue("title"))
    fmt.Println("post title:", r.PostFormValue("content"))
    io.WriteString(w, "表单提交成功")
}
获取用户请求数据,使用它们的好处是不再需要单独调用 ParseForm 对表单数据进行解析,
不过使用这两个方法的时候只能获取特定请求数据,不能一次获取所有请求数据
FormValue/PostFormValue 的区别和 Form/PostForm 一样,
这里通过命名就可以看出来,前者可以获取所有 GET/POST 请求数据(即查询字符串和请求实体),后者只能获取 POST 请求实体数据
只能解析通过 application/x-www-form-urlencoded 编码的数据。

封装一个Context

我们需要有更强大的 Context,除了可以控制超时之外,常用的功能比如获取请求、返回结果、实现标准库的 Context 接口,也都要有。

未自定义的控制器代码


// 控制器
func Foo1(request *http.Request, response http.ResponseWriter) {
  obj := map[string]interface{}{
    "data":   nil,
  }
    // 设置控制器 response 的 header 部分
  response.Header().Set("Content-Type", "application/json")

    // 从请求体中获取参数
  foo := request.PostFormValue("foo")
  if foo == "" {
    foo = "10"
  }
  fooInt, err := strconv.Atoi(foo)
  if err != nil {
    response.WriteHeader(500)
    return
  }
    // 构建返回结构
  obj["data"] = fooInt 
  byt, err := json.Marshal(obj)
  if err != nil {
    response.WriteHeader(500)
    return
  }
    // 构建返回状态,输出返回结构
  response.WriteHeader(200)
  response.Write(byt)
  return
}
调用了 http.Request 和 http.ResponseWriter ,实现 WebService 接收和处理协议文本的功能。
但这两个结构提供的接口粒度太细了,需要使用者非常熟悉这两个结构的内部字段
将这些内部实现封装起来,对外暴露语义化高的接口函数,易用性就会提升


// 控制器
func Foo2(ctx *framework.Context) error {
  obj := map[string]interface{}{
    "data":   nil,
  }
    // 从请求体中获取参数
   fooInt := ctx.FormInt("foo", 10)
    // 构建返回结构  
  obj["data"] = fooInt
    // 输出返回结构
  return ctx.Json(http.StatusOK, obj)
}

controller.go 实现

封装标准的Context库

标准库的 Context 通用性非常高,基本现在所有第三方库函数,都会根据官方的建议,将第一个参数设置为标准 Context 接口。
所以我们封装的结构只有实现了标准库的 Context,才能方便直接地调用。

自己封装的 Context 最终需要提供四类功能函数:
base 封装基本的函数功能,比如获取 http.Request 结构
context 实现标准 Context 接口
request 封装了 http.Request 的对外接口
response 封装了 http.ResponseWriter 对外接口
context.go实现

为单个请求设置超时

封装了自定义的 Context,从设计层面实现了标准库的 Context。为单个请求提供超时逻辑

继承 request 的 Context,创建出一个设置超时时间的 Context;
创建一个新的 Goroutine 来处理具体的业务逻辑;
设计事件处理顺序,当前 Goroutine 监听超时时间 Contex 的 Done() 事件,和具体的业务处理结束事件,哪个先到就先处理哪个。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值