2019年12月

Go1.13 标准库的 http 包重大 bug


概述

2019 年 11 月 21 日,golang 的官方 github 仓库提交了一个 https://github.com/golang/go/issues/35750 ,该 issue 指出如果初始化 http.Server 结构体时指定了一个非空的 ConnContext 成员函数,且如果在该函数内使用了 context.Value 方法写入数据到上下文并返回,则 Context 将会以链表的方式泄漏。

这是一个很恐怖的 bug,因为一方面 http.Server 是几乎所有市面上流行 web 框架如 gin,beego 的底层依赖,一旦发生问题则全部中招,一个都跑不了;另一方面则 ConnContext 函数在每一个请求初始化时都会被调用,这意味着如果一旦发生泄漏,则服务端程序几乎一定会溢出。

据官方开发人员表示,该 bug 于 1.13 版本引进,目前已经在 1.13.5 修复。

影响范围

所有 1.13~1.13.4 版本,使用原生 http.Server 指定了 ConnContext 成员函数,且在该函数中使用 With*方法写入数据并返回新 Context;或者使用了上层框架的相应功能。

现象

内存根据访问量持续上升,且 pprof 分析发现 cpu 大量耗费在 Context 的底层方法上。

故障原理

问题出在 Server.Serve 方法,该方法是 http.Server 启动的底层唯一入口,负责循环 Accept 连接以及为每个新连接开启 goroutine 做下一步处理。

来看看出问题的代码,为了简洁这里略去不必要代码:

type Server struct {
    ...
    // ConnContext optionally specifies a function that modifies
    // the context used for a new connection c. The provided ctx
    // is derived from the base context and has a ServerContextKey
    // value.
    ConnContext func(ctx context.Context, c net.Conn) context.Context
    ...
}

func (srv *Server) Serve(l net.Listener) error {
        ...
    baseCtx := context.Background()
        ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
            ...
        if cc := srv.ConnContext; cc != nil {
            ctx = cc(ctx, rw)
            if ctx == nil {
                panic("ConnContext returned nil")
            }
        }
                ...
        go c.serve(ctx)
    }
}

我们知道 Context 是一个反向链表结构,从最初的 Background 通过各种 With 方法推入表头节点,而 With 方法返回的则是新的表头节点。

从上边的代码中我们看到,如果 srv.ConnContext 不为空,则每次 Accept 连接后都会调用此函数并传入 ctx,然后将返回的结果存入 ctx 中,这意味着如果在此函数中使用 With 函数写入节点并返回,则该节点将被缓存到全局的 ctx,从而造成泄漏。

复现

这个 bug 非常容易复现,下面我们复现一下:

// go version:1.13.4

func main() {
    var count int32 = 0
    server := &http.Server{
        Addr: ":4444",
        Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
            rw.Header().Set("Connection", "close")
        }),
        ConnContext: func(ctx context.Context, c net.Conn) context.Context {
            atomic.AddInt32(&count, 1)
            if c2 := ctx.Value("count"); c2 != nil {
                fmt.Printf("发现了遗留数据: %d\n", c2.(int32))
            }
            fmt.Printf("本次数据: %d\n", count)
            return context.WithValue(ctx, "count", count)
        },
    }
    go func() {
        panic(server.ListenAndServe())
    }()

    var err error

    fmt.Println("第一次请求")
    _, err = http.Get("http://localhost:4444/")
    if err != nil {
        panic(err)
    }
    fmt.Println("\n第二次请求")

    _, err = http.Get("http://localhost:4444/")
    if err != nil {
        panic(err)
    }
}

结果:

第一次请求
本次数据: 1

第二次请求
发现了遗留数据: 1
本次数据: 2

可以看到,第二个从请求的 Context 中能读取到第一个请求的 Context 中写入的数据,确实发生了泄漏。

修复

我们首先要理解 ConnContext 这个函数的作用,按照设计它应该是为每个请求的 Context 做一些初始化处理,然后将这个处理后的 Context 链传入go c.serve(ctx),而不应该缓存到全局;下一个请求过来后应该将原始的 Context 传入 ConnContext 进行处理,从而得到新的 Context 链。

明白了目的,再看看问题代码,我们发现罪魁祸首在这里

ctx = cc(ctx, rw)

这一行错误地将 cc 方法生成的新链缓存到了全局,导致泄漏(ps:实在是搞不懂 google 的大神居然会犯这么低级且致命的错误...)。

修复后的代码如下:

func (srv *Server) Serve(l net.Listener) error {
    ...
    baseCtx := context.Background()
    ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
        ...
        connCtx = ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        ...
        go c.serve(connCtx)
    }
}

timg.jpg


wenyan-lang 中文文言文编程语言


screenshot01.png

学程序难?英语不会?没关系,现在中文编程语言来了!!!

Helloworld

文言:

吾有一數。曰三。名之曰「甲」。
為是「甲」遍。
    吾有一言。曰「「問天地好在。」」。書之。
云云。

