错误处理策略

  • 隐藏内部细节直接返回错误:上层不清楚错误是在哪一层导致的,只是知道了错误
  • 返回和检查错误值: 通过返回特定值表示是否成功或具体的错误,有点类似linux中的错误码处理
  • 自定义错误类型:逻辑中增加类型断言,断言成功就是具体的错误

隐藏内部细节直接返回错误

conn, err := net.Dial("tcp", "golang.org:80")
if err != nil {
    return err
}

返回和检查错误值

fp, err := os.Open("./xxx.txt")  
if err != nil {
    fmt.Println("打开文件失败。", err)
    return
}
defer fp.Close()
for {
    n, err2 := fp.Read(buf)
    if err2 == io.EOF {  // io.EOF表示文件末尾
        fmt.Println("文件读取结束")
        break
    }
    fmt.Print(string(buf[:n]))
}

自定义错误类型

type MyError struct {
    error errors.error
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("message:%s error=%s", e.Message, errors.Error())
}

func test() *MyError{
    return &MyError{Message:"xxx", error:errors.New("error")}
}
func main() {
    err := test()
    if _,ok := err.(*MyError);ok {
        //错误处理
    }
}

比较出名的errors包

因为golang errors比较“简洁”,所以这个时候出现了一些解决痛点的包
比较出名的是
https://github.com/pkg/errors

可以输出具体的堆栈

package main
import (
    "fmt"
    "github.com/pkg/errors"
)

func test() error {
    cause := errors.New("test")
    return errors.WithStack(cause)
}

func main() {
    err := test()
    fmt.Printf("%+v", err)
}

输出

test
main.test
    /Users/timi/go/src/test/test.go:1485
main.main
    /Users/timi/go/src/test/test.go:1490
runtime.main
    /usr/local/go/src/runtime/proc.go:203
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1357
main.test
    /Users/timi/go/src/test/test.go:1486
main.main
    /Users/timi/go/src/test/test.go:1490
runtime.main
    /usr/local/go/src/runtime/proc.go:203
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1357%

多层嵌套错误


import (
    "fmt"
    "github.com/pkg/errors"
)

func error1() error {
    return errors.New("error1")
}

func inner() error {
    err := error1()
    if err != nil {
        return errors.Wrap(err, "inner")
    }
    return nil
}

func middle() error {
    err := inner()
    if err != nil {
        return errors.Wrap(err, "middle")
    }
    return nil
}
func outer() error {
    err := middle()
    if err != nil {
        return errors.Wrap(err, "outer")
    }
    return nil
}

