Go 语言的实战案例 | 青训营笔记

61 阅读14分钟

1、猜数字游戏

游戏介绍:每次游戏程序会自动生成一个0~100的随机数,玩家输入自己猜的数字,程序会告诉玩家你猜的数字大于或小于正确答案,直到用户输入正确的答案为止

1.1 生成随机数

这里要导入math/rand包,用maxNum声明并变量赋值,并调用rand包下的Intn函数产生随机数(最大为maxNum)

package main

import (
   "fmt"
   "math/rand"
)

func main() {
   maxNum := 100
   secretNumber := rand.Intn(maxNum)
   fmt.Println("The secret number is ", secretNumber)
}

但是运行后出现了问题,每次运行都会显示随机数为81,原因是没有设置随机数的种子,导致每次产生的随机数相同。

解决方法:通常用时间戳来初始化随机数种子

修改代码如下:

package main

import (
   "fmt"
   "math/rand"
   "time"
)

func main() {
   maxNum := 100
   rand.Seed(time.Now().UnixNano())
   secretNumber := rand.Intn(maxNum)
   fmt.Println("The secret number is ", secretNumber)
}

修改后每次运行都会产生不同的随机数结果。

1.2 接收用户的输入并输出

go接收输入可以通过scanf这种比较简单的方式来实现,此处为后续项目的学习做准备,使用较麻烦的一种方式。首先new一个reader,通过reader的ReadString方法去读一个字符串

reader :=bufio.NewReader(os.Stdin)
input, err :=reader.ReadString('\n')

注意使用ReadString方法得到的字符串再结尾会有一个换行符,要通过strings包中的Trim方法消去换行。得到一个数字字符串后,在利用前面的Atoi方法将字符串转换为数字,最后输出用户的输入进行检验,代码如下:

package main

import (
   "bufio"
   "fmt"
   "os"
   "strconv"
   "strings"
)

func main() {
   fmt.Println("Please input your guess")
   reader := bufio.NewReader(os.Stdin)
   input, err := reader.ReadString('\n')
   if err != nil {
      fmt.Println("An error occured while reading input. Please try again", err)
      return
   }
   input = strings.Trim(input, "\r\n")
   guess, err := strconv.Atoi(input)
   if err != nil {
      fmt.Println("Invalid input. Please enter an integer value")
      return
   }
   fmt.Println("You guess is", guess)
}

此处要注意包的引入,要包含我们用到的函数,并且每次对数据进行处理时都要进行必要的错误校验,如果发生异常要进行相应处理。

1.3 逻辑的判断与循环

逻辑 的判断使用if else语句比较即可

需要注意的是,整个用户输入,提示语的输出应该是在大循环内的,以保证游戏是一直玩下去的

当用户输入了正确答案的时候,要break退出循环

用户在输入,程序处理代码时如果出现错误,不是卡死退出,而要continue跳出该回合,用户继续游戏

游戏完整代码如下:

package main

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

func main() {
   maxNum := 100
   rand.Seed(time.Now().UnixNano())
   secretNumber := rand.Intn(maxNum)
   // fmt.Println("The secret number is ", secretNumber)
   fmt.Println("Please input your guess")
   reader := bufio.NewReader(os.Stdin)
   for {
      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
      }
   }
}

2、在线命令行词典

在命令行输入一个单词会输出这个单词的音标,词性和释义。

2.1 初步尝试

首先打开一个彩云小译的网页,输入要翻译的单词good,点击翻译后,打开网页开发者工具,选择network(网络),找到一个请求方法为POST的 dict,从负载和预览中可以看到一些请求的详细信息,我们在用golang开发时,也要使用对应的api。

这种请求代码一般比较复杂,这里介绍一种可以代码生成的方法,首先右键dict选择复制----copy as cURL

打开代码生成网址:curlconverter.com/#go

将我们刚刚复制的一串代码输入到上方curl command(bash),会自动生成代码,我们将其复制到编辑器,代码解析参考注释

package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
   "strings"
)

