Go语言快速上手-实战案例

案例一——猜数游戏

游戏说明

程序首先会生成个介于1到100之间的随机整数,然后提示玩家进行猜测。玩家每次输入一个数字,程序会告诉玩家这个清测的值是高于还是低于那个秘密的随机数,并且让玩家继续猜测

如果玩家猜对了,告诉玩家取得胜利,游戏结束

游戏构建

随机数生成

v1

package main

import (
  “fmt”
  “math/rand”
)

func main() {
  MaxNum := 100
  //生成随机数
  secretNum := rand.Intn(MaxNum)
  fmt.Println(“随机数为:”, secretNum)
}

运行过后可以发现,每次的结果都是相同的

v2

这是因为rand.Intn函数在使用之前要设置随机数种子,否则每一次都会生成相同的数字

一般的惯例是在使用之前,用启动的时间戳初始化随机数种子

读取用户输入输出

然后接下来我们需要实现用户输入输出,并理解析成数字。

  • bufio读取Stdin文件实现读取输入每个程序执行的时候都会打开几个文件,stdin stdout stderr等,stdin 文件可以用 os.Stdin 来得到。然后直接操作这个文件很不方便,我们会用 bufio.NewReader 把一个文件转换成一个 reader 变量。reader 变量上会有很多用来操作一个流的操作,我们可以用它的 ReadString 方法来读取一行。如果失败了的话,我们会打印错误并能退出。
  • Scanf函数实现读取输入

ReadString 返回的结果包含结尾的换行符,我们把它去掉,再转换成数字。如果转换失败,我们同样打印错误,退出。package main

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

func main() {
  //生成随机数
  MaxNum := 100
  rand.Seed(time.Now().UnixNano())
  secretNum := rand.Intn(MaxNum)
  fmt.Println(“随机数为:”, secretNum)

  //获取输入输出
  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
  }
  //去掉多余的换行符(亲测还有一个\r)
  input = strings.TrimSuffix(input, “\r\n”)

  // 转换为数字
  guess, err := strconv.Atoi(input)
  if err != nil {
    fmt.Println(“输入不合法,请输入一个整数值”, err)
    return
  }
  fmt.Println(“你猜的数字为”, guess)
}

运行结果略

实现判断逻辑

太简单了,直接上代码package main

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

func main() {
  //生成随机数
  MaxNum := 100
  rand.Seed(time.Now().UnixNano())
  secretNum := rand.Intn(MaxNum)
  fmt.Println(“随机数为:”, secretNum)

  //获取输入输出
  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
  }
  //去掉多余的换行符(亲测还有一个\r)
  input = strings.TrimSuffix(input, “\r\n”)

  // 转换为数字
  guess, err := strconv.Atoi(input)
  if err != nil {
    fmt.Println(“输入不合法,请输入一个整数值”, err)
    return
  }
  fmt.Println(“你猜的数字为”, guess)

  // 判断输入值大小
  if guess > secretNum {
    fmt.Println(“Your guess is bigger than secret number.Please try again.”)
  } else if guess < secretNum {
    fmt.Println(“Your guess is smaller than secret number.Please try again.”)
  } else {
    fmt.Println(“Correct!You Legend!”)
  }

}

实现游戏循环

改写for循环,胜利break,出错continuepackage main

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

func main() {
  //生成随机数
  MaxNum := 100
  rand.Seed(time.Now().UnixNano())
  secretNum := rand.Intn(MaxNum)
  //fmt.Println(“随机数为:”, secretNum)

  for {
    //获取输入输出
    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)
        continue
    }
    //去掉多余的换行符(亲测还有一个\r)
    input = strings.TrimSuffix(input, “\r\n”)

    // 转换为数字
    guess, err := strconv.Atoi(input)
    if err != nil {
        fmt.Println(“输入不合法,请输入一个整数值”, err)
        continue
    }
    fmt.Println(“你猜的数字为”, guess)

    // 判断输入值大小
    if guess > secretNum {
        fmt.Println(“Your guess is bigger than secret number.Please try again.”)
    } else if guess < secretNum {
        fmt.Println(“Your guess is smaller than secret number.Please try again.”)
    } else {
        fmt.Println(“Correct!You Legend!”)
        break
    }
  }

}

优化

使用fmt.Scanf简化代码