func main() {
    err := outer()
    fmt.Printf("%+v\n", err)
    `fmt.Println(errors.Cause(err))

}

输出

error1
main.error1
    /Users/timi/go/src/test/test.go:1485
main.inner
    /Users/timi/go/src/test/test.go:1489
main.middle
    /Users/timi/go/src/test/test.go:1497
main.outer
    /Users/timi/go/src/test/test.go:1504
main.main
    /Users/timi/go/src/test/test.go:1512
runtime.main
    /usr/local/go/src/runtime/proc.go:203
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1357
inner
main.inner
    /Users/timi/go/src/test/test.go:1491
main.middle
    /Users/timi/go/src/test/test.go:1497
main.outer
    /Users/timi/go/src/test/test.go:1504
main.main
    /Users/timi/go/src/test/test.go:1512
runtime.main
    /usr/local/go/src/runtime/proc.go:203
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1357
middle
main.middle
    /Users/timi/go/src/test/test.go:1499
main.outer
    /Users/timi/go/src/test/test.go:1504
main.main
    /Users/timi/go/src/test/test.go:1512
runtime.main
    /usr/local/go/src/runtime/proc.go:203
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1357
outer
main.outer
    /Users/timi/go/src/test/test.go:1506
main.main
    /Users/timi/go/src/test/test.go:1512
runtime.main
    /usr/local/go/src/runtime/proc.go:203
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1357
++++++++
outer: middle: inner: error1

golang1.13 官方标准包errors新特性

主要增加了几个函数
fmt.Errorf//包装 每次调用包一次
errors.Unwrap//解包装 每次调用解一次
errors.Is
errors.As
单讲语法没什么意义
主要将以前写法和现在写法的区别
这里有个需要注意的点,就是1.13开始支持错误链,类似上面实现的那样 error1->inner->middle->outer

//错误链演示
import (
    "errors"
    "fmt"
)

var IamError error = errors.New("i am error1")

func error1() error {
    return IamError
}

func inner() error {
    err := error1()
    if err != nil {
        return fmt.Errorf("inner,%w", err)
    }
    return nil
}

func middle() error {
    err := inner()
    if err != nil {
        return fmt.Errorf("middle,%w", err)
    }
    return nil
}
func outer() error {
    err := middle()
    if err != nil {
        return fmt.Errorf("outer,%w", err)
    }
    return nil
}

func main() {
    err := outer()

    fmt.Println(err)
    fmt.Println("++++++++")
    fmt.Println(errors.Is(err, IamError))

}

输出

outer,middle,inner,i am error1
++++++++
true
if err == io.EOF//以前这么写判断
if errors.Is(err, io.EOF)//1.13可以这样写 只要错误链含有io.EOF就返回true
if _,ok := err.(*MyError);ok {//以前这么写判断

var myError *MyError//1.13可以这样写
errors.As(newErr, &myError)//1.13可以这样写 只要错误链含有myError就返回true

费米思维

费米思维,是著名的美籍意大利物理学家费米,给大家提出了一个解决难题的对策、思路。我们在管理过程中,领导者、管理者往往会面对一个巨大的目标,无从下手,面对一个大的困境,会总觉得我不知怎么如何切入,因为解决这个问题的信息资源有局限,条件资源有局限,这个问题怎么解决,无从下手。

费米给大家提供了一个解决难题的对策、思路,是什么对策思路呢?当领导者、管理者面对一个大的目标,你要善于把它分解成若干个次一级的目标,当我们领导者、管理者从次一级目标入手的时候,很快就会找到次一级问题的正确答案,可能无需求助于专家,无需求助于书本知识,可能很快根据你管理的经验和知识,就能找到解决次一级目的的正确答案,这时候你已经在逐渐接近整体问题的正确答案了。

比如,你想知道地球周围的大气质量是多少,这个问题处理起来好像无从下手,但是稍有物理知识的人都知道一个标准大气压约为1.01×105帕,大气有压强完全是因为大气有重力,而地球的半径约为6400千米是我们熟悉的物理量,求出地球的表面积后再乘以大气压强的值,我们就顺利地得到地球周围大气的总重力,进而顺利地得到地球上空气的总质量。

解题路径:

  • 从上到下/左右开弓:从宏观层面由上层往下推/由从某个点横向切入,反推上去;
  • 上下限:利用上下限思想,“夹”逼推出来;
  • 第三者:直接求是不行,往往通过计算出第三者才能知道答案。

前两步骤中可以运用Top-down,bottom-up法,中心思想:分别从两个角度展开。一是从宏观层面,由上往下推。

分析方法:

时间分析法、空间分析法

例子

估算深圳市丰田汽车的数量

层级拆解:深圳市汽车(第一层)、丰田汽车(第二层)、丰田(横向可以是特斯拉)

分析思路1:Top-down

先假设已知深圳市全市共有机动车300万辆;(一层)
在深圳市几个车流密集区域采样,记录每100辆车中丰田车的占比,取平均占比;(二层)
假如平均占比为25% ,那么得出:300万×25%=75万辆。
分析思路2:Bottom-up

先假设已知深圳全市有「特斯拉」8万辆;(与丰田是横向同层)
在深圳某地多点采样得出:在每100辆汽车车中,「特斯拉」车的占比为2%,丰田车的占比为20%;
那么得出:8万/2%*20%=80万辆。(利用横向反推)
补充边界

(1)在对丰田汽车采样的时候,选择什么地点呢?(证明边界有考虑)

选择密集的区域就正确?如口岸、机场、商场等是否有影响数据的真实性?
若只取口岸附近的车流密集区,则占比有可能偏高。(香港牌保姆车多数是什么品牌?)
那么应该取多少个才比较“均衡”呢?
(2)在对特斯拉进行采样时,该怎么分析?(证明你对特斯拉有研究,知道是可以充电)

若取样地点是「特斯拉充电桩」附近,同样也会造成数据偏高的情况;
特斯拉在深圳属于什么定位?

美国飞机的乘客数有多少

两个角度回答这个问题。

  1. 从美国总人口数入手。

考虑求解此问题的可行公式:美国飞机的乘客数=美国的总人口数×平均乘坐次数/人/年
问题变成估算:美国总人口数(这个数据要记住,估算问题常用到,3×10的8次幂)
平均乘坐次数/人/年:考虑大多数一年出差一次(2次),少部分频繁出行→取平均的话,3次
即可求解啦。

  1. 从美国飞机场的运客能力入手。

考虑求解此问题的可行公式:美国飞机的乘客数=机场数×(航班数/机场)×(乘客数/航班)×365天/年

芝加哥有多少个调音师

计算模型:调音师 = 每天所需调钢琴数 ÷ 每个钢琴师每天可调钢琴数

从需求端进行分析,弹钢琴属于一个中等需求,但国外孩子兴趣浓,家长喜欢让孩子有丰富的课外活动,我们需要知道芝加哥的总钢琴数、每个钢琴师每天可跳钢琴数(top-down法)

假设所需数据数据如下:

共需调弦钢琴总数为37.5万个(芝加哥人口500万,每4个人组成一个家庭,30%的家庭中有钢琴,则500万÷4×30%=37.5万个钢琴,每个钢琴5年调弦一次

每个钢琴师每天可调弦数为4个

据此估计芝加哥有调弦师:375000÷(5×365)÷4=51位

常见的2种工具

tcpcopy是用c写的
gor用go写的

实现原理

原理和我之前blog的tcp流量监听器差不多

gor

goreplay提供了独特的阴影处理方法。后台goreplay不再是代理,而是侦听网络接口上的流量,无需更改生产基础结构,而是在与服务相同的计算机上运行goreplay守护程序。

tcpcopy

如图所示,TCPCopy由两部分组成: tcpcopy和intercept。当tcpcopy在联机服务器上运行并捕获联机请求时,拦截在辅助服务器上运行,并执行一些辅助工作,例如将响应信息传递到tcpcopy。应该注意的是,测试应用程序在目标服务器上运行。

tcpcopy默认情况下使用原始套接字输入技术来捕获网络层的联机数据包并进行必要的处理(包括TCP交互模拟,网络等待时间控制和通用上层交互模拟),并且默认情况下使用原始套接字输出技术来将数据包发送到目标服务器(图中粉红色箭头所示)。

在目标服务器上进行TCPCopy所需的唯一操作是设置适当的路由命令,以将响应数据包(图中绿色箭头所示)路由到辅助服务器。

拦截负责将响应头(默认情况下)传递给tcpcopy。通过捕获响应数据包,intercept将提取响应头信息,并使用特殊通道将响应头发送到tcpcopy(图中紫色箭头所示)。当tcpcopy接收到响应标头时,它将利用标头信息来修改联机数据包的属性,并继续发送另一个数据包。应当注意,来自目标服务器的响应被路由到辅助服务器,该服务器应充当blackhole(来什么接收什么,就是不转发到服务器或者目标,相当于要访问什么就阻止访问什么)。

tcpcopy配置比较麻烦,重点讲下gor的使用吧

gor功能

  • 捕获和重放流量
  • 重播HTTP流量
  • 重播二进制协议
  • 记录和重放保持活动的TCP会话
  • 从文件保存和重放
  • 限速
  • 请求过滤
  • 要求改写

要求

必须装有tcpdump

使用

实时引流

现在在终端中运行以下命令:

[root@xxx]# gor --input-raw :20000 --output-http 192.168.0.100:1111 

将本机的:20001的请求,同时打到192.168.0.100:1111

保存请求到文件并回放

[root@xxx]# gor --input-raw :20000 --output-file requests.gor

回放请求

gor --input-file requests.gor --output-http "192.168.0.100:1111"

重播二进制协议

Gor包含使用二进制格式(例如thrift或)的基本支持protocol-buffers。要开始设置--input-raw-protocol为“二进制”(默认为“ http”)。要重放,您应该使用--output-binary,例如:

gor --input-raw :80 --input-raw-protocol binary --output-binary staging:8081

在与--input-raw您一起工作时,您可能会注意到将消息发送到输出之前有2秒的延迟。因为对于一般的二进制协议,不可能知道TCP消息何时结束,所以这种现象是预期发生的,因此Gor必须设置不活动超时。每个协议都有自己的规则(例如,将消息长度写为第一个字节),并且需要单独处理才能知道消息长度。

请注意,您可以将所有负载测试功能用于二进制协议。例如,以下命令将循环播放并以10倍速重放记录的有效负载30秒:

gor --input-file './binary*.gor|1000%' --output-binary staging:9091 --input-file-loop --exit-after 30s

作用

  • 可以用golang实现本机的tcp抓包
  • 可以用来做tcp流量复制工具
  • 在用户态实现异常流量监控

demo源码

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "log"
    "net"
    "strings"
)

func main() {
    netaddr, _ := net.ResolveIPAddr("ip4", interfaceAddress("eth0"))
    conn, _ := net.ListenIP("ip4:tcp", netaddr)
    for {
        buf := make([]byte, 1480)
        n, addr, _ := conn.ReadFrom(buf)
        tcpheader := NewTCPHeader(buf[0:n])
        log.Println(n, addr, tcpheader)
    }
}

func interfaceAddress(ifaceName string) string {
    iface, err := net.InterfaceByName(ifaceName)
    if err != nil {
        panic(err)
    }
    addr, err := iface.Addrs()
    if err != nil {
        panic(err)
    }
    addrStr := strings.Split(addr[0].String(), "/")[0]
    return addrStr
}

const (
    FIN = 1  // 00 0001
    SYN = 2  // 00 0010
    RST = 4  // 00 0100
    PSH = 8  // 00 1000
    ACK = 16 // 01 0000
    URG = 32 // 10 0000
)

type TCPHeader struct {
    Source      uint16
    Destination uint16
    SeqNum      uint32
    AckNum      uint32
    DataOffset  uint8 // 4 bits
    Reserved    uint8 // 3 bits
    ECN         uint8 // 3 bits
    Ctrl        uint8 // 6 bits
    Window      uint16
    Checksum    uint16 // 如果它为0内核将会自动设置
    Urgent      uint16
    Options     []TCPOption
}

type TCPOption struct {
    Kind   uint8
    Length uint8
    Data   []byte
}

// Parse packet into TCPHeader structure
func NewTCPHeader(data []byte) *TCPHeader {
    var tcp TCPHeader
    r := bytes.NewReader(data)
    binary.Read(r, binary.BigEndian, &tcp.Source)
    binary.Read(r, binary.BigEndian, &tcp.Destination)
    binary.Read(r, binary.BigEndian, &tcp.SeqNum)
    binary.Read(r, binary.BigEndian, &tcp.AckNum)

    var mix uint16
    binary.Read(r, binary.BigEndian, &mix)
    tcp.DataOffset = byte(mix >> 12)  // top 4 bits
    tcp.Reserved = byte(mix >> 9 & 7) // 3 bits
    tcp.ECN = byte(mix >> 6 & 7)      // 3 bits
    tcp.Ctrl = byte(mix & 0x3f)       // bottom 6 bits

    binary.Read(r, binary.BigEndian, &tcp.Window)
    binary.Read(r, binary.BigEndian, &tcp.Checksum)
    binary.Read(r, binary.BigEndian, &tcp.Urgent)

    return &tcp
}

func (tcp *TCPHeader) HasFlag(flagBit byte) bool {
    return tcp.Ctrl&flagBit != 0
}
func (tcp *TCPHeader) String() string {
    if tcp == nil {
        return "<nil>"
    }
    return fmt.Sprintf("Source=%v Destination=%v SeqNum=%v AckNum=%v DataOffset=%v Reserved=%v ECN=%v Ctrl=%v Window=%v Checksum=%v Urgent=%v", tcp.Source, tcp.Destination, tcp.SeqNum, tcp.AckNum, tcp.DataOffset, tcp.Reserved, tcp.ECN, tcp.Ctrl, tcp.Window, tcp.Checksum, tcp.Urgent)
}

func (tcp *TCPHeader) Marshal() []byte {

    buf := new(bytes.Buffer)
    binary.Write(buf, binary.BigEndian, tcp.Source)
    binary.Write(buf, binary.BigEndian, tcp.Destination)
    binary.Write(buf, binary.BigEndian, tcp.SeqNum)
    binary.Write(buf, binary.BigEndian, tcp.AckNum)

    var mix uint16
    mix = uint16(tcp.DataOffset)<<12 | // top 4 bits
        uint16(tcp.Reserved)<<9 | // 3 bits
        uint16(tcp.ECN)<<6 | // 3 bits
        uint16(tcp.Ctrl) // bottom 6 bits
    binary.Write(buf, binary.BigEndian, mix)

    binary.Write(buf, binary.BigEndian, tcp.Window)
    binary.Write(buf, binary.BigEndian, tcp.Checksum)
    binary.Write(buf, binary.BigEndian, tcp.Urgent)

    for _, option := range tcp.Options {
        binary.Write(buf, binary.BigEndian, option.Kind)
        if option.Length > 1 {
            binary.Write(buf, binary.BigEndian, option.Length)
            binary.Write(buf, binary.BigEndian, option.Data)
        }
    }

    out := buf.Bytes()

    // Pad to min tcp header size, which is 20 bytes (5 32-bit words)
    pad := 20 - len(out)
    for i := 0; i < pad; i++ {
        out = append(out, 0)
    }

    return out
}

// Csum TCP Checksum
func Csum(data []byte, srcip, dstip [4]byte) uint16 {

    pseudoHeader := []byte{
        srcip[0], srcip[1], srcip[2], srcip[3],
        dstip[0], dstip[1], dstip[2], dstip[3],
        0,                  // zero
        6,                  // protocol number (6 == TCP)
        0, byte(len(data)), // TCP length (16 bits), not inc pseudo header
    }

    sumThis := make([]byte, 0, len(pseudoHeader)+len(data))
    sumThis = append(sumThis, pseudoHeader...)
    sumThis = append(sumThis, data...)
    //fmt.Printf("% x\n", sumThis)

    lenSumThis := len(sumThis)
    var nextWord uint16
    var sum uint32
    for i := 0; i+1 < lenSumThis; i += 2 {
        nextWord = uint16(sumThis[i])<<8 | uint16(sumThis[i+1])
        sum += uint32(nextWord)
    }
    if lenSumThis%2 != 0 {
        //fmt.Println("Odd byte")
        sum += uint32(sumThis[len(sumThis)-1])
    }

    // Add back any carry, and any carry from adding the carry
    sum = (sum >> 16) + (sum & 0xffff)
    sum = sum + (sum >> 16)

    // Bitwise complement
    return uint16(^sum)
}

使用原因

分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许HTTP由网页服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供。

通常,HTTP应答消息中发送的数据是整个发送的,Content-Length消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。通常数据块的大小是一致的,但也不总是这种情况。

原理

  • HTTP分块传输编码允许服务器为动态生成的内容维持HTTP持久链接。通常,持久链接需要服务器在开始发送消息体前发送Content-Length消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。
  • 分块传输编码允许服务器在最后发送消息头字段。对于那些头字段值在内容被生成之前无法知道的情形非常重要,例如消息的内容要使用散列进行签名,散列的结果通过HTTP消息头字段进行传输。没有分块传输编码时,服务器必须缓冲内容直到完成后计算头字段的值并在发送内容前发送这些头字段的值。
  • HTTP服务器有时使用压缩 (gzip或deflate)以缩短传输花费的时间。分块传输编码可以用来分隔压缩对象的多个部分。在这种情况下,块不是分别压缩的,而是整个负载进行压缩,压缩的输出使用本文描述的方案进行分块传输。在压缩的情形中,分块编码有利于一边进行压缩一边发送数据,而不是先完成压缩过程以得知压缩后数据的大小。

格式

如果一个HTTP消息(包括客户端发送的请求消息或服务器返回的应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。

每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及换行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。

最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。

消息最后以CRLF结尾。

golang实现

package main
import (
    "flag"
    "log"
    "net"
    "os"
)

var host = flag.String("host", "127.0.0.1", "host")
var port = flag.String("port", "80", "port")

func main() {
    flag.Parse()
    var l net.Listener
    var err error
    l, err = net.Listen("tcp", *host+":"+*port)
    if err != nil {
        log.Println("Error listening:", err)
        os.Exit(1)
    }
    defer l.Close()
    for {
        conn, err := l.Accept()
        if err != nil {
            log.Println("Error accepting: ", err)
            os.Exit(1)
        }
        go handleRequest(conn)
    }
}
func handleRequest(conn net.Conn) {
    defer conn.Close()
    conn.Write([]byte("HTTP/1.1 200 OK\r\n"))
    conn.Write([]byte("Transfer-Encoding: chunked\r\n"))
    conn.Write([]byte("\r\n"))
    conn.Write([]byte("8\r\n"))
    conn.Write([]byte("12345678\r\n"))
    conn.Write([]byte("7\r\n"))
    conn.Write([]byte("timiguo\r\n"))

    conn.Write([]byte("0\r\n"))
    conn.Write([]byte("\r\n"))

}