等同于以下 JavaScript:

var n = 3;
for (var i = 0; i < n; i++) {
    console.log("問天地好在。");
}

输出:

問天地好在。
問天地好在。
問天地好在。

看;
看了;
看了以;
看了以后;
看了以后更;
看了以后更懵;
看了以后更懵逼;

screenshot03.png


golang之UrlEncode编码/UrlDecode解码


为什么需要编码和解码

1.是因为当字符串数据以url的形式传递给web服务器时,字符串中是不允许出现空格和特殊字符的;
2.因为 url 对字符有限制,比如把一个邮箱放入 url,就需要使用 urlencode 函数,因为 url 中不能包含 @ 字符;
3.url转义其实也只是为了符合url的规范而已。因为在标准的url规范中中文和很多的字符是不允许出现在url中的。

哪些字符是需要转化的呢?

1. ASCII 的控制字符

这些字符都是不可打印的,自然需要进行转化。

2. 一些非ASCII字符

这些字符自然是非法的字符范围。转化也是理所当然的了。

3. 一些保留字符

很明显最常见的就是“&”了,这个如果出现在url中了,那你认为是url中的一个字符呢,还是特殊的参数分割用的呢?

4. 就是一些不安全的字符了。

例如:空格。为了防止引起歧义,需要被转化为“+”。
明白了这些,也就知道了为什么需要转化了,而转化的规则也是很简单的。

按照每个字符对应的字符编码,不是符合我们范围的,统统的转化为%的形式也就是了。自然也是16进制的形式。

5.和字符编码无关

通过urlencode的转化规则和目的,我们也很容易的看出,urleocode是基于字符编码的。同样的一个汉字,不同的编码类型,肯定对应不同的urleocode的串。gbk编码的有gbk的encode结果。
apache等服务器,接受到字符串后,可以进行decode,但是还是无法解决编码的问题。编码问题,还是需要靠约定或者字符编码的判断解决。
因此,urleocode只是为了url中一些非ascii字符,可以正确无误的被传输,至于使用哪种编码,就不是encode所关心和解决的问题了。
编码问题,不是urlencode所要解决的。

golang之UrlEncode编码/UrlDecode解码

package main

import(
    "fmt"
    "net/url"
)

func main()  {
    var urlStr string = "傻了吧:%:%@163& .html.html"
    escapeUrl := url.QueryEscape(urlStr)
    fmt.Println("编码:",escapeUrl)

    enEscapeUrl, _ := url.QueryUnescape(escapeUrl)
    fmt.Println("解码:",enEscapeUrl)
}

输出

编码: %E5%82%BB%E4%BA%86%E5%90%A7%3A%25%3A%25%40163%26+.html.html
解码: 傻了吧:%:%@163& .html.html

0926382a0ae4cf26d6b7ace130b60b86.jpg


bootstrap4提示框怎么自动关闭


CSS

<style>
    .msg {
      background-color: red;
      width: 30%;
      height: 50%;

      position: absolute;
      left: 100;
      top: 0;
      right: 0;
      bottom: 0;
      pointer-events: none;

    }
  </style>

HTML

<!-- 信息提示 -->
  <div class="msg" id='msg'>
  </div>

Js

//消息弹窗
  //str :信息
  //code: 状态;默认为2;  1成功/2信息/3警告/4错误/5首选/6次要的/7深灰色 
  var i = 0
  function msg(str, code) {
    i++;
    var mstStateClass = 'alert alert-success alert-dismissible'
    switch (code) {
      case 1:
        mstStateClass = 'alert alert-info alert-dismissible'
        break;
      case 2:
        mstStateClass = 'alert alert-warning alert-dismissible'
        break;
      case 3:
        mstStateClass = 'alert alert-danger alert-dismissible'
        break;
      case 4:
        mstStateClass = 'alert alert-primary alert-dismissible'
        break;
      case 5:
        mstStateClass = 'alert alert-secondary alert-dismissible'
        break;
      case 6:
        mstStateClass = 'alert alert-dark alert-dismissible'
        break;
      case 7:
        mstStateClass = 'alert alert-light alert-dismissible'
        break;

    }
    var htmlStr = '<div id="msg_id_' + i + '"  class="' + mstStateClass + '"><button type="button" class="close" data-dismiss="alert"></button>' + str + '</div>'
    $("#msg").append(htmlStr)
    removeHtml($("#msg_id_" + i))
  }
  //根据dom删除元素
  function removeHtml(dom) {
    setTimeout(function () {
      dom.slideUp(300, function () {
        dom.remove();
      });
    }, 1 * 1000);//延迟5000
  }

使用

只要在需要的地方调用 msg方法就行~

效果图

Peek 2019-12-08 20-48.gif


GoLang并发通信


信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。

(“箭头”就是数据流的方向。)

和映射与切片一样,信道在使用前必须创建:

ch := make(chan int)

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // 将和送入 c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从 c 中接收

    fmt.Println(x, y, x+y)
}