Go 工程实践 | 青训营笔记

70 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

三个实践项目
猜数字使用了随机数生成/标准IO流/字符串处理/for循环
字典使用了HTTP请求/JSON解析/代码生成
socks5代理使用了net网络连接/goroutine/context机制等

1. 猜数字

1.1 生成随机数

math/rand包实现了伪随机数生成器。

随机数从资源生成。同一包水平的函数都使用的默认的公共资源。该资源会在程序每次运行时都产生确定的序列。如果需要每次运行产生不同的序列,应使用Seed函数进行初始化。

    maxNum := 100
    rand.Seed(time.Now().UnixNano())//用时间戳来初始化随机数种子
    secretNumber := rand.Intn(maxNum)

1.2 bufio实现标准IO流的处理和strings字符串处理的应用

bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。

    reader := bufio.NewReader(os.Stdin)//将标准stdin文件转为一个只读的带缓冲的流
    input, err := reader.ReadString('\n')//ReadString读取直到第一次遇到指定字节,返回一个包含已读取的数据和delim字节的字符串
    if err != nil {//当且仅当ReadString方法返回的切片不以指定字节结尾时,会返回一个非nil的错误
            fmt.Println("An error occured while reading input. Please try again", err)
            return
    }
    input = strings.TrimSuffix(input, "\n")//返回将s后端指定字符包含的utf-8码值都去掉的字符串。windows系统为\r\n

    guess, err := strconv.Atoi(input)//Atoi是ParseInt(s, 10, 0)的简写,字符串转换为10进制整数
    if err != nil {//返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange
            fmt.Println("Invalid input. Please enter an integer value")
            return
    }

1.3 for循环实现功能逻辑

实现思路1

  • 设置死循环,当猜测命中时break跳出循环

实现思路2

  • 将不命中设置为循环条件,此时需要注意guess变量需要在循环体外声明

2.词典

2.1 HTTP请求

HTTP Web API 的 CLI 封装

    client := &http.Client{}//开启一个http客户端(可指定参数如timeout),用于管理HTTP客户端的头域、重定向策略和其他设置
    data =strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)//创建一个post请求,设置请求类型/url/数据读取流
    /*
    设置请求头
    */
    resp, err := client.Do(req)//发起请求,返回结果保存到变量response
    defer resp.Body.Close()//为防止资源泄露用defer设置关流,函数结束之后从下往上触发
    bodyText, err := ioutil.ReadAll(resp.Body)//读取响应
    if resp.StatusCode != 200 {//确认返回状态正常,否则后续结果为空难以排查bug
            log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
    }
    

2.2 JSON解析

通过设置结构体DictRequest和DictResponse用于对请求的输入输出流进行序列化和反序列化

2.3 代码生成

使用curlconverter.com/go/ 可将curl命令及结果转换为go代码
使用oktools.net/json2go 可将JSON转换为go的结构体

3. socks5代理服务器

3.1 socks5原理

四阶段:协商——认证——请求——relay
此处实现简易版本,是明文传输没有加密,省略认证阶段

image.png

3.2 TCP echo server 实现

net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。
可通过Dial、Listen和Accept等函数提供的基本接口和相关的Conn(代表通用的面向流的网络连接)和Listener接口实现简易服务器设置。
go 启动一个子线程
nc 用于对网络连接进行调试和探测

3.3 auth协商阶段

客户端发送代理请求

                   +----+----------+----------+
                   |VER | NMETHODS | METHODS  |
                   +----+----------+----------+
                   | 1  |    1     | 1 to 255 |
                   +----+----------+----------+
  • VER: 协议版本,socks5为0x05
  • NMETHODS: 支持认证的方法数量
  • METHODS: 是一个数组,包含了支持的认证方式列表,对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
    • X’00’ NO AUTHENTICATION REQUIRED 不需要认证
    • X’02’ USERNAME/PASSWORD 用户名和密码方式认证

服务端收到客户端的代理请求后,选择双方都支持的加密方式将VER和METHODS回复给客户端
此时客户端收到服务端的响应请求后,双方握手完成,开始进行协议交互

3.4 connect请求阶段

与auth阶段的函数签名一致,传入只读流和连接

        +----+-----+-------+------+----------+----------+
        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+
  • VER 版本号,socks5的值为0x05
  • CMD 代理指令
    • 0x01表示CONNECT请求,TCP代理时使用
  • RSV 保留字段,值为0x00
  • ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
    • 0x01表示IPv4地址,DST.ADDR为4个字节
    • 0x03表示unix域socket类型代理域名,DST.ADDR是一个可变长度的域名
    • 0x04: IPv6地址类型
  • DST.ADDR 需要连接的目的地址
  • DST.PORT 目标端口,固定2个字节

读取到目标地址和端口后使用net.Dial函数与host建立连接

3.5 response 响应阶段

        +----+-----+-------+------+----------+----------+
        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

其中VER/RSV/ATYP的含义同上,其他字段的意思:

  • REP Relay field,内容取值如下
    • X’00’ succeeded
    • X’01’-X’08’: 失败
    • X’09’-X’ff’: 未使用
  • BND.ADDR 连接到的远程地址
  • BND.PORT 服务绑定的端口
    • 0x00: 成功

3.6 双向数据传输

底层服务器与用户浏览器
使用io.copy同时开两个goroutine

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    go func() {
            _, _ = io.Copy(dest, reader)
            cancel()
    }()
    go func() {
            _, _ = io.Copy(conn, dest)
            cancel()
    }()

    <-ctx.Done()

等待任一方copy失败,某一个连接关闭了才结束程序,使用context机制等待一个cancel被调用才结束。