Go实战之练习 |青训营

19 阅读8分钟

通过三个练习(猜数字、在线词典、SOCK5)从简单到复杂,借助一些网站工具,达到效果

猜数字

需求与功能分析

  1. 获取输入
  2. 生成随机数并将输入与随机数比对
  3. 返回结果
  4. 继续游戏(或退出)
  5. 完善出错/异常处理

各功能实现

  1. 获取输入与返回结果

import 包 fmt
输入 fmt.Scanln(&num)
输出fmt.Println("你猜的数是:", num)

  1. 生成随机数

import 包 math/rand time
通过时间生成随机数种子 rand.Seed(time.Now().Unix())
产生随机数 ran := rand.Intn(100)

  1. 循环判断是否继续游戏

增加一个for死循环,由用户输入true/false判断是否继续

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	var flag bool = true
	var num int
    rand.Seed(time.Now().Unix())
	ran := rand.Intn(100)
	for flag == true {
		fmt.Printf("输入猜测的数(0-100)")
		fmt.Scanln(&num)
		if ran == num {
			fmt.Println("你猜对了")
            flag = false
		} else {
			fmt.Println("你猜错了,数值为", ran)
            fmt.Println("你要继续吗?")
			fmt.Scanln(&flag)
		}
	}
}

展示效果

image.png

在线词典

需求与功能分析

  1. 获取用户输入单词
  2. 将该单词封装为请求
  3. 将请求发送个指定翻译网站的翻译服务器
  4. 接收服务器响应
  5. 将响应报文转化为对应结构体
  6. 提取所需要的翻译内容

各功能实现(两种翻译引擎)

  1. 获取输入

import 包 fmt
输入 fmt.Scanln(&word)

  1. 定义请求/相应结构体

请求结构体(右键负载复制object,粘贴至oktools.net/json2go 转为结构体)
image.png

// 请求结构体
type Myreq struct {
	Trans_type string `json:"trans_type"`
	Source     string `json:"source"`
}

响应结构体(右键预览复制object,粘贴至oktools.net/json2go 转为结构体)
image.png

// 响应结构体
type AutoGenerated 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"`
}
  1. 通信实现(右击请求响应的curl,复制,输入curlconverter.com/go/,获取为go通信代码,根据上述结构体构造逻辑)
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
)
func main() {
	//获取输入
	var word string
	fmt.Scanln(&word)
	query1(word)
}
func query1(word string) {
	//创建请求内容
	client := &http.Client{}
	request := Myreq{Trans_type: "en2zh", Source: word}
	buf, err := json.Marshal(request) //结构体转成json
	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-US;q=0.8,en;q=0.7,en-GB;q=0.6")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("device-id", "f46430288c84d1b75ef71587166e43c6")
	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="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 Edg/115.0.1901.183")
	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)

	//转化为结构体
	var myreps AutoGenerated
	err = json.Unmarshal(bodyText, &myreps)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(word, " UK:", myreps.Dictionary.Prons.En, " US:", myreps.Dictionary.Prons.EnUs)
	for _, item := range myreps.Dictionary.Explanations {
		fmt.Println(item)
	}
}
  1. 测试效果

image.png

课后作业(2、3)

image.png

增加另一种引擎

  1. 获取结构体——海词翻译

请求只有一个字符串很简单
image.png
响应结构体(右键预览复制object,粘贴至oktools.net/json2go 转为结构体)
image.png

type HaiciResponse struct {
	Ok  int `json:"ok"`
	Out struct {
		Query       string        `json:"query"`
		Dict        string        `json:"dict"`
		Transform   []interface{} `json:"transform"`
		Translation [][2]string   `json:"translation"`
	} `json:"out"`
}
  1. 获取通信代码——海词翻译
