当前位置: 首页 > news >正文

【字节跳动青训营】后端笔记整理-2 | Go实践记录:猜谜游戏,在线词典,Socks5代理服务器

**本人是第六届字节跳动青训营(后端组)的成员。本文由博主本人整理自该营的日常学习实践,首发于稀土掘金:🔗Go实践记录:猜谜游戏,在线词典,Socks5代理服务器 | 青训营

我的go开发环境:

*本地IDE:GoLand 2023.1.2

*go:1.20.6

一、猜谜游戏

猜数字游戏也算是入门一门编程语言必写的程序了。通过这个程序,我们可以熟悉Go语言中的输入输出、流程控制与随机函数的调用。

1、生成随机数

在Go语言中,标准库math/rand下的一系列方法可以用来生成随机数。

math/rand库的官方文档:https://pkg.go.dev/math/rand

通过调用库中的rand.Intn(n)函数,可以生成一个 [0,n) 的随机整数:

//Go 1.20
package mainimport ("fmt""math/rand"
)func main() {maxNum := 100for i := 0; i < 10; i++ {    //生成10个随机整数secretNum := rand.Intn(maxNum)        //每次生成的随机数在 [0,100)fmt.Println("随机数是", secretNum)}
}

注意:生成的这10个随机整数是否相同呢?

在我所使用的 Go 1.20.6 下,生成的10个随机数是各不相同的。也就是说,从 Go 1.20 开始,rand.Intn()生成的是真随机数,不需要设定Seed()。

依照官方的说法,math/rand 已弃用 rand.Seed(…) 全局函数,现在自动为全局随机数生成器Int生成一个随机值,并且顶级Seed函数已被弃用。因此,每次运行程序时会生成不同的随机数。

但如果在 Go 1.20 版本之前,如培训中王克纯老师演示的(Go 1.18),在调用rand.Intn()之前必须先通过 rand.Seed()设置随机数种子,这样才能让每次运行生成的随机数值不同,否则,由于随机数种子固定,每次运行生成的随机数也是固定的一个值,并不“随机”。

通常,用时间戳来作为随机数的种子。(这一点特性和C语言的rand()是类似的。)

//Go 1.20 之前package mainimport ("fmt""math/rand""time"
)func main() {maxNum := 100rand.Seed(time.Now().UnixNano())secretNumber := rand.Intn(maxNum)fmt.Println("The secret number is ", secretNumber)
}

2、读取用户输入

这里直接用 fmt.Scan() 来读取用户的输入。

fmt.Scan() 函数用于从标准输入中读取数据,并根据提供的格式字符串将输入解析为相应的变量。这个函数会一直等待用户输入,直到按下回车键,并尝试将输入解析为指定的变量类型:

package mainimport ("fmt"
)func main() {var num intfmt.Print("请输入一个整数: ")_, err := fmt.Scan(&num)if err != nil {fmt.Println("输入错误:", err)return}fmt.Println("您输入的整数是:", num)
}

在这个示例中,fmt.Scan(&num) 会等待用户输入,并将输入解析为整数,并将其存储到 num 变量中。如果输入无法解析为整数,将会返回错误。

注意,fmt.Scan() 函数需要提供变量的地址作为参数,以便将解析后的值存储到变量中。也可以在 fmt.Scan() 中使用格式字符串来匹配特定的输入格式,以及处理多个变量的读取。

通过 fmt.Scan() 完善猜数字的代码:

maxNum := 100
secretNum := rand.Intn(maxNum)
fmt.Println("随机数是", secretNum)fmt.Println("请输入你猜测的数字:>")
var guess int
_, err := fmt.Scan(&guess)
if err != nil {fmt.Println("输入出错!", err)return
}

3、实现逻辑判断与循环游戏

加上 if-else 对 输入的 guess 与 随机数 secretNum 进行值的校验,来得出用户是否猜中数字;加上循环,实现多次游戏。这样,猜数字游戏的程序就完成了。

