02 Context的使用

对于 HTTP 服务而言,超时往往是造成服务不可用、甚至系统瘫痪的罪魁祸首。

context 标准库设计思路

为了防止雪崩,context 标准库的解决思路是:在整个树形逻辑链条中,用上下文控制器 Context,实现每个节点的信息传递和共享。

具体操作是:用 Context 定时器为整个链条设置超时时间,时间一到,结束事件被触发,链条中正在处理的服务逻辑会监听到,从而结束整个逻辑链条,让后续操作不再进行。
(也就是一个ctx到处传递)

在这里插入图片描述

从图中最后一层的代码 req.ctx = ctx 中看到,每个连接的 Context 最终是放在 request 结构体中的(这也是每个连接的request的ctx赋值的时机)。

所以,每个连接的Context 都是基于 baseContext 复制而来,对应到代码中就是,在为某个连接开启 Goroutine 的时候,为当前连接创建了一个 connContext,这个 connContext 是基于 server 中的 Context 而来,而 server 中 Context 的基础就是 baseContext。

因此,生成最终的 Context 的流程中,net/http 设计了两处可以注入修改的地方,都在 Server 结构里面,一处是 BaseContext,另一处是 ConnContext。

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
    ...
}

最后,我们回看一下 req.ctx 是否能感知连接异常。
是可以的,因为链条中一个父节点为 CancelContext,其 cancelFunc 存储在代表连接的 conn 结构中,连接异常的时候,会触发这个函数句柄。

总结:标准包中ctx基于baseCtx逐级传递到每个连接中,经过多次的withCancel
、withValue,其中baseCtx和ConnCtx都是可以自己注入的。

封装一个自己的Context

在框架中使用Ctx,除了可以控制超时外,常用的有获取请求、返回结果、实现标准库ctx接口,也都要有。
先看一个不封装自定义ctx的控制器代码:

// 控制器, 接收参数为*http.Request, http.ResponseWriter
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
}

可以发现代码较为复杂,如果我们将内部代码封装起来,对外暴露高度语义化的接口函数,则框架的易用性会明显提升。比如:

// 控制器, 接收参数为ctx 
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)
}

这样封装性高,也更优雅易读。
这样就要求总的入口函数处handler(也就是core)的ServceHTTP方法内部将http.request和http.ResponseWriter进行封装。

func (c *Core) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := NewContext(r, w)
	handler := c.router["foo"] // r.URL.path
	if handler == nil {
		return
	}

	handler(ctx)
}

功能点:

  • 获取请求、返回结果
  • 实现标准库的Context接口
  • 为单个请求设置超时

一个简单的foo处理函数:

func FooControllerHandler(ctx *framework.Context) error {
	finish := make(chan struct{}, 1)
	panicChan := make(chan interface{}, 1)

	durationCtx, cancel := context.WithTimeout(ctx.BaseContext(), 1*time.Second)
	defer cancel()

	go func() {
		defer func() {
			if p := recover(); p != nil {
				panicChan <- p
			}
		}()

		time.Sleep(10 * time.Second)
		ctx.Json(200, "ok")
		finish <- struct{}{}
	}()

	select {
	case <-finish:
		fmt.Println("finish")
	case p := <-panicChan:
		ctx.WriterMux().Lock()
		defer ctx.WriterMux().Unlock()
		log.Println(p)
		ctx.Json(500, "panic")
	case <-durationCtx.Done():
		ctx.WriterMux().Lock()
		defer ctx.WriterMux().Unlock()
		ctx.Json(500, "time out")
		ctx.SetHasTimeout()
	}

	return nil
}

这里注意有几点:

  1. Go中每开启一个goroutine,最好使用defer recover()语句来捕获panic异常
  2. 在写入responseWriter时注意加锁
  3. 如果超时后,不允许再写入responseWriter
  4. url和body中参数的获取,分别用http.Request.RUL.Query()和http.Request.PostForm

【总结】:

  1. go的net/http在进入处理每个请求的逻辑时,最重要的是调用了server.handler.ServerHTTP(w http.ResponseWriter, r *http.Request), 因此整个框架就是围绕着这个函数改编而来
  2. 自定义的ctx由 w http.ResponseWriterr *http.Request封装而来,这样handler处理函数就可以接收此封装的ctx参数
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
libusb 是一个用户空间的 USB 库,它可以帮助我们在 Linux、Windows 和 Mac OS X 等操作系统上访问 USB 设备。下面是一个简单的示例程序,演示如何使用 libusb 读取一个 USB 设备的描述符信息。 首先,我们需要安装 libusb 库。在 Ubuntu 上,可以使用以下命令安装: ``` sudo apt-get install libusb-1.0-0-dev ``` 接下来,我们编写一个 C 语言程序。程序的主要流程如下: 1. 初始化 libusb 库。 2. 打开 USB 设备。 3. 读取设备描述符信息。 4. 关闭 USB 设备。 5. 释放 libusb 库。 程序代码如下: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <libusb-1.0/libusb.h> int main(int argc, char** argv) { libusb_device_handle* handle = NULL; libusb_context* context = NULL; unsigned char descriptor[1024]; int r; // 初始化 libusb 库 r = libusb_init(&context); if (r < 0) { fprintf(stderr, "libusb_init error %d\n", r); return 1; } // 打开 USB 设备 handle = libusb_open_device_with_vid_pid(NULL, 0x1234, 0x5678); if (handle == NULL) { fprintf(stderr, "libusb_open_device_with_vid_pid error\n"); goto exit; } // 读取设备描述符信息 r = libusb_get_descriptor(handle, LIBUSB_DT_DEVICE, 0, descriptor, sizeof(descriptor)); if (r < 0) { fprintf(stderr, "libusb_get_descriptor error %d\n", r); goto close; } // 打印设备描述符信息 printf("Device Descriptor:\n"); for (int i = 0; i < r; i++) { printf("%02x ", descriptor[i]); if ((i + 1) % 16 == 0) { printf("\n"); } } printf("\n"); close: // 关闭 USB 设备 libusb_close(handle); exit: // 释放 libusb 库 libusb_exit(context); return 0; } ``` 在上面的程序中,我们使用 libusb_open_device_with_vid_pid 函数打开 USB 设备,其中的 VID 和 PID 分别是设备的厂商 ID 和产品 ID。我们使用 libusb_get_descriptor 函数读取设备描述符信息。最后,我们使用 libusb_close 函数关闭 USB 设备,使用 libusb_exit 函数释放 libusb 库。 注意:在运行程序之前,请将 VID 和 PID 替换为你自己的设备的厂商 ID 和产品 ID。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值