字节网课笔记(第一堂)|青训营笔记

136 阅读12分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

老师介绍的基础语法实在过于简略,新手一定会很吃力(我很吃力),可以参照极客兔兔的 Golang简明教程

我也正在啃《go语言圣经》后续会将我的笔记整理后发出来,现在也很多地方不明白哈哈哈 下面重点说下实战项目吧

实战项目1:猜数字

package main

import (
   "bufio"
   "fmt"
   "math/rand" //生成随机数的包
   "os"
   "strconv"
   "strings"
   "time"
)

//初始化随机数资源库,避免产生伪随机
func init() {
   rand.Seed(time.Now().UnixNano())
}
func main() {
   maxNum := 100 //设置生成随机数的上限

   //rand是生成随机数的函数
   //rand.Init()生成的是一个伪随机数,即程序运行多少次生成的随机数都是这一个
   //调用 rand.Seed (), 并且传入一个变化的值作为参数,如 time.Now().UnixNano() , 就是可以生成时刻变化的值
   secretNumber := rand.Intn(maxNum) //左闭右开区间

   //fmt.Println("The secret number is", secretNumber)

   fmt.Println("Please input your guess")
   // bufio.NewReader()是一种读取文件的方式
   reader := bufio.NewReader(os.Stdin) //从标准输入中接收数据 返回缓存区的一个切片
   for {
      input, err := reader.ReadString('\n')
      //以'\n'给间隔符 \n之后的数据不再读取
      // 其实这里并没多大用因为os.Stdin就是读取一行的数据 但是必须要输入参数
      //input 存放从缓存中读取的数据
      if err != nil {
         fmt.Println("An error occured while reading input. Please try again", err)
         continue
      }
      input = strings.TrimSuffix(input, "\n") //除去指定后缀
      guess, err := strconv.Atoi(input) //string型转int型
      if err != nil {
         fmt.Println("Invalid input. Please enter an integer value")
         return
      }
      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
      }
   }
}

对老师的代码进行了小修改加了一些注释 有一个地方 input = strings.TrimSuffix(input, "\n") //除去指定后缀 值得注意的是:Windows环境下的换行符是"\r\n" 所以如果你是在Windows系统的下开的IDE,那么上面的代码需要换成input = strings.TrimSuffix(input, "\r\n")(这个错我琢磨了好久,用上面的代码能够去掉\n并且输入的input也不带换行符但会产生err,表名这不是一个规范的输入 最开始我用input=input[:len(input)-1]也能实现去掉换行符,但是感觉很low问了大佬才知道是Windows环境下换行符是:\r\n 具体怎么来的有时间我再去研究) input = input[:len(input)-1]

扩展点1:关于golang中的随机数rand

代码中的randn是生成整数随机数,生成浮点数等有对应的函数 golang中生成的随机数下限都是从0开始要改变下线+/-某个数就ok。 rand.Intn()实现的是伪随机,加入时间戳实现随机,可在初始化函数里面就对随机数资源库进行初始化即func init(){}

扩展点2:关于golang中读取数据

bufio.NewReader()是一种读取文件的方式 详细请参考Go语言NewReader读取文件

os.Stdin是标准输入 和C++中的cin比较像以换行符结束但是换行符是被标准输入接收了才有了下面去换行符的代码 可参考标准输入stdin

关于bufio

详细可参考bufio包 NewReader()

// NewReaderSize 将 rd 封装成一个带缓存的 bufio.Reader 对象,
// 缓存大小由 size 指定(如果小于 16 则会被设置为 16)。
// 如果 rd 的基类型就是有足够缓存的 bufio.Reader 类型,则直接将
// rd 转换为基类型返回。
func NewReaderSize(rd io.Reader, size int) *Reader

// NewReader 相当于 NewReaderSize(rd, 4096)
func NewReader(rd io.Reader) *Reader

ReadString()

NewReader()创建一个bufio.Reader实例,表示创建一个从给定文件中读取数据的读取器对象。然后调用读取器对象(Reader实例)的ReadString()方法,这个方法以\n作为分隔符,它的分隔符必须只能是单字符,且必须使用单引号包围,因为它会作为byte读取。ReadString()读取来自os.Stdin的内容后将其保存到input变量中,同时返回是否出错的信息。ReadString()只有一种情况会返回err:没有遇到分隔符。 ReadString会将读取的内容包括分隔符都一起放进缓冲中,如果读取文件时读到了结尾,则会将整个文件内容放进缓冲,并将文件终止标识符io.EOF放进设置为err