package mainimport ("fmt""math/rand"
)func main() {maxNum := 100secretNum := rand.Intn(maxNum)//fmt.Println("随机数是", secretNum)for {fmt.Println("请输入你猜测的数字:>")var guess int_, err := fmt.Scan(&guess)if err != nil {fmt.Println("输入出错!", err)continue    //输入出错不能直接退出程序,而是进入下一次输入}if guess > secretNum {fmt.Println("猜大了!")} else if guess < secretNum {fmt.Println("猜小了!")} else {fmt.Println("恭喜你!猜中了!")break}}
}


二、在线词典

第二个案例是一个命令行词典。效果是输入一个单词,命令行中会显示这个单词的发音和注释。

原理是调用第三方的API去查询结果,并且其打印出来。

实现这个程序的关键在于,如何用Go语言发送HTTP请求和解析JSON。

1、抓包

以彩云小译为例:https://fanyi.caiyunapp.com/#/

分析这个请求:

2、代码自动生成

我们要实现在Golang里发送这个请求。

但这个请求很复杂,直接用代码来构造很麻烦,所以我们可以用另一种简单的方式来生成请求。

右键浏览器中的请求:

点击 Copy as cURL(bash) 后,打开这个网址:https://curlconverter.com/go/

把刚才复制的东西粘贴进去,自动会生成响应的请求代码:

将这些代码复制到Goland:

package mainimport ("fmt""io""log""net/http""strings"
)func main() {client := &http.Client{}var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello"}`)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")req.Header.Set("app-name", "xy")req.Header.Set("cache-control", "no-cache")req.Header.Set("content-type", "application/json;charset=UTF-8")req.Header.Set("device-id", "b72e9f6cbb97432941b8adf317a17dee")req.Header.Set("origin", "https://fanyi.caiyunapp.com")req.Header.Set("os-type", "web")req.Header.Set("os-version", "")req.Header.Set("pragma", "no-cache")req.Header.Set("referer", "https://fanyi.caiyunapp.com/")req.Header.Set("sec-ch-ua", `"Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"`)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/115.0.0.0 Safari/537.36")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)}fmt.Printf("%s\n", bodyText)
}

代码解读:

这里特别解释一下上图中第40行的 defer 关键字。

在 Go 语言中,defer 是一个用于延迟函数调用执行的关键字。通过使用 defer,可以在函数返回之前(无论这个函数是正常结束还是发生了异常),推迟某个函数的执行。这在编写代码时可以很有用,特别是在需要确保资源释放或清理操作的情况下。

defer 语句的语法是:

defer functionCall(arguments)

当在一个函数内部使用 defer 时,被推迟执行的函数调用会被添加到一个栈中,而不会立即执行。在函数执行完毕并即将返回之前,栈中的函数调用会按照逆序执行。因此,如果有多个 defer 语句,它们会按照后进先出(LIFO)的顺序执行,即最后一个推迟的函数调用会最先执行,而最早的推迟的函数调用会最后执行。

以下示例演示了有三个 defer 的情况:

package mainimport "fmt"func main() {defer fmt.Println("第一个defer")defer fmt.Println("第二个defer")defer fmt.Println("第三个defer")fmt.Println("正常的函数调用")
}

在这个示例中,尽管 fmt.Println("正常的函数调用") 在代码中位于三个 defer 语句之前,但它会最先执行。然后,defer 语句会按照后进先出的顺序执行,所以先执行第三个 defer,然后是第二个 defer,最后是第一个 defer。这就是 defer 的执行顺序。

defer 的设计是为了在函数返回前执行一些清理工作,或者确保一些资源被正确释放。通过使用 defer,我们可以更方便地管理代码,并确保清理操作不会被遗漏。

运行自动生成得到的代码块,成功输出一大串JSON:

说明请求成功。

但是,这片请求代码是固定的,传入的data也是固定的 data = strings.NewReader(`{"trans_type":"en2zh","source":"hello"}`)

我们肯定希望用户能通过一个变量进行输入,想翻译什么单词,就翻译什么单词,而不是固定的只能输入JSON字符串。因此,我们要用到JSON序列化。

3、生成request body

如何进行JSON序列化?在上一篇文章中提到过:https://juejin.cn/post/7265577455208955938#heading-62

只需要构造一个结构体,让这个结构体的字段名称和JSON的结构一一对应,然后直接调用 json.Marshal() 即可。

