GO语言实战案例 | 青训营笔记

178 阅读9分钟

1.猜谜游戏

package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"
)

func main() {
    //生成随机数
	maxNum := 100
	rand.Seed(time.Now().UnixNano())//用时间戳来初始化随机数种子
	secretNumber := rand.Intn(maxNum)
	// fmt.Println("The secret number is ", secretNumber)

	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)
			continue
		}
        
        //ReadString返回的结果包含结尾的换行符,将其去掉
		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
		}
	}
}

思路:

  1. 生成随机数
  2. 读取输入文本
  3. 删除不必要的换行符
  4. 转化文本为数字
  5. 循环判断是否猜数正确
  6. 正确退出循环
  7. 不正确则从第二步重新开始

2.词典爬虫

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

// 请求参数结构体
type DictRequest struct {
	// 翻译类型
	TransType string `json:"trans_type"`
	// 翻译文本
	Source string `json:"source"`
	// 用户id
	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 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)
	}
}

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)
}

  • 在网站输入信息观察网络活动

  • 通过浏览器开发者工具复制cURL(bash)

  • 利用网站生成代码

  • 运行获取响应数据

  • 利用网站将响应数据转换为结构体

  • 把请求参数封装为结构体

  • 把前面生成的请求代码封装改造成方法

  • 在main函数中调用方法

示例抓包网站:fanyi.caiyunapp.com/

代码生成网站:

curl命令——json

curlconverter.com/go/

json——golang struct

oktools.net/json2go

3.socks5代理

[ SOCKS5代理实例理解分析 | 青训营笔记 ] - 掘金 (juejin.cn)

1.socks5协议

Socks 协议是一种代理 (Proxy) 协议, 它在使用TCP/IP协议通信的前端机器和服务器之间发挥中介作用,使得内部网中的前端机器能够访问Internet网中的服务器。

例如我们所熟知的 Shdowsocks 便是 Socks 协议的一个典型应用程序, Socks 协议有多个版本, 目前最新的版本为 5, 其协议标准文档为 RFC 1928

2.代理服务器

代理服务器(Proxy Server)的功能是代理网络用户去取得网络信息。形象地说,它是网络信息的中转站,是个人网络和Internet服务商之间的中间代理机构,负责转发合法的网络信息,对转发进行控制和登记

3.socks5代理服务器

主要功能:处于使用TCP/IP协议通信的前端机器,将信息或请求发送给Socks5,再由Socks5发送给服务器,使得内部网中的前端机器能够访问Internet网中的服务器

4.socks5交互原理

屏幕截图 2023-05-23 214509.png

屏幕截图 2023-05-23 214657.png

  • 客户端请求代理连接:客户端通过建立与 SOCKS5 代理服务器的连接来请求代理服务。通常,客户端会在其配置中指定代理服务器的地址和端口。

  • 协商认证方式:一旦连接建立,客户端和代理服务器会协商认证方式。SOCKS5 协议支持多种认证方式,包括无需认证、用户名/密码认证等。客户端和代理服务器会选择一种双方都支持的认证方式进行验证。

  • 请求目标服务器连接:认证完成后,客户端发送请求给代理服务器,包括目标服务器的地址和端口号。客户端还可以指定是否需要远程解析 DNS,以及是否启用 UDP 代理。

  • 代理服务器连接目标服务器:代理服务器使用自己的 IP 地址和端口与目标服务器建立连接。这样,代理服务器充当了客户端和目标服务器之间的中间人。

  • 代理数据传输:一旦代理服务器成功建立与目标服务器的连接,它就开始在客户端和目标服务器之间传输数据。客户端发送的数据会经过代理服务器中转,然后代理服务器将响应从目标服务器传回给客户端。

  • 连接关闭:当客户端或目标服务器关闭连接时,代理服务器也会关闭与两者的连接。这标志着一次 SOCKS5 代理会话的结束。

5.go语言实现原理

在Go语言中,可以使用net包来实现Socks5代理的功能。通过net.Listen函数可以创建一个Socks5代理服务器,通过net.Dial函数可以创建一个Socks5代理客户端。