案例二——在线词典

用户可以在命令行里面查询一个单词。我们能通过调用第三方的API查询到单词的翻译并打印出来。 这个例子里面,我们会学习如何用go语言来来发送HTTP请求、解析json 过来,还会学习如何使用代码生成来提高开发效率。

抓包

彩云小译 – 在线翻译 (caiyunapp.com)

image-20230117222809803

找到Network里包含hello的dict数据包

预览如下:

image-20230117222941640

请求数据:{
  “trans_type”: “en2zh”,
  “source”: “hello”
}

  • trans_type:翻译类型(en英语转zh中文)
  • source:翻译的词

响应数据:{
“prons”: {
  “en-us”: “[həˈlo]”,
  “en”: “[ˈheˈləu]”
},
“explanations”: [
  “int.喂;哈罗”,
  “n.引人注意的呼声”,
  “v.向人呼(喂)”
],
“synonym”: [
  “greetings”,
  “salutations”
],
“antonym”: [],
“wqx_example”: [
  [
    “say hello to”,
    “向某人问候,和某人打招呼”
  ],
  [
    “Say hello to him for me . “,
    “代我问候他。”
  ]
],
“entry”: “hello”,
“type”: “word”,
“related”: [],
“source”: “wenquxing”
}

代码生成

使用Go去发送这个请求时很麻烦的,故采用代码生成的方法

首先右键请求、copy as curl

image-20230117225556644