因此这里,我们也需要构造出一个结构体,然后将结构体序列化,通过这样的方式,让data中的值是可变的:

package mainimport ("bytes""encoding/json""fmt""io""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: "hello"}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")req.Header.Set("app-name", "xy")req.Header.Set("cache-control", "no-cache")req.Header.Set("content-type", "application/json;charset=UTF-8")req.Header.Set("device-id", "b72e9f6cbb97432941b8adf317a17dee")req.Header.Set("origin", "https://fanyi.caiyunapp.com")req.Header.Set("os-type", "web")req.Header.Set("os-version", "")req.Header.Set("pragma", "no-cache")req.Header.Set("referer", "https://fanyi.caiyunapp.com/")req.Header.Set("sec-ch-ua", `"Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"`)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/115.0.0.0 Safari/537.36")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)}fmt.Printf("%s\n", bodyText)
}

上述代码中,手动将要翻译的类型(“en2zh”)和单词(“hello”)传入了结构体。

此时代码的运行结果应与一开始的代码运行结果完全相同。对请求的序列化的逻辑就完成了。

4、解析 response body

完成了请求的序列化后,我们还要进行响应的反序列化,这样才能得到结果。这我们要解析出这一堆响应,并且获取到其中的一些关键信息如"explanations"等。

如果是python或者js,返回的会是一个字典的结构,可以直接通过 [] 或 . 去取值。但是Go是一种强类型的语言,这种方式并不是最佳实践(虽然也可以做到)。

更常见的方式是和处理request body一样,写一个结构体,这个结构体的字段和返回的response是一一对应的。再把返回的 json 序列化 (json.Unmarshal())到结构体里面。

但是,我们看到,浏览器里返回的这个response结构非常复杂,手动一一创建结构体去对应显然不是好的做法。所以我们还是用代码生成的方式来实现。

打开这个工具网站:https://oktools.net/json2go

然后,把彩云小译界面的 response 的json粘贴进去:

点击“转换-嵌套”,就会生成一个结构体。(如果点击“转换-展开”,会生成独立的多个结构体。)

将生成的结构体代码粘贴到Goland中,将结构体改名为 DictResponse,然后修改最后处理响应的部分代码:

package mainimport ("bytes""encoding/json""fmt""io""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 {} `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 main() {client := &http.Client{}request := DictRequest{TransType: "en2zh", Source: "hello"}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")req.Header.Set("app-name", "xy")req.Header.Set("cache-control", "no-cache")req.Header.Set("content-type", "application/json;charset=UTF-8")req.Header.Set("device-id", "b72e9f6cbb97432941b8adf317a17dee")req.Header.Set("origin", "https://fanyi.caiyunapp.com")req.Header.Set("os-type", "web")req.Header.Set("os-version", "")req.Header.Set("pragma", "no-cache")req.Header.Set("referer", "https://fanyi.caiyunapp.com/")req.Header.Set("sec-ch-ua", `"Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"`)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/115.0.0.0 Safari/537.36")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)}//处理响应var dictResponse DictResponseerr = json.Unmarshal(bodyText, &dictResponse)if err != nil {log.Fatal(err)}//用 %#v 会以最详细的方式来打印结构体,包括结构体的名字和字段的名字fmt.Println("%#v\n", dictResponse)
}

我们需要从response中筛选出我们需要的信息:如“音标”,“解释”:

//fmt.Println("%#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)
}

最终经过调整,完整的程序代码如下:

package mainimport ("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      []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() {//检查命令行传入的参数个数(第一个参数是程序名称本身,第二个参数是传入的单词,因此必须是两个参数)if len(os.Args) != 2 {fmt.Fprintf(os.Stderr, `usage: simpleDict WORD example: simpleDict hello`)os.Exit(1)}//将第二个命令行参数也就是单词传给变量 wordword := os.Args[1]query(word)
}// 将程序的主要代码封装为query()函数,传入要翻译的单词
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("authority", "api.interpreter.caiyunai.com")req.Header.Set("accept", "application/json, text/plain, */*")req.Header.Set("accept-language", "zh-CN,zh;q=0.9")req.Header.Set("app-name", "xy")req.Header.Set("cache-control", "no-cache")req.Header.Set("content-type", "application/json;charset=UTF-8")req.Header.Set("device-id", "b72e9f6cbb97432941b8adf317a17dee")req.Header.Set("origin", "https://fanyi.caiyunapp.com")req.Header.Set("os-type", "web")req.Header.Set("os-version", "")req.Header.Set("pragma", "no-cache")req.Header.Set("referer", "https://fanyi.caiyunapp.com/")req.Header.Set("sec-ch-ua", `"Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"`)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/115.0.0.0 Safari/537.36")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 DictResponseerr = json.Unmarshal(bodyText, &dictResponse)if err != nil {log.Fatal(err)}//fmt.Println("%#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)}
}

