字节青训营第一天笔记-3个实战项目| 青训营笔记

76 阅读7分钟

Go实战案例

  1. 猜数字

package main

import (
    "bufio"
    "fmt"
    "math/rand"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {

    maxNum := 100
    //以现在的时间戳作为随机数种子
    rand.Seed(time.Now().UnixNano())
    
    //产生随机数,最大值不大于100
    secretNumber := rand.Intn(maxNum)
    // fmt.Println("The secret number is ", secretNumber)

    
    fmt.Println("Please input your guess")
    //把输入转换成一个带缓冲的数据流
    reader := bufio.NewReader(os.Stdin)
    for {
        //读取字符串,碰到\n结束本次读取
       input, err := reader.ReadString('\n')
       if err != nil {
          fmt.Println("An error occured while reading input. Please try again", err)
          continue
       }
       //去掉文本两端的空格
       input = strings.Trim(input, "\r\n")
        
        //字符转换为数字
       guess, err := strconv.Atoi(input)
       if err != nil {
          fmt.Println("Invalid input. Please enter an integer value")
          continue
       }
       fmt.Println("You guess is", guess)
       //数字判断
       if guess > secretNumber {
          fmt.Println("Your guess is bigger than the secret number. Please try again")
       } else if guess < secretNumber {
          fmt.Println("Your guess is smaller than the secret number. Please try again")
       } else {
          fmt.Println("Correct, you Legend!")
          break
       }
    }
}

语法基础复习

  1. 词典

第三方调用,http请求,json字符串处理

  • 找到请求(edge浏览器)

  • 复制curl

  • 使用网址工具将curl转换为Go语言请求: curlconverter.com/
  • 根据原Json请求, 编写请求封装结构体
var data = strings.NewReader(`{"trans_type":"en2zh","source":"cat"}`)

//to, 其中Source是要翻译的单词, 编写成变量, 用函数参数传递
type DictRequest struct {
    TransType string `json:"trans_type"`
    Source    string `json:"source"`
    UserID    string `json:"user_id"`
}

  • 将请求封装到结构体对象中, Json序列化
request := DictRequest{TransType: "en2zh", Source: word}
buf, err := json.Marshal(request)
if err != nil {
    log.Fatal(err)
}
  • 在响应返回后, 做响应状态码判断
if resp.StatusCode != 200 {
    log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
  • 将响应正文反序列化, 得到 dictResponse结构体对象
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
    log.Fatal(err)
}
  • 将需要展示的信息打印出来
fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
    fmt.Println(item)
}
  • 完整代码
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)

type DictRequest struct {
    TransType string `json:"trans_type"`
    Source    string `json:"source"`
    UserID    string `json:"user_id"`
}

type DictResponse struct {
    Rc   int `json:"rc"`
    Wiki struct {
    } `json:"wiki"`
    Dictionary struct {
       Prons struct {
          EnUs string `json:"en-us"`
          En   string `json:"en"`
       } `json:"prons"`
       Explanations []string      `json:"explanations"`
       Synonym      []interface{} `json:"synonym"`
       Antonym      []interface{} `json:"antonym"`
       WqxExample   [][]string    `json:"wqx_example"`
       Entry        string        `json:"entry"`
       Type         string        `json:"type"`
       Related      []interface{} `json:"related"`
       Source       string        `json:"source"`
    } `json:"dictionary"`
}