curl ‘https://api.interpreter.caiyunai.com/v1/dict’ \
-H ‘authority: api.interpreter.caiyunai.com’ \
-H ‘accept: application/json, text/plain, */*’ \
-H ‘accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6’ \
-H ‘app-name: xy’ \
-H ‘content-type: application/json;charset=UTF-8’ \
-H ‘device-id;’ \
-H ‘origin: https://fanyi.caiyunapp.com’ \
-H ‘os-type: web’ \
-H ‘os-version;’ \
-H ‘referer: https://fanyi.caiyunapp.com/’ \
-H ‘sec-ch-ua: “Not_A Brand”;v=”99″, “Microsoft Edge”;v=”109″, “Chromium”;v=”109″‘ \
-H ‘sec-ch-ua-mobile: ?0’ \
-H ‘sec-ch-ua-platform: “Windows”‘ \
-H ‘sec-fetch-dest: empty’ \
-H ‘sec-fetch-mode: cors’ \
-H ‘sec-fetch-site: cross-site’ \
-H ‘user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.55’ \
-H ‘x-authorization: token:qgemv4jr1y38jyq6vhvi’ \
–data-raw ‘{“trans_type”:”en2zh”,”source”:”hello”}’ \
–compressed

打开另一个网址:Convert curl commands to Go (curlconverter.com)

image-20230117230121817

package main

import (
“fmt”
“io/ioutil”
“log”
“net/http”
“strings”
)

func main() {
client := &http.Client{}
//指定参数
var data = strings.NewReader(`{“trans_type”:”en2zh”,”source”:”hello”,”user_id”:”63c6bb0b5834410017188916″}`)
//创建请求
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-GB;q=0.7,en-US;q=0.6”)
req.Header.Set(“app-name”, “xy”)
req.Header.Set(“content-type”, “application/json;charset=UTF-8”)
req.Header.Set(“device-id”, “”)
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”, `”Not_A Brand”;v=”99″, “Microsoft Edge”;v=”109″, “Chromium”;v=”109″`)
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/109.0.0.0 Safari/537.36 Edg/109.0.1518.55”)
req.Header.Set(“x-authorization”, “token:qgemv4jr1y38jyq6vhvi”)
//发送请求
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)
}

我们来看一下这生成的代码,首先第 12 行我们创建了一个 HTTP client,创建的时候可以指定很多参数,包括比如请求的超时是否使用 cookie 等。

接下来是构造一个 HTTP 请求,这是一个 post 请求,然后会用到 HTTP .NewRequest ,第一个参数是 http 方法 POST, 第二个参数是 URL, 最后一个参数是 body ,body因为可能很大,为了支持流式发送,是一个只读流。

我们用了 strings.NewReader 来把字符串转换成一个流。这样我们就成功构造了一个 HTTP request ,接下来我们需要对这个 HTTP request 来设置一堆 header。 接下来我们把我们调用 client.do request ,就能得到 response 如果请求失败的话,那么这个 error 会返回非 nil,会打印错误并且退出进程。response 有它的 HTTP 状态码, response header和body。 body同样是一个流,在golang里面,为了避免资源泄露,你需要加一个 defer 来手动关闭这个流,这个 defer 会在这个函数运行结束之后去执行。接下来我们是用 ioutil.ReadAll 来读取这个流,能得到整个body。我们再用 print 打印出来。

构造Request Body并序列化JSON

  • 定义结构体
  • 创建结构体对象
  • 序列化
  • 利用bytes.NewBuffer(buf)将byte数组转换为字符串

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”:”en2zh”,”source”:”hello”,”user_id”:”63c6bb0b5834410017188916″}`)
//创建结构体对象
request := DictRequest{TransType: “en2zh”, Source: “hello”}
//序列化
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewBuffer(buf) //byte数组转字符串
//创建请求
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-GB;q=0.7,en-US;q=0.6”)
req.Header.Set(“app-name”, “xy”)
req.Header.Set(“content-type”, “application/json;charset=UTF-8”)
req.Header.Set(“device-id”, “”)
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”, `”Not_A Brand”;v=”99″, “Microsoft Edge”;v=”109″, “Chromium”;v=”109″`)
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/109.0.0.0 Safari/537.36 Edg/109.0.1518.55”)
req.Header.Set(“x-authorization”, “token:qgemv4jr1y38jyq6vhvi”)
//发送请求
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

Go中最常用的解析方法,是创建一个结构体,各字段与json中的字段对应,再将请求回来的Response Body反序列化到结构体对象

手动构造结构体容易出错,因此依然使用代码生成来实现

JSON转Golang Struct – 在线工具 – OKTools

image-20230118114517067

点击展开会分成多个结构体

嵌套则会写在一个结构体中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 []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”`
}

防御式编程

虽然成功返回了Response,但它不一定就百分百是我们需要的Response,也有可能是参数错了之类的错误

导致出现403、404…….

出错的时候应该检测到它不是200

应该把返回的状态码和报文信息打印出来方便诊断问题

完整代码

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 []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”:”hello”,”user_id”:”63c6bb0b5834410017188916″}`)
//创建结构体对象
request := DictRequest{TransType: “en2zh”, Source: word}
//序列化
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewBuffer(buf) //byte数组转字符串
//创建请求
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-GB;q=0.7,en-US;q=0.6”)
req.Header.Set(“app-name”, “xy”)
req.Header.Set(“content-type”, “application/json;charset=UTF-8”)
req.Header.Set(“device-id”, “”)
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”, `”Not_A Brand”;v=”99″, “Microsoft Edge”;v=”109″, “Chromium”;v=”109″`)
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/109.0.0.0 Safari/537.36 Edg/109.0.1518.55”)
req.Header.Set(“x-authorization”, “token:qgemv4jr1y38jyq6vhvi”)
//发送请求
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, “\nBody:”, string(bodyText))
}

//fmt.Printf(“%s\n”, bodyText)
//创建响应结构体变量
var dictresponse DictResponse
//json反序列化到结构体变量
err = json.Unmarshal(bodyText, &dictresponse)
if err != nil {
log.Fatal(err)
}
//fmt.Printf(“%#v\n”, dictresponse)
//只显示需要的字段
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.Println(os.Stderr, `usage: simpleDict WORD example:simpleDict hello`)
os.Exit(1)
}
word := os.Args[1]
Query(word)
}

运行:go run .\案例二.在线词典.go abandon

优化

  • 增加另一种翻译引擎的支持(Google Translate、火山翻译…..)
  • 并行请求两个翻译引擎提高响应速度

案例三——SOCKS5代理

此代理服务器并不能用来FQ,协议都是明文传输的

用途是某些企业的内网为了确保安全性,配置了很严格的防火墙

但副作用是,某些管理员为了访问内部资源会很麻烦

socks5相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。

实际上很多翻墙软件,最终暴露的也是一个socks5协议的端口。 如果有同学开发过爬虫的话,就知道,在爬取过程中很容易会遇到P访问频率超过限制。这个时候很多人就会去网上找一些代理IP池,这些代理IP池里面的很多代理的协议就是socks5。

原理

image-20230118141649037

接下来我们来了解一下 socks5协议的工作原理。

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

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

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

  1. 第一个握手阶段,浏览器会向socks5代理发送请求报文,包的内容包括一个协议的版本号,还有支持的认证的种类(密码或不需要认证),socks5 服务器会选中一个认证方式,返回给浏览器。如果返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了。
  2. 第二阶段(若有)即为认证阶段
  3. 第三个阶段是请求阶段,认证通过之后浏览器会socks5服务器发起请求报文。主要信息包括版本号,请求的类型,一般主要是 connection请求,就代表代理服务器要和某个域名或者某个IP地址某个端口建立TCP连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。
  4. 第四个阶段是relay阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上代理服务器并不 关心流量的细节,可以是HTTP流量,也可以是其它TCP流量。

这个就是 socks5协议的工作原理,接下来我们就会试图去简单地实现它。

v1:TCP echo server

一个发送什么,回复什么的服务端package main

import (
“bufio”
“fmt”
“log”
“net”
)

/*
*
TCP Echo Server
*/
func main() {
fmt.Println(“开始监听”)
//侦听端口,返回server
server, err := net.Listen(“tcp”, “127.0.0.1:3456”)
if err != nil {
panic(err)
}
for {
//接受请求,成功返回连接
client, err := server.Accept()
if err != nil {
log.Printf(“Accept Failed %v”, err)
continue
}
//在process函数中处理连接
//通过go关键字启动一个协程(Goroutine)
go process(client)
}
}