在命令行运行:输入要翻译的单词,可以调用接口实现在线翻译

三、SOCKS5代理服务器

对于大家来说,一提到代理服务器,第一想到的是翻墙。不过遗憾的是, socks5 协议它虽然是代理协议,但它并不能用来翻墙,它的协议都是明文传输。这个协议历史比较久远,诞生于互联网早期。

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

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

**socks5协议的原理

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

如果设置代理服务器之后,流程会变得复杂一些:

  • 首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段:

    • 握手阶段
    • 认证阶段
    • 请求阶段
    • relay 阶段
  • 第一个握手阶段,浏览器会向 socks5 代理发送请求,包的内容包括一个协议的版本号和支持的认证的种类。

  • socks5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程。

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

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

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

1、TCP echo server

在实现代理服务器之前,我们先实现一个简单的回显服务器,以测试我们的server写的对不对。

当运行时,此代码将创建一个简单的 TCP 服务器,监听在 127.0.0.1 的 1080 端口上,接受客户端连接并将客户端发送的数据原样返回。以下是逐句加注释的代码解释:

package mainimport ("bufio""log""net"
)func main() {// 在本地的 127.0.0.1:1080 地址上创建一个 TCP 服务器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}// 启动一个新的 goroutine 处理客户端连接go process(client)}
}func process(conn net.Conn) {// 在函数退出时关闭客户端连接defer conn.Close()// 创建一个用于读取客户端数据的 bufio.Readerreader := bufio.NewReader(conn)for {// 从客户端读取一个字节b, err := reader.ReadByte()if err != nil {break}// 将读取的字节原样发送回客户端_, err = conn.Write([]byte{b})if err != nil {break}}
}

这是一个基本的 TCP 服务器,用于接受客户端连接并将接收到的数据返回给客户端。每当客户端发送一个字节,服务器会将相同的字节返回。

我们通过nc命令来进行测试。首先需要下载netcat,地址及教程:https://blog.csdn.net/BoomLee/article/details/102563472

安装配置完毕后,Goland中启动echo-server程序(直接启动或用命令go run echo-server.go)。启动后,另开一个cmd窗口,在其中输入nc命令:nc 127.0.0.1 1080

输入什么就显示什么,一个简单的回显服务器就搞定了。

2、认证阶段:auth

package mainimport ("bufio""fmt""io""log""net"
)const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04func main() {server, err := net.Listen("tcp", "127.0.0.1:1080") // 在 127.0.0.1 的 1080 端口上创建 TCP 服务器if err != nil {panic(err)}for {client, err := server.Accept() // 接受客户端连接if err != nil {log.Printf("Accept failed %v", err)continue}go process(client) // 启动一个新的 goroutine 处理客户端连接}
}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")
}// +----+----------+----------+
// |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
func auth(reader *bufio.Reader, conn net.Conn) (err error) {// 解析客户端发送的认证请求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
}

这段代码演示了一个简单的 SOCKS5 服务器,用于处理客户端的认证请求,并向客户端发送响应。现在我们用 curl 命令进行一下测试。首先还是一样,在Goland运行项目程序。然后在另一个终端执行curl命令:

curl --socks5 127.0.0.1:1080 -v http://www.qq.com

此时curl 命令肯定是不成功的,因为协议还没实现完成。但是看日志会发现, version和method 可以正常打印,说明当前我们的实现是正确的。

3、请求阶段

接下来我们开始做第三步:请求阶段。

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