实战项目2 抓包

首先该项目用的是http请求中的POST请求。相关知识需自行恶补计算机网络(我也没学完计网,学完了再把这部分知识补上)

image.png

关于这个请求包含的内容:这是一个JSON,JSON可以理解为一个轻量级的Javascript数据交换格式 source:资源 此处即我们要翻译的单词 trans_type:要翻译的形式:en2zh(英文 to 中文)

复制dict

image.png 复制成bash形式

image.png

package main 
import (
    "fmt" 
    "io/ioutil"
    "log" "net/http"
    "strings" 
) 
func main() {
    client := &http.Client{}
    var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data) 
    if err != nil { 
    log.Fatal(err)
    } 
    req.Header.Set("Connection", "keep-alive")
    req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="99", "Microsoft Edge";v="99"`)
    req.Header.Set("sec-ch-ua-mobile", "?0")
    req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
    Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39") req.Header.Set("app-name", "xy") req.Header.Set("Content-Type", "application/json;charset=UTF-8") req.Header.Set("Accept", "application/json, text/plain, */*")
    req.Header.Set("os-type", "web")
    req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi") 
    req.Header.Set("sec-ch-ua-platform", `"Windows"`)
    req.Header.Set("Origin", "https://fanyi.caiyunapp.com") 
    req.Header.Set("Sec-Fetch-Site", "cross-site") 
    req.Header.Set("Sec-Fetch-Mode", "cors") 
    req.Header.Set("Sec-Fetch-Dest", "empty")
    req.Header.Set("Referer", "https://fanyi.caiyunapp.com/") 
    req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") resp, err := client.Do(req)
    if err != nil {
    log.Fatal(err) 
    }
    defer resp.Body.Close() 
    bodyText, err := ioutil.ReadAll(resp.Body) 
    if err != nil {
    log.Fatal(err) 
    } 
    fmt.Printf("%s\n", bodyText) }

生成response body

package main

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

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

func main() {
   client := &http.Client{}
   //var data = strings.NewReader(`{"trans_type":"en_zh","source":"good"}`)
   request := DictRequest{TransType: "en2zh", Source: "good"}
   buf, err := json.Marshal(request)
   if err != nil {
      // log.Fatal() 函数在控制台屏幕上打印带有时间戳的指定消息
      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("Connection", "keep-alive")
   req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="99", "Microsoft Edge";v="99"`)
   req.Header.Set("sec-ch-ua-mobile", "?0")
   req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko)Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39")
   req.Header.Set("app-name", "xy")
   req.Header.Set("Content-Type", "application/json;charset=UTF-8")
   req.Header.Set("Accept", "application/json, text/plain, */*")
   req.Header.Set("os-type", "web")
   req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
   req.Header.Set("sec-ch-ua-platform", `"Windows"`)
   req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
   req.Header.Set("Sec-Fetch-Site", "cross-site")
   req.Header.Set("Sec-Fetch-Mode", "cors")
   req.Header.Set("Sec-Fetch-Dest", "empty")
   req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
   req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
   resp, err := client.Do(req)
   if err != nil {
      log.Fatal(err)
   }
   defer resp.Body.Close()
   bodyText, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Printf("%s\n", bodyText)
}

解析response body

