猜谜游戏在编程语言实践都已经和 HelloWord 程序成为必不可少的新手实践环节,毕竟,它能够让我们基本熟悉 for 循环、变量定义、打印、if else 语句等等的使用,当我们基本熟悉该语言基础之后,就要学会其优势方面的程序实践,比如 Golang 所具备的爬虫及其并发优势。我们将采用彩云词典的英文单词翻译成中文的在线词典爬虫程序,及其改进版本,在并发上,我们将采用 SOCKS5 代理服务器的方式体验 Golang 语言的高并发易用性。
欢迎关注我的字节后端青训营代码仓库,更新每日课后作业及其改进代码,除此之外,还会每周发布对应笔记,欢迎一起 star 或者 contribute 代码仓库。
思路:
官方版
package main import ( "bufio" "fmt" "math/rand" "os" "strconv" "strings" "time" ) // 官方版本 func main() { maxNum := 100 // 定义随机种子为当前时间,如果没有设定随机种子,生成数一致 rand.Seed(time.Now().UnixNano()) // 设置随机数最高值n,最小值默认从零开始,即生成一个值在区间 [0, n) 的 Int 数 secretNumber := rand.Intn(maxNum) fmt.Println("Please input your guess") // 读取文本 reader := bufio.NewReader(os.Stdin) // 输入判断,猜数正确退出循环 for { input, err := reader.ReadString('\n') // nil 即为 golang 的空值 if err != nil { fmt.Println("An error occured while reading input. Please try again", err) // continue 返回循环开始处 continue } // windows 需要修改换行符 input = strings.Trim(input, "\r\n") // 利用 string 方法转化为数字 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 结束循环 break } } }
简易版:
package main import ( "fmt" "math/rand" "time" ) func main() { maxNum := 100 rand.Seed(time.Now().UnixNano()) // 设置随机数种子 secretNumber := rand.Intn(maxNum) fmt.Println("Please input your guess") for { // 采用 fmt.Scanf 则无需额外处理文本 var guess int _, err := fmt.Scanf("%d\n", &guess) 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 } } }
在 https://fanyi.caiyunapp.com/ 进行抓包,即网站加载结束后,在输入英文前打开浏览器自带的开发者工具,进行网络录制(network),输入英文,出现如下网络活动:
通过筛选,选择 dict 获取以下响应数据:
我们可以看出,发送数据中,source 为需要翻译的词,trans_type 为翻译类型,此处为英语翻译成汉语。响应数据中,entry 参数为需要翻译的词 test ,explanations 为翻译结果。为了方便爬取,采用代码生成的方法进行获取 go 参数。
复制为 cURL(bash),注意 edge 浏览器选择复制成 bash 格式,而不是 cmd 格式,否则,代码生成会发生错误。
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: f1de93819e3bb9f68a199a51c6ee2efb' \ -H 'origin: https://fanyi.caiyunapp.com' \ -H 'os-type: web' \ -H 'os-version;' \ -H 'referer: https://fanyi.caiyunapp.com/' \ -H 'sec-ch-ua: "Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"' \ -H 'sec-ch-ua-mobile: ?1' \ -H 'sec-ch-ua-platform: "Android"' \ -H 'sec-fetch-dest: empty' \ -H 'sec-fetch-mode: cors' \ -H 'sec-fetch-site: cross-site' \ -H 'user-agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36 Edg/113.0.1774.35' \ -H 'x-authorization: token:qgemv4jr1y38jyq6vhvi' \ --data-raw '{"trans_type":"en2zh","source":"test"}' \ --compressed
利用 Convert curl to Go (curlconverter.com) 生成代码如下:
package main import ( "fmt" "io" "log" "net/http" "strings" ) func main() { client := &http.Client{} var data = strings.NewReader(`{"trans_type":"en2zh","source":"test"}`) 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", "f1de93819e3bb9f68a199a51c6ee2efb") req.Header.Set("origin", "https://fanyi.caiyunapp.com") req.Header.Set("os-type", "web") req.Header.Set("os-version", "") req.Header.Set("referer", "https://fanyi.caiyunapp.com/") req.Header.Set("sec-ch-ua", `"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`) req.Header.Set("sec-ch-ua-mobile", "?1") req.Header.Set("sec-ch-ua-platform", `"Android"`) 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 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36 Edg/113.0.1774.35") req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi") resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() bodyText, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } fmt.Printf("%s\n", bodyText) }
运行结束后,获取到的响应数据(未格式化展示)如下
{"rc":0,"wiki":{},"dictionary":{"prons":{"en-us":"[t\u03b5st]","en":"[test]"},"explanations":["n.,vt.\u8bd5\u9a8c,\u6d4b\u8bd5,\u68c0\u9a8c"],"synonym":["examine","question","quiz","grill","query"],"antonym":[],"wqx_example":[["take a test","\u53c2\u52a0\u6d4b\u8bd5"],["receive a test","\u63a5\u53d7\u8003\u9a8c"],["put something to the test","\u68c0\u9a8c\u67d0\u4e8b"],["We will have an English test on Monday morning . ","\u661f\u671f\u4e00\u65e9\u4e0a\u6211\u4eec\u5c06\u6709\u4e00\u6b21\u82f1\u8bed\u6d4b\u9a8c\u3002"]],"entry":"test","type":"word","related":[],"source":"wenquxing"}}
利用该响应数据,我们就能够构造一个响应数据结构体,可利用 JSON转Golang Struct - 在线工具 - OKTools 进行代码生成。生成代码如下:
// 响应数据文本,少数参数有用 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"` }
同时,我们也可以把请求参数也封装成一个结构体,如下:
// 请求参数结构体 type DictRequest struct { // 翻译类型 TransType string `json:"trans_type"` // 翻译文本 Source string `json:"source"` // 用户id UserID string `json:"user_id"` }
把前面生成的请求代码封装改造(把请求参数和响应 json 数据序列化)成 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,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", "f1de93819e3bb9f68a199a51c6ee2efb") req.Header.Set("origin", "https://fanyi.caiyunapp.com") req.Header.Set("os-type", "web") req.Header.Set("os-version", "") req.Header.Set("referer", "https://fanyi.caiyunapp.com/") req.Header.Set("sec-ch-ua", `"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`) req.Header.Set("sec-ch-ua-mobile", "?1") req.Header.Set("sec-ch-ua-platform", `"Android"`) 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 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36 Edg/113.0.1774.35") req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi") // 发起请求 resp, err := client.Do(req) if err != nil { log.Fatal(err) } // 关闭请求流 defer resp.Body.Close() // 读取响应数据 bodyText, err := 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) } }
调用请求方法:main 函数
func main() { // 运行代码:go run dict.go hello // hello 即为要翻译的文本 if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, `usage: simpleDict WORD example: simpleDict hello`) os.Exit(1) } word := os.Args[1] query(word) }
运行结果如下:
test UK: [test] US: [tεst] n.,vt.试验,测试,检验
以上为官方版本,我自行改造了一部分内容,添加了以下功能:
在序列化之前(request := DictRequest{TransType: “en2zh”, Source: word} 之前)添加的判断代码如下:
// 判断是否为英文 dictionary := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" for _, v := range word { if !strings.Contains(dictionary, string(v)) { log.Fatal("Translation error, please enter English!") } }
当然,我们可以
main 函数改造如下
func main() { fmt.Printf("请输入您想翻译的单词:") var word string _, err := fmt.Scanf("%v", &word) if err != nil { fmt.Println(err) return } query(word) return }
建立简单 tcp 服务器,以方便验证代理服务器结果,实现效果:往 tcp 服务器发送什么数据,就会返回打印什么数据,可用 netcat 进行验证,先安装 netcat ,步骤如下:
tcp 服务器代码如下:
func main() { // 运行命令:go run tcp.go // windows 安装 netcat 之后,解压缩到 C:\Windows\System32 便可以使用 nc 命令 // 测试命令:nc 127.0.0.1 1080 // 监听发送给该端口的请求 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 } } }
接下来就是建立代理服务器的步骤了
从上图,我们可以知道 SOCKS5 的实现步骤分为以下三步:
认证阶段包括以下三个字段
VER | NMETHODS | METHODS |
---|---|---|
1 | 1 | 1 to 255 |
VER: 协议版本,socks5为0x05
NMETHODS: 支持认证的方法数量
METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
0x00:不需要认证
0x02 :用户密码认证
认证阶段逻辑步骤如下:
浏览器会给代理服务器发送一个请求参数包,以便通过认证,然后服务端得选择一种认证方式,告诉客户端:VER 0x05,METHOD 可为如下
代理服务器读取请求参数,并利用 io.ReadFull 读满一个缓冲区。
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) } // 设置为无需认证 _, err = conn.Write([]byte{socks5Ver, 0x00}) // 代理服务器还需要返回一个 response,返回包包括两个字段, // 一个是 version 一个是 method, if err != nil { return fmt.Errorf("write failed:%w", err) } return nil }
请求阶段:在完成认证以后,客户端需要告知服务端它的目标地址,需要包括以下请求参数包
VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1 | 1 | X’00’ | 1 | Variable | 2 |
VER:0x05,socks5的值为0x05
CMD:连接方式,0x01=CONNECT, 0x02=BIND, 0x03=UDP ASSOCIATE
RSV:保留字段,现在没什么用
ATYP:地址类型,0x01=IPv4,0x03=域名,0x04=IPv6
DST.ADDR
目标地址
目标地址类型,DST.ADDR的数据对应这个字段的类型。
0x01表示IPv4地址,DST.ADDR为4个字节
0x03表示域名,DST.ADDR是一个可变长度的域名
DST.PORT:目标端口,2字节,网络字节序(network octec order)
func connect(reader *bufio.Reader, conn net.Conn) (err error) { 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] // 读取 socks5Ver if ver != socks5Ver { return fmt.Errorf("not supported ver:%v", ver) } // 读取 cmd if cmd != cmdBind { return fmt.Errorf("not supported cmd:%v", cmd) } addr := "" // 处理 atyp 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) } // BigEndian:“network octec order” 网络字节序 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) _, 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
最后照例简单总结下:
欢迎关注我的字节后端青训营代码仓库,更新每日课后作业及其改进代码,除此之外,还会每周发布对应笔记,欢迎一起 star 或者 contribute 代码仓库。