func process(conn net.Conn) {
// 关闭连接
defer conn.Close()
//基于当前连接创建一个带缓冲的流
reader := bufio.NewReader(conn)
for {
//循环从数据流中,读一个字节
b, err := reader.ReadByte()
if err != nil {
break
}
//将读入的字节,写入Response返回客户端
_, err = conn.Write([]byte{b})
if err != nil {
break
}

}

}

使用netcat测试一下nc 127.0.0.1 3456

image-20230129143009693

输入内容,回车,会输出一行输入的内容

v2:认证阶段-auth

首先第一步的话,浏览器会给代理服务器发送一个包,然后这个包有三个字段,

  • VER 字段表征 Socks 协议版本, 占 1 字节, 对于 Socks 5 其值固定为 0x05
  • NMETHODS 字段指示其后的 METHOD 字段所占的字节数, 其本身占 1 字节
  • METHODS 字段为可变长字段, 用来指示客户端和代理服务器之间的认证方法, 其长度区间为 [1, 255] 个字节, 即客户端在向代理服务器发起握手时同时声明其所支持的认证方法的列表, 代理服务器会从中选择一个方法作为接下来与客户端进行认证的方法, 所以对于 Socks 5 协议来说, 客户端发起的握手实际上本身也是启动了一个协商过程
  • 第一个字段 Version 也就是 协议版本号 ,固定是 5 (socks5协议固定)
  • 第二个字段 Methods, 认证的方法数目
  • 第三个字段 每个 method的编码, 0代表 不需要认证, 2 代表用户名密码认证
img

最后,代理服务器还需要返回一个response, 返回包包括 两个字段,一个是 version 一个是 method,也就是我们选中的鉴传方式,

img

新增auth函数package main

import (
“bufio”
“fmt”
“io”
“log”
“net”
)

//全局变量
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
fmt.Println(“开始监听”)
//侦听端口,返回server
server, err := net.Listen(“tcp”, “127.0.0.1:3456”)
if err != nil {
panic(err)
}
for {
//接受请求,成功返回连接
client, err := server.Accept()
if err != nil {
log.Printf(“Accept Failed %v”, err)
continue
}
//在process函数中处理连接
//通过go关键字启动一个协程(Goroutine)
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 faild:%v”, conn.RemoteAddr(), err)
return
}
log.Printf(“auth access!”)
}

/**
鉴权函数
*/
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
//第一个字段是协议版本号Version
//第二个字段是鉴权方式的数目Methods
//第三个字段是每个鉴权方式的编码

//读出版本号(1字节)
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf(“read version failed:%v”, err)
}
if ver != socks5Ver {
return fmt.Errorf(“Not surport this version:%v”, err)
}

//读取MethodsSize(1字节)
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf(“read MethodSize failed:%v”, err)
}