package main

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

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 {
      KnownInLaguages int `json:"known_in_laguages"`
      Description     struct {
         Source string      `json:"source"`
         Target interface{} `json:"target"`
      } `json:"description"`
      ID   string `json:"id"`
      Item struct {
         Source string `json:"source"`
         Target string `json:"target"`
      } `json:"item"`
      ImageURL  string `json:"image_url"`
      IsSubject string `json:"is_subject"`
      Sitelink  string `json:"sitelink"`
   } `json:"wiki"`
   Dictionary struct {
      Prons struct {
         EnUs string `json:"en-us"`
         En   string `json:"en"`
      } `json:"prons"`
      Explanations []string      `json:"explanations"`
      Synonym      []string      `json:"synonym"`
      Antonym      []string      `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 main() {
   client := &http.Client{}
   request := DictRequest{TransType: "en2zh", Source: "good"}
   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("Connection", "keep-alive")
   req.Header.Set("DNT", "1")
   req.Header.Set("os-version", "")
   req.Header.Set("sec-ch-ua-mobile", "?0")
   req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
   req.Header.Set("app-name", "xy")
   req.Header.Set("Content-Type", "application/json;charset=UTF-8")
   req.Header.Set("Accept", "application/json, text/plain, */*")
   req.Header.Set("device-id", "")
   req.Header.Set("os-type", "web")
   req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
   req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
   req.Header.Set("Sec-Fetch-Site", "cross-site")
   req.Header.Set("Sec-Fetch-Mode", "cors")
   req.Header.Set("Sec-Fetch-Dest", "empty")
   req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
   req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
   req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
   resp, err := client.Do(req)
   if err != nil {
      log.Fatal(err)
   }
   defer resp.Body.Close()
   bodyText, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      log.Fatal(err)
   }
   var dictResponse DictResponse                 //DictResponse就是响应的json转换为golang的结构体变量
   err = json.Unmarshal(bodyText, &dictResponse) //反序列化json,将他转变到galong对应的结构体变量中
   if err != nil {
      log.Fatal(err)
   }
   fmt.Printf("%#v\n", dictResponse) //详细的打印结构体
}

上述代码是将所有的json转换为golang中结构体详细的打印出来,但是我们需要的并没有这么多,翻译的操作输出的只有用户想要看到的json

package main

import (
   "bytes"
   "encoding/json"
   "fmt"
   "io/ioutil"
   "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 {
      KnownInLaguages int `json:"known_in_laguages"`
      Description     struct {
         Source string      `json:"source"`
         Target interface{} `json:"target"`
      } `json:"description"`
      ID   string `json:"id"`
      Item struct {
         Source string `json:"source"`
         Target string `json:"target"`
      } `json:"item"`
      ImageURL  string `json:"image_url"`
      IsSubject string `json:"is_subject"`
      Sitelink  string `json:"sitelink"`
   } `json:"wiki"`
   Dictionary struct {
      Prons struct {
         EnUs string `json:"en-us"`
         En   string `json:"en"`
      } `json:"prons"`
      Explanations []string      `json:"explanations"`
      Synonym      []string      `json:"synonym"`
      Antonym      []string      `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{}
   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("Connection", "keep-alive")
   req.Header.Set("DNT", "1")
   req.Header.Set("os-version", "")
   req.Header.Set("sec-ch-ua-mobile", "?0")
   req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
   req.Header.Set("app-name", "xy")
   req.Header.Set("Content-Type", "application/json;charset=UTF-8")
   req.Header.Set("Accept", "application/json, text/plain, */*")
   req.Header.Set("device-id", "")
   req.Header.Set("os-type", "web")
   req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
   req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
   req.Header.Set("Sec-Fetch-Site", "cross-site")
   req.Header.Set("Sec-Fetch-Mode", "cors")
   req.Header.Set("Sec-Fetch-Dest", "empty")
   req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
   req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
   req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
   resp, err := client.Do(req)
   if err != nil {
      log.Fatal(err)
   }
   defer resp.Body.Close()
   bodyText, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      log.Fatal(err)
   }
   if resp.StatusCode != 200 { //返回的请求不一定是正确的,如果返回的状态码不是200如404之类的要打印错误
      log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
   }
   var dictResponse DictResponse                 //DictResponse就是response中json转换为golang的结构体变量
   err = json.Unmarshal(bodyText, &dictResponse) //反序列化json,将他转变到galong对应的结构体变量中
   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)
}

实战项目3:代理

image.png TCp echo server 简单来说输入什么就输出什么,具体可参考golang 实现TCP echo server

package main

import (
   "bufio"
   "log"
   "net"
)

func main() {
   server, err := net.Listen("tcp", "127.0.0.1:1080")
   if err != nil {
      panic(err)
   }
   for {
      client, err := server.Accept()
      if err != nil {
         log.Printf("Accept failed $v", err)
         continue
      }
      go process(client)
   }
}
func process(conn net.Conn) {
   defer conn.Close()
   reader := bufio.NewReader(conn)
   for {
      b, err := reader.ReadByte()
      if err != nil {
         break
      }
      _, err = conn.Write([]byte{b})
      if err != nil {
         break
      }
   }
}