func query(word string) {
    client := &http.Client{}
    //var data = strings.NewReader(`{"trans_type":"en2zh","source":"cat"}`)
    request := DictRequest{TransType: "en2zh", Source: word}
    buf, err := json.Marshal(request)
    if err != nil {
       log.Fatal(err)
    }
    var data = bytes.NewReader(buf)
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
       log.Fatal(err)
    }
    req.Header.Set("authority", "api.interpreter.caiyunai.com")
    req.Header.Set("accept", "application/json, text/plain, */*")
    req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7,en-GB;q=0.6,ja;q=0.5")
    req.Header.Set("app-name", "xy")
    req.Header.Set("content-type", "application/json;charset=UTF-8")
    req.Header.Set("device-id", "ff311187df232f0dd4a5e308daf4cdb9")
    req.Header.Set("origin", "https://fanyi.caiyunapp.com")
    req.Header.Set("os-type", "web")
    req.Header.Set("os-version", "")
    req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
    req.Header.Set("sec-ch-ua", `"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`)
    req.Header.Set("sec-ch-ua-mobile", "?0")
    req.Header.Set("sec-ch-ua-platform", `"Windows"`)
    req.Header.Set("sec-fetch-dest", "empty")
    req.Header.Set("sec-fetch-mode", "cors")
    req.Header.Set("sec-fetch-site", "cross-site")
    req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35")
    req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
    resp, err := client.Do(req)
    if err != nil {
       log.Fatal(err)
    }
    defer resp.Body.Close()
    bodyText, err := io.ReadAll(resp.Body)
    if err != nil {
       log.Fatal(err)
    }
    if resp.StatusCode != 200 {
       log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
    }
    var dictResponse DictResponse
    err = json.Unmarshal(bodyText, &dictResponse)
    if err != nil {
       log.Fatal(err)
    }
    fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
    for _, item := range dictResponse.Dictionary.Explanations {
       fmt.Println(item)
    }

}