当客户端连接到代理服务器时,代理服务器将解析连接请求,验证客户端的身份,然后将请求转发到目标服务器。当目标服务器响应后,代理服务器将解析响应,将响应返回给客户端。

在Go语言实现Socks5代理时,需要使用bufio包来处理I/O缓冲区。bufio.NewReader函数用于创建一个缓冲读取器,可以用来读取从客户端和代理服务器之间传输的数据。bufio.Reader提供了一些方便的方法来读取和操作缓冲区的数据。

在实现Socks5代理时,我们需要处理客户端和代理服务器之间的数据报格式。Socks5代理协议的数据报格式由多个字段组成,每个字段都有特定的含义和格式。具体来说,Socks5代理协议的数据报格式如下:

客户端发送的数据报格式(握手协议):

VERNMETHODSMETHODS
111 to 255

代理服务器回复的数据报格式(握手协议):

VERMETHOD
11

客户端发送的数据报格式(请求协议):

VERCMDRSVATYPDST.ADDRDST.PORT
1101Variable2

代理服务器回复的数据报格式(请求协议):

VERREPRSVATYPBND.ADDRBND.PORT
1101Variable2

其中,VER表示协议版本号,NMETHODS表示支持的认证方法数量,METHODS表示支持的认证方法列表。CMD表示请求类型,RSV为保留字段,ATYP表示目标地址类型,DST.ADDR表示目标地址,DST.PORT表示目标端口。REP表示回复类型,BND.ADDR表示代理服务器绑定的地址,BND.PORT表示代理服务器绑定的端口。

使用Go语言实现Socks5代理时,我们可以使用bufio包来处理数据报格式中的各个字段。在建立连接后,我们可以使用bufio.NewReader函数创建一个缓冲读取器来读取客户端发送的数据。通过使用bufio包提供的方便的方法,我们可以轻松地解析Socks5代理协议的数据报格式,并处理代理请求和回复。

总之,Go语言提供了方便易用的bufio包来处理I/O缓冲区,使得在实现Socks5代理协议时能够更加轻松地解析数据报格式。熟练掌握bufio包的使用,可以让我们更加高效地实现Socks5代理协议,并提供高效稳定的代理服务。

6.代码与分析

package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

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)
	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/PASSWORD

	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)
	}

	// +----+--------+
	// |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 代理服务器的简单示例。以下是代码的主要功能和流程解释:

  1. 定义了一些常量,包括 SOCKS5 协议版本、命令类型、地址类型等。
  2. main 函数中,创建一个 TCP 服务器监听在 127.0.0.1:1080 地址上。
  3. 通过无限循环接受客户端连接,在每个连接上调用 process 函数进行处理。
  4. process 函数处理每个客户端连接的逻辑。它首先读取客户端发送的认证请求,并根据协议规范进行认证。
  5. auth 函数处理认证过程。它首先读取客户端发送的协议版本,然后读取支持的认证方法数量和方法列表。根据规范,服务器返回支持的认证方法给客户端。
  6. 客户端在收到认证响应后,可以选择其中一种认证方法进行身份验证。
  7. 客户端完成认证后,调用 connect 函数建立与目标服务器的连接。
  8. connect 函数读取客户端发送的连接请求,并解析目标地址和端口。
  9. 根据解析得到的目标地址和端口,建立与目标服务器的连接。
  10. 客户端和目标服务器之间的数据传输通过两个 goroutine 实现:一个从客户端读取数据并发送到目标服务器,另一个从目标服务器读取数据并发送到客户端。
  11. 使用上下文 ctx 和取消函数 cancel 来控制数据传输的生命周期。
  12. 当其中一个 goroutine 完成数据传输或发生错误时,取消函数被调用,通知另一个 goroutine 停止数据传输。
  13. 最后,函数等待上下文的完成信号,结束处理。

总体而言,这段代码实现了一个简单的 SOCKS5 代理服务器,它可以接受客户端连接并转发流量到目标服务器。