func query2(word string) {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{Transport: tr}
	url := "http://fanyi.dict.cn/search.php?jsoncallback=jQuery19105906961361475107_1692691662426&q=" + word + "&_=1692691662447"
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Accept", "application/json;charset=UTF-8, text/plain, */*; q=0.01")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,en-GB;q=0.6")
	req.Header.Set("Connection", "keep-alive")
	req.Header.Set("Cookie", "__utma=7761447.1192487628.1690789693.1690793682.1692691663.3; __utmc=7761447; __utmz=7761447.1692691663.3.3.utmcsr=cn.bing.com|utmccn=(referral)|utmcmd=referral|utmcct=/; __utmt=1; __utmb=7761447.1.10.1692691663; __gads=ID=29c666bb1b061390-226f8bab1de300e9:T=1690789691:RT=1692691662:S=ALNI_MYMAF1XRCTm_juD75qHxvKqKAH3ig; __gpi=UID=00000c259d0d4c55:T=1690789691:RT=1692691662:S=ALNI_MZyUt2PZTBbyZ8pAxeTsuupuXiTuQ")
	req.Header.Set("Referer", "http://fanyi.dict.cn/")
	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 Edg/115.0.1901.203")
	req.Header.Set("X-Requested-With", "XMLHttpRequest")
	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)
	}
	first := strings.Index(string(bodyText), "(")
	last := strings.LastIndex(string(bodyText), ")")
	responseBody := bodyText[first+1 : last]

	//转对象
	var Myreps HaiciResponse
	err = json.Unmarshal(responseBody, &Myreps)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(word)
	for _, sentence := range Myreps.Out.Translation {
		fmt.Printf(sentence[0] + " ")
		fmt.Println(sentence[1])
	}
}
  1. 测试效果

image.png

并行请求两个翻译引擎

  1. 并行采用goroutine
func go2(word string) {
	go func(word string) {
		query1(word)
	}(word)
	go func(word string) {
		query2(word)
	}(word)
	time.Sleep(time.Second)
}
  1. 结果

image.png

SOCK5代理

大致流程图
image.png

  1. 导包
package main
import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
)
  1. 定义代理所需的常量
const (
	socks5Ver = 0x05
	cmdBind   = 0x01 //connect
	atypeIPV4 = 0x01
	atypeHOST = 0x03 //域名
	atypeIPV6 = 0x04
)
  1. 实际调用过程
    • net.Listen("tcp", "127.0.0.1:1080")——tcp连接 监听IP地址为:127.0.0.1的1080端口
    • server.Accept()——for循环中等待连接(一次一个连接)
    • go process(client)——for循环中每次获取到连接,就开启一个协程进入通信代理
func main() {
    //tcp连接 监听IP地址为: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)
	}
}
  1. 代理通信详解
    • auth(reader, conn)——调用代理函数()
    • connect(reader, conn)——(代理成功后)调用连接
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
	}
}
  1. 代理(auth)详情
    • 按顺序逐字节读取(ver,methodSize,方法s)
      • ver, err := reader.ReadByte()——读取;
      • if ver != socks5Ver——校验版本
      • methodSize, err := reader.ReadByte()——读取;
      • method := make([]byte, methodSize)——按照methodSize的大小创建等大小的byte[]
      • _, err = io.ReadFull(reader, method)——将剩下的方法读到method缓冲区
      • _, err = conn.Write([]byte{socks5Ver, 0x00})——把socks版本号写回conn,0x00 表示认证成功
/*
ver——协议版本,socks5为0x05
methodSize——支持认证的方法数量
method——对应nmethods的值为多少,就有多少字节
*/
func auth(reader *bufio.Reader, conn net.Conn) (err error) {

	//按顺序逐字节读取(ver,methodSize,方法s)
	ver, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ver failed:%w", err)
	}

	//检验socks版本号
	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)
	}

	//按照methodSize的大小创建等大小的byte[]
	method := make([]byte, methodSize)

	//将剩下的方法读到method缓冲区
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("read method failed:%w", err)
	}
	//log.Println("ver", ver, "method", method)
	//ver method

	//把socks版本号写回conn,0x00 表示认证成功
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}
  1. 连接详情
    1. buf := make([]byte, 4)——创建一个大小为4的byte[],用来读取header
    2. _, err = io.ReadFull(reader, buf)——tips:经过auth,reader目前读取指针指向header,读取header
    3. ver, cmd, atype := buf[0], buf[1], buf[3]——header 的第1,2,4字节分别是版本号ver、命令码cmd(0x01表示connect请求,0x02表示bind...)、目标地址类型atype(有三种可能)
    4. if ver != socks5Ver ——验证socks版本号
    5. if cmd != cmdBind——验证命令码
    6. switch atype——通过确认目标地址类型获取地址
    7. _, err = io.ReadFull(reader, buf[:2])——接着读取端口,两个字节
  2. relay阶段
    1. dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))——tcp连接指定地址和端口
    2. defer dest.Close()——延迟关闭连接
    3. _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})——server确定能够代理,返回响应
    4. ctx, cancel := context.WithCancel(context.Background())——利用根context获得父context和取消函数
    5. _, _ = io.Copy(dest, reader)——并发执行代理转发
    6. <-ctx.Done()——取消并行事件
func connect(reader *bufio.Reader, conn net.Conn) (err error) {

	//创建一个大小为4的byte[],用来读取header
	buf := make([]byte, 4)

	//tips:经过auth,reader目前读取指针指向header(已读ver,methodSize,method)
	_, err = io.ReadFull(reader, buf)
	if err != nil {
		return fmt.Errorf("read header failed:%w", err)
	}

	/*header 的第1,2,4字节分别是版本号ver、
    命令码cmd(0x01表示connect请求,0x02表示bind...)、
    目标地址类型atype(有三种可能)*/
	ver, cmd, atype := buf[0], buf[1], buf[3]

	//验证socks版本号
	if ver != socks5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}

	//验证命令码
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", ver)
	}

	//通过确认目标地址类型获取地址
	addr := ""
	switch atype {
	case atypeIPV4:
		//继续接着读取reader(此时内容为目的地址)
		_, err = io.ReadFull(reader, buf)
		if err != nil {
			return fmt.Errorf("read atpye 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 atpye failed:%w", err)
		}
		addr = string(host)
	case atypeIPV6:
		//返回错误,不支持ipv6
		return errors.New("IPV6: no supported yet")
	default:
		return errors.New("invalid atype")
	}

	//接着读取端口,两个字节
	_, err = io.ReadFull(reader, buf[:2])
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	//二进制转大端字节序的 16 位无符号整数
	port := binary.BigEndian.Uint16(buf[:2])

	//relay阶段
	//tcp连接指定地址和端口
	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)

	//server确定能够代理,返回响应
	//ver methodSize method cmd atype 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获得父context和取消函数
	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
}