func main() {
    if len(os.Args) != 2 {
       fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
       `)
       os.Exit(1)
    }
    word := os.Args[1]
    query(word)
}
  1. socks5代理服务器

socks5

  • 代理服务器原理

    1. 握手阶段: 浏览向sock5代理发起请求,内容包括:协议的版本号, 支持认证的种类. sock5会选择其中一个认证方式,返回给浏览器. 返回00表示不需要认证, 其它代表了具体认证类型
    2. 认证阶段
    3. 请求阶段: 浏览器向sock5服务器发起请求, 内容包括: 版本号, 请求类型(一般是connection请求), 代理服务器就与目标ip建立链接
    4. relay阶段: 浏览器发送请求, 经由代理服务器转换到真正的服务器, 真正的服务器的响应也经由代理服务器返回. 锁仓k不关心流量的细节
  • 分为4个阶段完成

    1. 简点的echo server
    2. 完成auth认证
    3. 完成请求阶段
    4. 完成relay阶段
v1
  • 使用net.listen监听一个端口,会返回一个server
server, err := net.Listen("tcp", "127.0.0.1:1080")
  • 死循环中使用accept, 持续接受连接
for {
    client, err := server.Accept()
    if err != nil {
       log.Printf("Accept failed %v ", err)
       continue
    }
    go process(client)
}

其中, go关键字表示启动一个 goroutinue(类似于子线程)

  • 实现处理函数process

    • func process(conn net.Conn){
          ......
      }
      
    • 回退式地处理, 关闭连接

    • defer conn.Close()
      
    •   defer关键字修饰的语句会在程序结束后, 由后向前执行

    • 使用bufio.NewReader创建带缓冲的只读流来读取输入

    • reader := bufio.NewReader(conn)
      for {
          b, err := reader.ReadByte()
          if err != nil {
             break
          }
      
    •    带缓冲的流底层可以合并读写操作, 可以减少系统调用的次数. 这边一个一个字节读取,出现err说明读取结束

    • 把读到的byte返回

    • _, err = conn.Write([]byte{b})
      
v2
  • 写上需要用到的常量
const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03 //域名
const atypeIPV6 = 0x04
  • 写一个空的auth,需要参数redwe用来读取数据, conn链接对象
func auth(reader *bufio.Reader, conn net.Conn) (err error) {

}
  • 实现auto的逻辑

    • 读取浏览器数据包, 浏览器向代理服务器发送的数据包包括:

      • vesion协议版本号, 一个字节, 固定时版本5
      • ver, err := reader.ReadByte()
        if err != nil {
            return fmt.Errorf("read ver failed:%w", err)
        }
        if ver != socks5Ver {
            return fmt.Errorf("not supported ver:%v", ver)
        }
        
      • 认证的方法数, 一个字节
      • methodSize, err := reader.ReadByte()
        if err != nil {
            return fmt.Errorf("read methodSize failed:%w", err)
        }
        
      • 具体的方法, 用切片储存,每个方法是一个字节, 使用io.readFull读取, 会读到将切片填满为止
      • method := make([]byte, methodSize)
        _, err = io.ReadFull(reader, method)
        if err != nil {
            return fmt.Errorf("read method failed:%w", err)
        }
        
  • 将代理服务器接受到的的版本号和选择的认证方式传回浏览器. 我们选择不进行认证0x00

_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
    return fmt.Errorf("write failed:%w", err)
}
  • 正常进行返回nil
return nil
v3
  • 也是先读取浏览器发送的数据包, 包括如下字段:

    • version版本号,一个字节,固定为5

    • RSV保留字段, 我们不需要关心

    • command请求类型,我们只支持connection连接, connection表示要与代理服务器建立tcp连接

    • atype目标地址的类型,支持三种情况 IPV4,IPV6和域名(Host)

    •   我们使用切片储存

    • buf := make([]byte, 4)
      _, err = io.ReadFull(reader, buf)
      if err != nil {
          return fmt.Errorf("read header failed:%w", err)
      }
      ver, cmd, atyp := buf[0], buf[1], buf[3]
      if ver != socks5Ver {
          return fmt.Errorf("not supported ver:%v", ver)
      }
      if cmd != cmdBind {
          return fmt.Errorf("not supported cmd:%v", cmd)
      }
      
    • 然后是目标地址,根据atyp的类型,地址长度不同

      1.     定义变量储存addr, 使用switch判断atyp类型
      2. addr := ""
        switch atyp {
            ......
        }
        
      3. IP4: IP4的大小是4个byte, 我们直接使用之前定义的buf储存
      4. case atypeIPV4:
            _, err = io.ReadFull(reader, buf)
            if err != nil {
               return fmt.Errorf("read atyp failed:%w", err)
            }
            addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
        
      5. Host: Host的第一个字节宝石主及地址的大小, 之后才是addr
      6. hostSize, err := reader.ReadByte()
        if err != nil {
            return fmt.Errorf("read hostSize failed:%w", err)
        }
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        if err != nil {
            return fmt.Errorf("read host failed:%w", err)
        }
        addr = string(host)
        
      7. IP6: IP6我们不支持, 也处理一下其它不支持的情况
      8. case atypeIPV6:
            return errors.New("IPv6: no supported yet")
        default:
            return errors.New("invalid atyp")
        
    • 最后是访问的端口号,都是两个字节, 也使用buf储存

    • _, err = io.ReadFull(reader, buf[:2])
      if err != nil {
          return fmt.Errorf("read port failed:%w", err)
      }
      
  • 将读到的端口号按大端序列转化成数字
  • port := binary.BigEndian.Uint16(buf[:2])
    
  • 代理服务器返回数据包包括如下字段:

    • version版本号,0x05
    • 返回类型, 成功返回0x00
    • 保留字段, 返回0即可
    • atype类型, 返回0x01
    • 第五个, 我们不关心, 返回0
    • 第六个, 我们不关心, 返回0
    • _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
      if err != nil {
          return fmt.Errorf("write failed: %w", err)
      }
      return nil
      
v4
  • 使用net.dial向刚才获得的 addr和port发起tcp链接,发起链接后要记得用defer来关闭连接
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
    return fmt.Errorf("dial dst failed:%w", err)
}
defer dest.Close()
log.Println("dial", addr, port)
  • 使用go关联字开启两个协程, 使用io.copy进行数据转发. 使用context机制确保所有协程结束之后才返回connenct函数, Done方法会在调用cancel后返回,程序可以继续进行. 任一协程先完成后,被Done堵塞,另一一个携程结束时调用cancel结束这个堵塞, 但本身也被堵塞, 被defer 的cancel释放
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

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

<-ctx.Done()
  • 使用SwitchyOmega使用代理服务器

新建情景模式, 使用socks5协议,服务器 127.0.0.1 , 端口 1080