再实现 connect 函数的代码。根据请求阶段的逻辑,浏览器会发送一个包,包里面包含如下6个字段:

  1. VER 版本号,socks5的值为0x05

  2. CMD 0x01表示CONNECT请求

  3. RSV 保留字段,值为0x00

  4. ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。

    1. 0x01表示IPv4地址,DST.ADDR为4个字节
    2. 0x03表示域名,DST.ADDR是一个可变长度的域名
  5. DST.ADDR 一个可变长度的值

  6. DST.PORT 目标端口,固定2个字节

接下来我们要挨个去把这6个字段读出来。

面这四个字段总共四个字节,我们可以一次性把它读出来。创建一个长度为4的缓冲区,然后用io.ReadFull()把它整个填充满。

//connect()buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {return fmt.Errorf("read header failed:%w", err)
}

这样就能一次性读取到前面4个字段,它们是定长的。对于每个字段,都要验证合法性:

//connect()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")
}

最后还有两个字节是 port ,我们读取它,然后按协议规定的大端字节序转换成数字。

由于上面的 buffer 已经不会被其他变量使用了,我们可以直接复用之前的内存,建立一个临时的 slice ,长度是2,用于读取,这样的话最多会只读两个字节回来。 接下来我们把这个地址和端口打印出来用于调试。

//connect()_, 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)

收到浏览器的这个请求包之后,我们需要返回一个包,这个包有很多字段,但其实大部分都不会使用:

运行测试:虽然还没有完全成功,但是能够打印出IP地址和端口号了,说明实验还是成功的。

4、relay阶段

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

接下来需要建立浏览器和下游服务器的双向数据转发。标准库的 io.Copy 可以实现一个单向数据转发。完成双向转发还需启动两个 goroutinue。

完整代码如下

package mainimport ("bufio""context""encoding/binary""errors""fmt""io""log""net"
)const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04func 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/PASSWORDver, 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
}

到此,socks5代理服务器就实现完成了。

也可以在浏览器中进行测试。Chrome浏览器只需安装SwitchyOmega插件:

可以在浏览器里面再测试一下,在插件中新建一个情景模式, 代理服务器选 socks5,端口 1080 ,保存并启用。此时你正常地访问其它网站,代理服务器这边会显示出浏览器版本的域名和端口。

至此,Go语言入门的三个实战项目就完成了。

相关文章:

【字节跳动青训营】后端笔记整理-2 | Go实践记录:猜谜游戏,在线词典,Socks5代理服务器

**本人是第六届字节跳动青训营&#xff08;后端组&#xff09;的成员。本文由博主本人整理自该营的日常学习实践&#xff0c;首发于稀土掘金&#xff1a;&#x1f517;Go实践记录&#xff1a;猜谜游戏&#xff0c;在线词典&#xff0c;Socks5代理服务器 | 青训营 我的go开发环境…...

GPT的第一个创作

嗨&#xff0c;大家好&#xff0c;我是赖兴泳&#xff01;今天&#xff0c;我要和大家聊一聊前端开发&#xff0c;就像我用音符创造音乐一样&#xff0c;前端开发也是创造美丽的用户界面的过程。 前端开发是构建网站和应用程序用户界面的关键部分。就像音乐家需要精心编排音符…...

Spring Boot 获取前端参数

Spring Boot 获取前端参数 在开发 Web 应用程序时&#xff0c;前端参数是非常重要的。Spring Boot 提供了多种方法来获取前端参数&#xff0c;本文将介绍其中的一些常用方法。 1. 使用 RequestParam 注解 RequestParam 注解是 Spring MVC 提供的一种常用方式&#xff0c;用于…...

java应用运行在docker,并且其他组件也在docker

docker启动redis容器 # create redis docker run -d --name redis-container -p 6379:6379 redis:latest创建java 应用 dockerfile FROM openjdk:17##Pre-create related directories RUN mkdir -p /data/etax/ms-app WORKDIR /data/etax/ms-appEXPOSE 10133 COPY ./target…...

Java真实面试题,offer已到手

