Go实战案例
-
猜数字
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
maxNum := 100
//以现在的时间戳作为随机数种子
rand.Seed(time.Now().UnixNano())
//产生随机数,最大值不大于100
secretNumber := rand.Intn(maxNum)
// fmt.Println("The secret number is ", secretNumber)
fmt.Println("Please input your guess")
//把输入转换成一个带缓冲的数据流
reader := bufio.NewReader(os.Stdin)
for {
//读取字符串,碰到\n结束本次读取
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
}
}
}
语法基础复习
-
词典
第三方调用,http请求,json字符串处理
- 找到请求(edge浏览器)
- 复制curl
- 使用网址工具将curl转换为Go语言请求: curlconverter.com/
- 根据原Json请求, 编写请求封装结构体
var data = strings.NewReader(`{"trans_type":"en2zh","source":"cat"}`)
//to, 其中Source是要翻译的单词, 编写成变量, 用函数参数传递
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
- 复制响应Json字符串,通过网址 oktools.net/json2go 转换为Go结构体
- 将请求封装到结构体对象中, Json序列化
request := DictRequest{TransType: "en2zh", Source: word}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
- 在响应返回后, 做响应状态码判断
if resp.StatusCode != 200 {
log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
- 将响应正文反序列化, 得到 dictResponse结构体对象
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)
}
- 完整代码
package main
import (
"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 []interface{} `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":"cat"}`)
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-US;q=0.7,en-GB;q=0.6,ja;q=0.5")
req.Header.Set("app-name", "xy")
req.Header.Set("content-type", "application/json;charset=UTF-8")
req.Header.Set("device-id", "ff311187df232f0dd4a5e308daf4cdb9")
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)
}
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)
}
-
socks5代理服务器
socks5
-
代理服务器原理
-
- 握手阶段: 浏览向sock5代理发起请求,内容包括:协议的版本号, 支持认证的种类. sock5会选择其中一个认证方式,返回给浏览器. 返回00表示不需要认证, 其它代表了具体认证类型
- 认证阶段
- 请求阶段: 浏览器向sock5服务器发起请求, 内容包括: 版本号, 请求类型(一般是connection请求), 代理服务器就与目标ip建立链接
- relay阶段: 浏览器发送请求, 经由代理服务器转换到真正的服务器, 真正的服务器的响应也经由代理服务器返回. 锁仓k不关心流量的细节
-
分为4个阶段完成
-
- 简点的echo server
- 完成auth认证
- 完成请求阶段
- 完成relay阶段
v1
- 使用net.listen监听一个端口,会返回一个server
server, err := net.Listen("tcp", "127.0.0.1:1080")
- 死循环中使用accept, 持续接受连接
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v ", err)
continue
}
go process(client)
}
其中, go关键字表示启动一个 goroutinue(类似于子线程)
-
实现处理函数process
-
func process(conn net.Conn){ ...... } -
回退式地处理, 关闭连接
-
defer conn.Close() -
defer关键字修饰的语句会在程序结束后, 由后向前执行
-
使用bufio.NewReader创建带缓冲的只读流来读取输入
-
reader := bufio.NewReader(conn) for { b, err := reader.ReadByte() if err != nil { break } -
带缓冲的流底层可以合并读写操作, 可以减少系统调用的次数. 这边一个一个字节读取,出现err说明读取结束
-
把读到的byte返回
-
_, err = conn.Write([]byte{b})
-
v2
- 写上需要用到的常量
const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03 //域名
const atypeIPV6 = 0x04
- 写一个空的auth,需要参数redwe用来读取数据, conn链接对象
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
}
-
实现auto的逻辑
-
读取浏览器数据包, 浏览器向代理服务器发送的数据包包括:
- vesion协议版本号, 一个字节, 固定时版本5
-
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) } - 具体的方法, 用切片储存,每个方法是一个字节, 使用io.readFull读取, 会读到将切片填满为止
-
method := make([]byte, methodSize) _, err = io.ReadFull(reader, method) if err != nil { return fmt.Errorf("read method failed:%w", err) }
-
-
将代理服务器接受到的的版本号和选择的认证方式传回浏览器. 我们选择不进行认证
0x00
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
- 正常进行返回nil
return nil
v3
-
也是先读取浏览器发送的数据包, 包括如下字段:
-
version版本号,一个字节,固定为5
-
RSV保留字段, 我们不需要关心
-
command请求类型,我们只支持connection连接, connection表示要与代理服务器建立tcp连接
-
atype目标地址的类型,支持三种情况 IPV4,IPV6和域名(Host)
-
我们使用切片储存
-
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) } -
然后是目标地址,根据atyp的类型,地址长度不同
- 定义变量储存addr, 使用switch判断atyp类型
-
addr := "" switch atyp { ...... } - IP4: IP4的大小是4个byte, 我们直接使用之前定义的buf储存
-
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]) - Host: Host的第一个字节宝石主及地址的大小, 之后才是addr
-
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) - IP6: IP6我们不支持, 也处理一下其它不支持的情况
-
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])
-
代理服务器返回数据包包括如下字段:
- version版本号,0x05
- 返回类型, 成功返回0x00
- 保留字段, 返回0即可
- atype类型, 返回0x01
- 第五个, 我们不关心, 返回0
- 第六个, 我们不关心, 返回0
-
_, 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
v4
- 使用net.dial向刚才获得的 addr和port发起tcp链接,发起链接后要记得用defer来关闭连接
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)
- 使用go关联字开启两个协程, 使用io.copy进行数据转发. 使用context机制确保所有协程结束之后才返回connenct函数, Done方法会在调用cancel后返回,程序可以继续进行. 任一协程先完成后,被Done堵塞,另一一个携程结束时调用cancel结束这个堵塞, 但本身也被堵塞, 被defer 的cancel释放
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
- 使用SwitchyOmega使用代理服务器
新建情景模式, 使用socks5协议,服务器 127.0.0.1 , 端口 1080