值得注意的是,nc命令在Windows上没有预先支持,如果是Windows系统的话需要去下载。可参考这篇文章Windows下载使用nc(netcat)命令

socks5代理

  1. 与代理服务器握手 首先客户端向代理服务器发起握手请求, 其数据包格式如下所示:
// +----+----------+----------+
// |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 | METHOD |
// +----+--------+
// | 1  |   1    |
// +----+--------+

其中 VER 字段与客户端请求数据包的 VER 字段含义相同, 表征协议版本, 固定为 0x05 METHOD 字段表征代理服务器所选择的协议版本, 长度为 1 字节, 当然代理服务器可能对于客户端所声明的所有认证方法都不支持, 此时代理服务器将 METHOD 字段值为 0xFF, 客户端收到该数据包时便知晓代理服务器不支持自己所声明的认证方法, 即协商失败, 客户端应主动关闭 TCP 连接

当认证过程通过后, Socks 握手正式完成, 此时客户端向代理服务器发起正式请求以指示所要访问的目标进程的地址, 端口等信息

// +----+-----+-------+------+----------+----------+
// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1  |  1  | X'00' |  1   | Variable |    2     |
// +----+-----+-------+------+----------+----------+
// VER 版本号,socks5的值为0x05
// CMD 0x01表示CONNECT请求
// RSV 保留字段,值为0x00
// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
//   0x01表示IPv4地址,DST.ADDR为4个字节
//   0x03表示域名,DST.ADDR是一个可变长度的域名
// DST.ADDR 一个可变长度的值
// DST.PORT 目标端口,固定2个字节

代理服务器在收到以上请求后,返回数据包形式:

// +----+-----+-------+------+----------+----------+
// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1  |  1  | X'00' |  1   | Variable |    2     |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X’00’ succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT
  1. relay阶段 后面我没怎么听懂了,还没学完计网,有些吃力。完整代码附在后面了。比起老师的加了一些注释,把计网补了再来填坑吧。 完整代码
package main

import (
   "bufio"
   "context"
   "encoding/binary"
   "errors"
   "fmt"
   "io"
   "log"
   "net"
)

const (
   socks5Ver = 0x05
   cmdBind   = 0x01
   atypIPV4  = 0x01
   atypeHOST = 0x03
   atypeIPV6 = 0x04
)

func main() {
   server, err := net.Listen("tcp", "127.0.0.1:1080")
   if err != nil {
      panic(err)
   }
   for {
      client, err := server.Accept()
      if err != nil {
         log.Printf("Accept failed $v", err)
         continue
      }
      go process(client)
   }
}