//建立缓冲区Slice,存放指定methodSize的数据
method := make([]byte, methodSize)
//填充数据到Slice
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf(“read method failed:%v”)
}
fmt.Println(“ver:”, ver, “method:”, method)

//返回数据包(告诉客户端选择哪种鉴权方式)
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf(“write filed:%v”, err)
}
return nil
}.\curl.exe –socks5 127.0.0.1:3456 -v http://www.qq.com

image-20230122224548723

通过控制台可以看到服务端输出了读取到的Version和method两个字段,通过了并且客户端的认证请求

v3:请求阶段

第三阶段实现请求阶段。

我们试图读取到报文。

携带需要访问的 URL 或者 IP 地址+端口的包, +—-+—–+——-+——+———-+———-+
|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个字节

请求阶段的逻辑:

浏览器会发送一个包,包里面包含如下6个字段,

  • version :版本号, 还是 5。
  • command :代表请求的类型,我们只支持 connection 请求,也就是让代理服务与请求的服务器建立新的TCP连接。
  • RSV :保留字段,一般是0,可有忽略掉。
  • atype :就是目标地址类型,可能是 IPV 4 IPV 6 或者域名 下面是域名, 这个地址的长度是根据 atype 的类型而不同的
    • 1代表IPv4,DST.ADDR即为固定长度为4个字节
    • 3代表域名,DST.ADDR为变长的字符串,第一个字节为长度,后面的n个字节为真正的域名
  • DST.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

第一个是版本号还是 socket 5。

第二个,就是返回的类型,这里是成功就返回0

第三个是保留字段 填 0

第四个 atype 地址类型 填 1

第五个,第六个暂时用不到,都填成 0。

一共 4 + 4 + 2 个字节,后面6个字节都是 0 填充。package main

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

// 全局变量
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

// 带鉴权的socks5 proxy server,获取报文并打印出来
func main() {
fmt.Println(“开始监听”)
//侦听端口,返回server
server, err := net.Listen(“tcp”, “127.0.0.1:3456”)
if err != nil {
panic(err)
}
for {
//接受请求,成功返回连接
client, err := server.Accept()
if err != nil {
log.Printf(“Accept Failed %v”, err)
continue
}
//在process函数中处理连接
//通过go关键字启动一个协程(Goroutine)
go process2(client)
}
}

func process2(conn net.Conn) {
// 关闭连接
defer conn.Close()
//基于当前连接创建一个带缓冲的流
reader := bufio.NewReader(conn)
//进行鉴权
err := auth(reader, conn)
if err != nil {
log.Printf(“Client %v auth faild:%v”, conn.RemoteAddr(), err)
return
}
log.Printf(“auth access!”)
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) {
//第一个字段是协议版本号Version
//第二个字段是鉴权方式的数目Methods
//第三个字段是每个鉴权方式的编码

//读出版本号(1字节)
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf(“read version failed:%v”, err)
}
if ver != socks5Ver {
return fmt.Errorf(“Not surport this version:%v”, err)
}

//读取MethodsSize(1字节)
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf(“read MethodSize failed:%v”, err)
}

//建立缓冲区Slice,存放指定methodSize的数据
method := make([]byte, methodSize)
//填充数据到Slice
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf(“read method failed:%v”)
}
fmt.Println(“ver:”, ver, “method:”, method)

//返回数据包(告诉客户端选择哪种鉴权方式)
//第一个字节是协议版本号,第二个参数是选择的鉴权方式
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf(“write filed:%v”, err)
}
return nil
}

// 获取报文并打印
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
//读取报文
//创建长度为4的缓冲区
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”, ver)
}
addr := “”
//判断atyp类型
switch atyp {
case atypIPV4:
//直接将IP地址,填充入缓冲区
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf(“read atyp failed:%w”, err)
}
//打印IP地址
addr = fmt.Sprintf(“%d.%d.%d.%d”, buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
//读取1字节的长度
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)

_, 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
}

通过命令使用curl指定ipv4,通过-v显示连接过程.\curl.exe –socks5 127.0.0.1:3456 -4 http://www.qq.com -v

image-20230129152650510

控制台可以正常打印出需要访问的Ip和端口,说明实现成功

v4:realy阶段

通过此阶段,代理服务器就算成功了。