func main() {
   client := &http.Client{} //创建一个httpclient,此处可以指定最大请求时间
   var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
   req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data) //创建http请求,参数:method,url,data(此处用了流,避免数据过大占用太多内存),因此上面用strings将字符串转换为流
   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)
   }
   fmt.Printf("%s\n", bodyText) //打印出最后的json字符串
}

2.2 生成request body

完成上述代码可以得到一串json数据,但good是固定的,我们需要到用json序列化。首先根据上节基础语法的知识,先构建一个json结构体:

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

然后new一个结构体变量并赋值,并用json序列化请求再将其转化为byte数组

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)

代码如下:

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{}
   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)
   }
   fmt.Printf("%s\n", bodyText)
}

2.3 解析response body 并打印结果

首先打开(oktools.net/json2go),该网站可以实现json到go的结构体转化,将刚刚翻译出dict的响应处代码复制到网站的json框,可以得到一个超大的go语言结构体。

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"`
}

最终将结果打印出来,通过 for range 循环,整体代码如下:

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 {
      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)
}

3、Socks5 代理

3.1 什么是Socks5

SOCKS5 是一个代理服务器 ,SOCKS5 的协议都是明文传输。

这个协议历史比较久远,诞生于互联网早期。在某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦,SOCKS5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻墙软件,最终暴露的也是一个 SOCKS5 协议的端口。

在开发爬虫的时候,在爬取过程中很容易会遇到IP访问频率超过限制,这个时候可以去网上找一些代理 IP 池,这些代理 IP 池里面的很多代理的协议就是 SOCKS5。

本质就是客户端要先和代理服务器建立TCP连接,然后socks代理服务器和需要访问的目标服务器建立TCP连接,对于所有从客户端发来的请求由代理服务器转发给目标服务器,且目标服务器的响应数据也由代理服务器转发给客户端(socks5代理服务器就是以sock5s协议为标准去建立客户端和代理服务器的通信。

3.2 socks5协议交互过程

正常浏览器访问一个网站,如果不经过代理服务器,就是先和对方的网站建立 TCP 连接,然后三次握手,握手完之后发起 HTTP 请求,然后服务返回 HTTP 响应。

如果设置代理服务器之后,流程会变得复杂一些。首先是浏览器和 SOCKS5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。

这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段。

第一个握手阶段,浏览器会向 SOCKS5 代理发送请求,内容包括协议的版本号,还有支持的认证的种类,SOCKS5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的就代表不需要认证,返回其他类型的话会开始认证流程。

第三个阶段是请求阶段,认证通过之后浏览器会 SOCKS5 服务器发起请求。主要信息包括版本号,请求的类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。

第四个阶段是 relay 阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。实际上代理服务器并不关心流量的细节,可以是 HTTP 流量,也可以是其它 TCP 流量。

这个就是 SOCKS5 协议的工作原理。

掘金1.png

3.3 TCP echo server

首先写一个主函数去监听一个端口,再在一个死循环中去接受一个请求,如果成功会返回一个连接,使用go关键字去在process函数中处理这个连接,go可以类比为开启一个子线程。

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)
   }
}

// process函数的实现 首先创建一个流,再在死循环中去每次读一个字节,并写入slice,如果出错就关闭连接
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
      }
   }
}

3.4 auth

接下来要开始实现协议的第一步,认证阶段。

我们实现一个 auth 函数,在 process 函数里面调用。

认证阶段的逻辑为,第一步,浏览器会给代理服务器发送一个包,这个包有三个字段。

第一个字段,version 协议版本号,固定是5。

第二个字段,methods 认证的方法数目。

第三个字段,每个 method 的编码,0代表不需要认证,2代表用户名密码认证。

先用 read bytes 来把版本号读出来,如果版本号不是 socket 5 的话直接返回报错,接下来再读取 method size ,也是一个字节。然后需要去 make 一个相应长度的一个 slice ,用 io.ReadFull 把它去填充进去。现在把获取到的版本号和认证方式打印一下。此时,代理服务器还需要返回一个 response,返回包括 两个字段,一个是 version ,一个是 method。