关于学习 在黑马程序员刚刚开始的时候学习尽头非常足&#xff0c;到后面逐渐失去了一些兴趣&#xff0c;以至于后面上课会出现走神等问题&#xff0c;但是毕业时后悔晚矣。等到开始学习项目一的时候&#xff0c;思路总会比别人慢一些&#xff0c;不看讲义写不出来代码。 建议…...

在序列化、反序列化下如何保持单例(Singleton)模式

1、序列化、反序列化 在 Java 中&#xff0c;当一个对象被序列化后再被反序列化&#xff0c;通常情况下会创建一个新的对象实例。这是因为序列化将对象的状态保存到字节流中&#xff0c;而反序列化则是将字节流重新转化为对象。在这个过程中&#xff0c;通常会使用类的构造函数…...

【数据结构】二叉树篇|超清晰图解和详解:二叉树的最近公共祖先

博主简介&#xff1a;努力学习的22级计算机科学与技术本科生一枚&#x1f338;博主主页&#xff1a; 是瑶瑶子啦每日一言&#x1f33c;: 你不能要求一片海洋&#xff0c;没有风暴&#xff0c;那不是海洋&#xff0c;是泥塘——毕淑敏 目录 一、题目二、题解三、代码 一、题目 …...

android ndk clang交叉编译ffmpeg动态库踩坑

1.ffmpeg默认使用gcc编译&#xff0c;在android上无法使用&#xff0c;否则各种报错&#xff0c;所以要用ndk的clang编译 2.下载ffmpeg源码 修改configure文件&#xff0c;增加命令 cross_prefix_clang 修改以下命令 cc_default"${cross_prefix}${cc_default}" cxx…...

简单记录牛客top101算法题(初级题C语言实现)BM24 二叉树的中序遍历 BM28 二叉树的最大深度 BM29 二叉树中和为某一值的路径

1. BM24 二叉树的中序/后续遍历 要求&#xff1a;给定一个二叉树的根节点root&#xff0c;返回它的中序遍历结果。                          输入&#xff1a;{1,2,#,#,3} 返回值&#xff1a;[2,3,1]1.1 自己的整体思路&#xff08;与二叉树的前序遍…...

前后端分离------后端创建笔记(05)用户列表查询接口(上)

本文章转载于【SpringBootVue】全网最简单但实用的前后端分离项目实战笔记 - 前端_大菜007的博客-CSDN博客 仅用于学习和讨论&#xff0c;如有侵权请联系 源码&#xff1a;https://gitee.com/green_vegetables/x-admin-project.git 素材&#xff1a;https://pan.baidu.com/s/…...

性能测试|App性能测试需要关注的指标

一、Android客户端性能测试常见指标&#xff1a; 1、内存 2、CPU 3、流量 4、电量 5、启动速度 6、滑动速度、界面切换速度 7、与服务器交互的网络速度 二、预期标准指定原则 1、分析竞争对手的产品&#xff0c;所有指标要强于竞品 2、产品经理给出的预期性能指标数据…...

Termux SFTP 进行远程文件传输

文章目录 1. 安装openSSH2. 安装cpolar3. 远程SFTP连接配置4. 远程SFTP访问4. 配置固定远程连接地址 SFTP&#xff08;SSH File Transfer Protocol&#xff09;是一种基于SSH&#xff08;Secure Shell&#xff09;安全协议的文件传输协议。与FTP协议相比&#xff0c;SFTP使用了…...

Sqlite3简介

SQLite3 简介 SQLite3 是一种轻量级的嵌入式数据库引擎&#xff0c;被广泛应用于各种应用程序中&#xff0c;包括移动设备、桌面应用程序和嵌入式系统。它以其简单、高效和零配置的特点而受到开发者的喜爱。 以下是 SQLite3 的一些重要特点&#xff1a; 嵌入式数据库引擎&…...

K8S调度

K8S调度 一、List-Watch 机制 controller-manager、scheduler、kubelet 通过 List-Watch 机制监听 apiserver 发出的事件&#xff0c;apiserver 通过 List-Watch 机制监听 etcd 发出的事件1.scheduler 的调度策略 预选策略/预算策略&#xff1a;通过调度算法过滤掉不满足条件…...

vue+element多层表单校验prop和rules