先与真正的服务器建立TCP连接//建立TCP连接
dest, err := net.Dial(“tcp”fmt.Sprintf(“%v:%v”,addr,port))
if err != nil{
return fmt.Errorf(“dial dst failed:%v”,err)
}
//没有出错关闭连接
defer dest.Close()

再建立浏览器与下游服务器的双向数据转发

在标准库中找到io.Copy函数,实现func Copy(dst Writer, src Reader) (written int64, err error)

从src中读取数据并写入dst

要实现双向数据转发,需要启动两个go routine分别调用Copy//实现数据双向转发
//使用context机制
ctx, cancel := context.WithCancel(context.Background())
defer cancel() //防御式编程
go func() {
//从Client浏览器拷贝到服务器
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
//从服务器拷贝数据到Client浏览器
_, _ = io.Copy(conn, dest)
cancel()
}()
//等待context执行完成(即cancel被调用),实现任意一方Copy失败,关闭双方连接,关闭数据
<-ctx.Done()

浅浅访问一下百度.\curl.exe –socks5 127.0.0.1:3456 -4 http://www.baidu.com -v

image-20230129165029722

完整代码package main

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

// 全局变量
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

// 带鉴权的socks5 proxy server
func main() {
fmt.Println(“开始监听”)
//侦听端口,返回server
server, err := net.Listen(“tcp”, “127.0.0.1:3456”)
if err != nil {
panic(err)
}
for {
//接受请求,成功返回连接
client, err := server.Accept()
if err != nil {
log.Printf(“Accept Failed %v”, err)
continue
}
//在process函数中处理连接
//通过go关键字启动一个协程(Goroutine)
go process2(client)
}
}

func process2(conn net.Conn) {
// 关闭连接
defer conn.Close()
//基于当前连接创建一个带缓冲的流
reader := bufio.NewReader(conn)
//进行鉴权
err := auth(reader, conn)
if err != nil {
log.Printf(“Client %v auth faild:%v”, conn.RemoteAddr(), err)
return
}
log.Printf(“auth access!”)
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) {
//第一个字段是协议版本号Version
//第二个字段是鉴权方式的数目Methods
//第三个字段是每个鉴权方式的编码

//读出版本号(1字节)
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf(“read version failed:%v”, err)
}
if ver != socks5Ver {
return fmt.Errorf(“Not surport this version:%v”, err)
}

//读取MethodsSize(1字节)
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf(“read MethodSize failed:%v”, err)
}

//建立缓冲区Slice,存放指定methodSize的数据
method := make([]byte, methodSize)
//填充数据到Slice
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf(“read method failed:%v”)
}
fmt.Println(“ver:”, ver, “method:”, method)

//返回数据包(告诉客户端选择哪种鉴权方式)
//第一个字节是协议版本号,第二个参数是选择的鉴权方式
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf(“write filed:%v”, err)
}
return nil
}

// 获取报文并打印
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
//读取报文
//创建长度为4的缓冲区
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”, ver)
}
addr := “”
//判断atyp类型
switch atyp {
case atypIPV4:
//直接将IP地址,填充入缓冲区
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf(“read atyp failed:%w”, err)
}
//打印IP地址
addr = fmt.Sprintf(“%d.%d.%d.%d”, buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
//读取1字节的长度
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])

//建立TCP连接
dest, err := net.Dial(“tcp”, fmt.Sprintf(“%v:%v”, addr, port))
if err != nil {
return fmt.Errorf(“dial dst failed:%v”, err)
}
//没有出错关闭连接
defer dest.Close()
log.Println(“dial”, addr, 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)
}
//实现数据双向转发
//使用context机制
ctx, cancel := context.WithCancel(context.Background())
defer cancel() //防御式编程
go func() {
//从Client浏览器拷贝到服务器
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
//从服务器拷贝数据到Client浏览器
_, _ = io.Copy(conn, dest)
cancel()
}()
//等待context执行完成(即cancel被调用),实现任意一方Copy失败,关闭双方连接,关闭数据
<-ctx.Done()

return nil
}

同时也可以使用浏览器插件SwitchyOmega

个人认为…那是欧米伽Ω,不是哦买噶(lll¬ω¬)

浏览器打开新的页面都会经过代理服务器

服务器控制台就会输出访问的域名+端口

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