func process(conn net.Conn) {
   defer conn.Close()
   reader := bufio.NewReader(conn)
   err := auth(reader, conn)
   if err != nil {
      log.Printf("client %v auth falied:%v", conn.RemoteAddr(), err) //失败打印日志
      return
   }
   err = connect(reader, conn)
   if err != nil {
      log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
      return
   }
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
   //客户端向代理服务器发起握手请求
   // +----+----------+----------+
   // |VER | NMETHODS | METHODS  |
   // +----+----------+----------+
   // | 1  |    1     | 1 to 255 |
   // +----+----------+----------+
   // VER: 协议版本,socks5为0x05
   // NMETHODS: 支持认证的方法数量
   // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。(每个认证方法的编码)
   //RFC预定义了一些值的含义,内容如下:
   //常用的认证方法编码方式
   // X’00’ NO AUTHENTICATION REQUIRED 0表示不需要认证
   // X’02’ USERNAME/PASSWORD 2代表认证用户名和密码
   //前两个信息是一个字节的我们可以用ReadByte()读取一个字节
   ver, err := reader.ReadByte()
   if err != nil {
      return fmt.Errorf("read ver failed:%w", err)
   }
   //如果读取的协议版本不是socks5Ver 打印错误日志
   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)
   }
   //方法的编码有多少个字节是由方法数决定的 一个方法对应一个编码
   method := make([]byte, methodSize)   //创建一个methodSize这么大的缓冲区
   _, err = io.ReadFull(reader, method) //用ReaderFull()将它填充满
   if err != nil {
      return fmt.Errorf("read method failed:%w", err)
   }
   log.Println("ver", ver, "method", method)
   //代理服务器在收到客户端发起的请求之后, 向客户端发回握手响应
   // +----+--------+
   // |VER | METHOD |
   // +----+--------+
   // | 1  |   1    |
   // +----+--------+
   //其中 VER 字段与客户端请求数据包的 VER 字段含义相同, 表征协议版本, 固定为 0x05
   //METHOD 字段表征代理服务器所选择的认证方法, 长度为 1 字节, 当然代理服务器可能对于客户端所声明的所有认证方法都不支持,
   //此时代理服务器将 METHOD 字段值为 0xFF, 客户端收到该数据包时便知晓代理服务器不支持自己所声明的认证方法,
   //即协商失败, 客户端应主动关闭 TCP 连接
   _, err = conn.Write([]byte{socks5Ver, 0x00}) //0X00代表选择的认证方式是不认证
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   //当认证过程通过后, Socks 握手正式完成, 此时客户端向代理服务器发起正式请求以指示所要访问的目标进程的地址, 端口等信息
   //其数据包格式如下所示:
   // +----+-----+-------+------+----------+----------+
   // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
   // +----+-----+-------+------+----------+----------+
   // | 1  |  1  | X'00' |  1   | Variable |    2     |
   // +----+-----+-------+------+----------+----------+
   // VER 版本号,socks5的值为0x05
   // CMD 0x01表示CONNECT请求
   // RSV 保留字段,值为0x00
   // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
   //   0x01表示IPv4地址,DST.ADDR为4个字节
   //   0x03表示域名,DST.ADDR是一个可变长度的域名
   // DST.ADDR 一个可变长度的值
   // DST.PORT 目标端口,固定2个字节

   //开始读取每个字段的信息
   buf := make([]byte, 4)            //前四个字段是定长的,我们创建4个字节的缓冲区来存储这四个字段的信息
   _, err = io.ReadFull(reader, buf) //把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", ver)
   }
   addr := "" //变长字符串来读取DST.ADDR中的信息
   switch atyp {
   case atypIPV4: //IPV4 长度为4byte,复用创建的buf
      _, 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])
   case atypeHOST: //变长字符串:域名
      //host先读取他的字节长度一个byte,再构建hostSize大小的缓冲区将信息填充进去
      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)
   case atypeIPV6:
      return errors.New("IPv6: no supported yet")
   default:
      return errors.New("invalid atyp")
   }
   //读取DST.PORT
   _, err = io.ReadFull(reader, buf[:2])
   if err != nil {
      return fmt.Errorf("read port failed:%w", err)
   }
   port := binary.BigEndian.Uint16(buf[:2])                       //binary里面的函数BigEndian(大端方式)解析该数据将其转换为无符号的int16
   dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port)) //Sprintf格式化输出
   if err != nil {
      return fmt.Errorf("dial dst failed:%w", err)
   }
   defer dest.Close()
   log.Println("dial", addr, port) //打印日志

   //代理服务器在收到以上请求后,返回数据包形式:
   // +----+-----+-------+------+----------+----------+
   // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
   // +----+-----+-------+------+----------+----------+
   // | 1  |  1  | X'00' |  1   | Variable |    2     |
   // +----+-----+-------+------+----------+----------+
   // VER socks版本,这里为0x05
   // REP Relay field,内容取值如下 X’00’ succeeded
   // RSV 保留字段
   // ATYPE 地址类型
   // BND.ADDR 服务绑定的地址
   // BND.PORT 服务绑定的端口DST.PORT
   _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) //很多字段都不是socks5代理需要使用的
   if err != nil {
      return fmt.Errorf("write failed: %w", err)
   }
   ctx, cancel := context.WithCancel(context.Background())
   defer cancel()
   go func() {
      _, _ = io.Copy(dest, reader) //从用户浏览器copy数据到服务器
      cancel()
   }()
   go func() {
      _, _ = io.Copy(conn, dest) //从底层服务器copy数据到用户浏览器
      cancel()
   }()
   <-ctx.Done() //等待ctx.Done()被执行 也是cancel()函数被调用的时机
   return nil
}