核心点&#xff1a;外层循环是item和index&#xff0c;内层循环是item2和index2 如果都是定义的同一个属性名 外层循环得写:prop"block.index.numerical" 同理内层循环就得写:prop"objectSpecs. index2 .numerical" 校验函数方法 :rules"getRules(it…...

Dubbo 核心概念和架构

以上是 Dubbo 的工作原理图&#xff0c;从抽象架构上分为两层&#xff1a;服务治理抽象控制面 和 Dubbo 数据面 。 服务治理控制面。服务治理控制面不是特指如注册中心类的单个具体组件&#xff0c;而是对 Dubbo 治理体系的抽象表达。控制面包含协调服务发现的注册中心、流量管…...

【数据结构OJ题】反转链表

原题链接&#xff1a;https://leetcode.cn/problems/reverse-linked-list/description/ 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 方法一&#xff1a;三指针翻转法 使用三个结构体指针n1&#xff0c;n2&#xff0c;n3&#xff0c;原地修改结点…...

Java8 Stream 之groupingBy 分组讲解

本文主要讲解&#xff1a;Java 8 Stream之Collectors.groupingBy()分组示例 Collectors.groupingBy() 分组之常见用法 功能代码: /** * 使用java8 stream groupingBy操作,按城市分组list */ public void groupingByCity() { Map<String, List<Em…...

优哲SSD大文件写性能测试

SDD磁盘性能测试&#xff1a; 空盘&#xff1a; 大文件读&#xff0c;写&#xff0c;读写&#xff08;4/6&#xff09;性能测试&#xff0c;删除性能测试&#xff0c;N进程&#xff0c;N线程 小文件读&#xff0c;写&#xff0c;读写&#xff08;4/6&#xff09;性能测试&am…...

Python基础教程: json序列化详细用法介绍

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 Python内置的json模块提供了非常完善的对象到JSON格式的转换。 废话不多说&#xff0c;我们先看看如何把Python对象变成一个JSON&#xff1a; d dict(nameKaven, age17, sexMale) print(json.dumps(d)) # {"na…...

Vue记事本应用实现教程

文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展&#xff1a;显示创建时间8. 功能扩展&#xff1a;记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

UE5 学习系列(三)创建和移动物体

这篇博客是该系列的第三篇&#xff0c;是在之前两篇博客的基础上展开&#xff0c;主要介绍如何在操作界面中创建和拖动物体&#xff0c;这篇博客跟随的视频链接如下&#xff1a; B 站视频&#xff1a;s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

2024年赣州旅游投资集团社会招聘笔试真

2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...

Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器

第一章 引言&#xff1a;语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域&#xff0c;文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量&#xff0c;支撑着搜索引擎、推荐系统、…...

Psychopy音频的使用

Psychopy音频的使用 本文主要解决以下问题&#xff1a; 指定音频引擎与设备&#xff1b;播放音频文件 本文所使用的环境&#xff1a; Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

【决胜公务员考试】求职OMG——见面课测验1

2025最新版&#xff01;&#xff01;&#xff01;6.8截至答题&#xff0c;大家注意呀&#xff01; 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:&#xff08; B &#xff09; A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

12.找到字符串中所有字母异位词

&#x1f9e0; 题目解析 题目描述&#xff1a; 给定两个字符串 s 和 p&#xff0c;找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义&#xff1a; 若两个字符串包含的字符种类和出现次数完全相同&#xff0c;顺序无所谓&#xff0c;则互为…...

Netty从入门到进阶(二)

二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架&#xff0c;用于…...

无人机侦测与反制技术的进展与应用

国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机&#xff08;无人驾驶飞行器&#xff0c;UAV&#xff09;技术的快速发展&#xff0c;其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统&#xff0c;无人机的“黑飞”&…...

打手机检测算法AI智能分析网关V4守护公共/工业/医疗等多场景安全应用

一、方案背景​ 在现代生产与生活场景中&#xff0c;如工厂高危作业区、医院手术室、公共场景等&#xff0c;人员违规打手机的行为潜藏着巨大风险。传统依靠人工巡查的监管方式&#xff0c;存在效率低、覆盖面不足、判断主观性强等问题&#xff0c;难以满足对人员打手机行为精…...