Go 语言的实战案例 | 青训营笔记
- 猜字谜游戏
- 在线字典
- SOCKS5 代理
猜字谜游戏
graph TD
1.生成随机数 --> 2.读取输入文本 --> 3.删除不必要的换行 --> 4.转换文本为数字 --> 5.循环判断是否猜数准确 --> 6.正确退出循环/错误返回第二步
猜字谜游戏代码
package main
import (
"fmt"
"math/rand"
"time"
"os"
"bufio"
"strconv"
"strings"
)
func main(){
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
//fmt.Println("The secret number issecretNumber)
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)
return
continue
}
input = strings.TrimSuffix(input,"\n")
guess,err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
return
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.PrintIn("Correct, you Legend!")
break
}
}
}
1.生成随机数
import"time"模块,rand.Seed(time.Now().UnixNano())该方法使用时间戳来初始化随机数种子,这样就会每次输出不一样要猜测的数据。若只有这secretNumber := rand.Intn(maxNum)一条代码,会导致每次只输出相同的数据。
2.读取输入文本
stdin文件用os.Stdin得到,直接操作这个文件非常不方便,所以使用reader := bufio.NewReader(os.Stdin)来转成一个只读的数据。
3.删除不必要的换行
input,err := reader.ReadString("\n')读取到一行,就发现会有不必要的换行符,input = strings.TrimSuffix(input,"\n")去掉换行。
4.转换文本为数字
用guess,err := strconv.Atoi(input)转成数字,若转换成失败会弹出错误。
在线字典
graph TD
1.在网站上获得请求 --> 2.将请求转换为Go语言类型的程序 --> 3.将JSON字符串输入转换成JSON序列化 --> 4.整理数据
1.在网站上获得请求
通过彩云小译(caiyunapp.com),并对英语单词进行翻译,在网站中获得其请求。
2.将请求转换为Go语言类型的程序
通过Convert curl to Go (curlconverter.com)将请求转换为Go语言类型,返回结果为JSON类型的字符串
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)创建请求头
package main
import (
"fmt"
"io"
"log"
"net/http"
"strings"
)
func main() {
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"awesome"}`)
//创建请求
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", "5b0ce53c39e131ad4014c3044e87886b")
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", "?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/113.0.0.0 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)
}
3.将JSON字符串输入转换成JSON序列化
用buf, err := json.Marshal(request)去序列化request数组,将固定的JSON字符串转变为JSON序列化
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
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":"awesome"}`)
request := DictRequest{TransType: "en2zh", Source: "good"}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf)
}
将响应结果转化并保存在结构体变量dictResponse中
\#%v来打印dictResponse,用最详细的方式打印结构体
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 []interface{} `json:"synonym"`
Antonym []interface{} `json:"antonym"`
WqxExample []interface{} `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
func main() {
// fmt.Printf("%s\n", bodyText)
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", dictResponse)
}
4.整理数据
对内容进行处理,选择我们想要的信息进行输出
func main() {
//对输出进行裁剪选择
fmt.Println("awesome", "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
fmt.Println(item)
}
}
SOCKS5 代理
SOCKS5 的实现步骤分为以下四步:
- 协商阶段:客户端向代理服务器发送代理请求,其中包含了代理的版本和认证方式;
- 认证阶段:服务端收到客户端的代理请求后,选择双方都支持的加密方式回复给客户端,此时客户端收到服务端的响应请求后,双方握手完成,开始进行协议交互;
- 请求阶段:客户端向代理服务器发送请求,由代理服务器将请求转发给真正想要请求的服务器;
- relay阶段:客户端向代理服务器发送数据,由代理服务器将数据转发给服务器,并将服务器返回的响应结果发送给客户端。
代码实现
1.协商阶段:
让服务器监听 127.0.0.1:1080 网址端口
reader := bufio.NewReader(conn)将连接关掉,结束整个函数周期
func main() {
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)
}
}
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
b, err := reader.ReadByte()
if err != nil {
break;
}
_, err = conn.Write([]byte{b})
if err != nil {
break
}
}
}
2.认证阶段: 客户端向代理服务器发送类似注释中的数据,代理服务器对客户端发送的数据进行错误处理,如果没有错误,就返回认证信息
| VER | NMETHODS | METHODS |
|---|---|---|
| 1 | 1 | 1 to 255 |
-
VER: 协议版本,socks5为0x05
-
NMETHODS: 支持认证的方法数量
-
METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
- 0X00 不需要认证
- 0X02 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)
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
3.请求阶段: 在客户端和代理服务器之间建立了连接后,客户端就向代理服务器发送请求,由代理服务器将请求转发给真正想要请求的服务器,再由服务器返回响应
| VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|---|---|---|---|---|---|
| 1 | 1 | x'00' | 1 | Variable | 2 |
- VER 版本号,socks5的值为0x05
- CMD 0x01表示CONNECT请求 连接方式
- RSV 保留字段,值为0x00
- ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
- 0x01表示IPv4地址,DST.ADDR为4个字节
- 0x03表示域名,DST.ADDR是一个可变长度的域名
- DST.ADDR 一个可变长度的值
- DST.PORT 目标端口,固定2个字节
| VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|---|---|---|---|---|---|
| 1 | 1 | X'00' | 1 | Variable | 2 |
- VER socks版本,这里为0x05
- REP Relay field,内容取值如下 X’00’ succeeded
- RSV 保留字段
- ATYPE 地址类型
- BND.ADDR 服务绑定的地址
- BND.PORT 服务绑定的端口DST.PORT
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
buf := make([]byte, 4) //四个字节的缓冲区
_, err = io.ReadFull(reader, buf) //先填充满buf,内容包括VER、CMD、RSV、ATYP
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
ver, cmd, atyp := buf[0], buf[1], buf[3]
//对VER、CMD、ATYP进行错误处理
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", ver)
}
addr := ""
switch atyp {
//根据ip地址的不同类型进行相应的处理
case atypIPV4:
_, 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")
}
//读取端口号保存在buf中
_, 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
}
4.relay阶段:
在请求阶段的代码中,添加服务器对客户端请求的数据的响应结果
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
... //请求阶段代码
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
}