package main

import (
   "bufio"
   "fmt"
   "io"
   "log"
   "net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const 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 failed:%v", conn.RemoteAddr(), err)
      return
   }
   log.Println("auth success")
}

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
   // X’02’ USERNAME/PASSWORD

   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)
   }
   method := make([]byte, methodSize)
   _, err = io.ReadFull(reader, method)
   if err != nil {
      return fmt.Errorf("read method failed:%w", err)
   }
   log.Println("ver", ver, "method", method)
   // +----+--------+
   // |VER | METHOD |
   // +----+--------+
   // | 1  |   1    |
   // +----+--------+
   _, err = conn.Write([]byte{socks5Ver, 0x00})
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}

3.5 请求阶段

接下来实现请求阶段。

试图读取到携带 URL 或者 IP 地址+端口的包,然后把它打印出来。实现一个和 auth 函数类似的 connect 函数,同样在 process 里面去调用。

请求阶段,浏览器会发送一个包,包里面包含如下6个字段, version 版本号,command 请求的类型,RSV 保留字段,atype 就是目标地址类型,可能是 IPV 4 IPV 6 或者域名,addr 地址,port 端口号,然后需要逐个去读取这些字段。

接下来把这个地址和端口打印出来用于调试,收到浏览器的这个请求包之后,需要返回一个包。

package main

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

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const 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 failed:%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
   // X’02’ USERNAME/PASSWORD

   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)
   }
   method := make([]byte, methodSize)
   _, err = io.ReadFull(reader, method)
   if err != nil {
      return fmt.Errorf("read method failed:%w", err)
   }

   // +----+--------+
   // |VER | METHOD |
   // +----+--------+
   // | 1  |   1    |
   // +----+--------+
   _, err = conn.Write([]byte{socks5Ver, 0x00})
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   // +----+-----+-------+------+----------+----------+
   // |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)
   _, 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)
   }
   addr := ""
   switch atyp {
   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])
   case atypeHOST:
      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")
   }
   _, err = io.ReadFull(reader, buf[:2])
   if err != nil {
      return fmt.Errorf("read port failed:%w", err)
   }
   port := binary.BigEndian.Uint16(buf[:2])

   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})
   if err != nil {
      return fmt.Errorf("write failed: %w", err)
   }
   return nil
}

3.6 relay 阶段

直接用 net.dial 建立一个 TCP 连接,建立完连接之后,要加一个 defer 来关闭连接。

接下来需要建立浏览器和下游服务器的双向数据转发。

标准库的 io.copy 可以实现一个单向数据转发,双向转发需要启动两个 goroutinue。

这里可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个context,在最后等待 ctx.Done() ,只要 cancel 被调用, ctx.Done就会立刻返回,然后在上面的两个 goroutinue 里面 调用一次 cancel 即可。

package main

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

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const 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 failed:%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
   // X’02’ USERNAME/PASSWORD

   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)
   }
   method := make([]byte, methodSize)
   _, err = io.ReadFull(reader, method)
   if err != nil {
      return fmt.Errorf("read method failed:%w", err)
   }

   // +----+--------+
   // |VER | METHOD |
   // +----+--------+
   // | 1  |   1    |
   // +----+--------+
   _, err = conn.Write([]byte{socks5Ver, 0x00})
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   // +----+-----+-------+------+----------+----------+
   // |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)
   _, 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)
   }
   addr := ""
   switch atyp {
   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])
   case atypeHOST:
      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")
   }
   _, err = io.ReadFull(reader, buf[:2])
   if err != nil {
      return fmt.Errorf("read port failed:%w", err)
   }
   port := binary.BigEndian.Uint16(buf[:2])

   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)

   // +----+-----+-------+------+----------+----------+
   // |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})
   if err != nil {
      return fmt.Errorf("write failed: %w", err)
   }
   ctx, cancel := context.WithCancel(context.Background())
   defer cancel()

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

   <-ctx.Done()
   return nil
}