实践记录(一)猜谜游戏、在线词典、SOCKS5服务器的实现 | 青训营

185 阅读9分钟

一、猜谜游戏

这是一个简单的猜谜游戏程序,玩家需要猜出系统随机生成的秘密数字。如果玩家的猜测大于秘密数字(0-100),则系统会提示"Your guess is bigger than the secret number. Please try again";如果猜测小于秘密数字,则系统会提示"Your guess is smaller than the secret number. Please try again";当猜测等于秘密数字时,系统会输出"Correct, you Legend!",游戏结束。

这个程序包含以下主要部分:

  1. 导入所需的包,包括"bufio"、"fmt"、"math/rand"和"os"。
  2. 定义最大数字maxNum,在这个例子中为100。
  3. 使用rand.Intn()函数生成一个0到maxNum-1之间的随机整数作为秘密数字secretNumber
  4. 进入游戏循环,不断接收玩家的输入并进行处理。
  5. 使用bufio.NewReader()os.Stdin创建一个输入读取器reader
  6. 在游戏循环中,使用reader.ReadString('\n')读取玩家输入的内容,直到遇到换行符。
  7. 使用strings.Trim(input, "\r\n")去除输入中的回车符和换行符,得到处理后的输入input
  8. 使用strconv.Atoi(input)将处理后的输入转换为整数类型guess
  9. 根据玩家的猜测与秘密数字之间的关系,输出相应的提示信息。
  10. 如果玩家猜中了秘密数字,输出"Correct, you Legend!",游戏结束。
  • 代码实现
package main

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

func generateSecretNumber(maxNum int) int {
	rand.Seed(time.Now().UnixNano())
	return rand.Intn(maxNum)
}

func getUserInput() int {
	fmt.Println("Please input your guess:")
	reader := bufio.NewReader(os.Stdin)
	input, _ := reader.ReadString('\n')
	input = strings.TrimSpace(input)

	guess, err := strconv.Atoi(input)
	if err != nil {
		fmt.Println("Invalid input. Please enter an integer value.")
		return getUserInput()
	}

	return guess
}

func main() {
	maxNum := 100
	secretNumber := generateSecretNumber(maxNum)

	fmt.Println("Guess the secret number between 0 and", maxNum-1)

	for {
		guess := getUserInput()

		fmt.Println("You guessed:", 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
		}
	}
}

二、在线词典

在线词典要求用户输入要翻译的内容。用户输入完内容后,程序将调用 query(word) 函数来进行翻译,并将翻译结果输出到控制台。

在程序的 main() 函数中,我们首先创建一个 scanner 对象,它是 bufio 包提供的一个方便读取输入的工具。然后,程序进入一个无限循环,不断等待用户的输入。

每次循环开始时,程序会输出一条提示信息:"请输入要翻译的内容:"。然后调用 scanner.Scan() 来读取用户输入的内容。scanner.Scan() 会等待用户输入,并将输入的内容读取为字符串。用户输入的内容被保存在变量 word 中。

接下来,程序将调用 query(word) 函数,把用户输入的内容作为参数传递给它。query(word) 函数是我们自己定义的,它用于发送翻译请求,并解析返回的翻译结果。

query(word string) 函数中,我们首先创建一个 HTTP 客户端 client,用于发送 HTTP 请求。然后,我们定义了一个 DictRequest 结构体,用于存储请求的数据。我们填充这个结构体的字段,包括 TransType(翻译类型)和 Source(要翻译的内容)。

接着,我们将 DictRequest 结构体转换成 JSON 格式,并将其作为请求体发送给在线词典的 API。我们设置了请求头的一些信息,如 Content-Typeapplication/json;charset=UTF-8

然后,我们使用 client.Do(req) 发送 POST 请求,并获取返回的响应对象 resp。我们从响应对象中读取返回的 JSON 数据,然后使用 json.Unmarshal() 解析成我们定义的 DictResponse 结构体,以便获取翻译结果。

最后,我们将翻译结果输出到控制台。这里输出了 "UK:" 和 "US:" 后跟着英式和美式发音,以及 dictResponse.Dictionary.Explanations 中的解释内容。

程序会一直循环等待用户输入,直到用户主动退出。如果用户输入的内容不符合预期,比如不是有效的整数,或者在线词典API返回了错误信息,程序会输出相应的错误提示,并重新等待用户输入。

总结:这个程序实现了一个简单的在线词典翻译功能。用户可以通过命令行输入要翻译的内容,然后程序会将其发送给在线词典 API 进行翻译,并将翻译结果输出到控制台。通过这个例子,我们可以学习如何使用 Go 的标准库来进行 HTTP 请求和 JSON 数据的处理,同时也了解了如何处理用户输入和错误情况。

  • 代码实现
package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"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 {
		KnownInLaguages int `json:"known_in_laguages"`
		Description     struct {
			Source string      `json:"source"`
			Target interface{} `json:"target"`
		} `json:"description"`
		ID   string `json:"id"`
		Item struct {
			Source string `json:"source"`
			Target string `json:"target"`
		} `json:"item"`
		ImageURL  string `json:"image_url"`
		IsSubject string `json:"is_subject"`
		Sitelink  string `json:"sitelink"`
	} `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"`
}

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("Connection", "keep-alive")
	req.Header.Set("DNT", "1")
	req.Header.Set("os-version", "")
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
	req.Header.Set("app-name", "xy")
	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
	req.Header.Set("Accept", "application/json, text/plain, */*")
	req.Header.Set("device-id", "")
	req.Header.Set("os-type", "web")
	req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
	req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
	req.Header.Set("Sec-Fetch-Site", "cross-site")
	req.Header.Set("Sec-Fetch-Mode", "cors")
	req.Header.Set("Sec-Fetch-Dest", "empty")
	req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
	req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
	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() {
	scanner := bufio.NewScanner(os.Stdin)
	for {
		fmt.Println("请输入要翻译的内容:")
		scanner.Scan()
		word := scanner.Text()

		query(word)
	}
}

三、SOCKS服务器

这是一个简单的 Socks5 代理服务器的实现,它可以将客户端的网络请求转发到目标服务器。

  1. 首先,在 main 函数中,我们创建一个监听器(listener)并绑定到本地的 127.0.0.1:1080 地址上。这样,代理服务器就会在本地监听 1080 端口,等待客户端的连接请求。
  2. process 函数用来处理每个客户端连接。当一个客户端连接上来后,我们先进行 Socks5 协议的认证过程,即客户端发送认证请求,服务器返回认证响应。在这个实现中,我们只支持无需认证(0x00)的方式。
  3. 接下来,客户端会发送连接请求,包括请求的目标地址和端口。服务器收到连接请求后,会解析请求,提取出目标地址和端口。然后,代理服务器会建立与目标服务器的连接,并将客户端的请求转发给目标服务器。
  4. 在建立与目标服务器的连接后,代理服务器会使用两个 goroutine 来进行数据的传输。一个 goroutine 负责将客户端发送的数据转发给目标服务器,另一个 goroutine 负责将目标服务器返回的数据转发给客户端。使用两个 goroutine 进行并发的数据传输,可以实现同时处理多个客户端连接。
  5. 当任意一个 goroutine 发生错误或数据传输完成时,它会取消另一个 goroutine 的执行,并关闭连接,释放资源。

总体来说,这个代码实现了一个简单的 Socks5 代理服务器,可以接受客户端连接,并将客户端的网络请求转发到目标服务器。这个代理服务器目前只支持无需认证的方式,并且只支持 IPv4 地址类型,对于 IPv6 地址类型尚未实现。在实际应用中,还可以进一步扩展功能,例如添加用户认证、支持更多的地址类型等。

  • 代码实现
package main

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

const (
	socks5Ver   = 0x05
	cmdBind     = 0x01
	atypeIPV4   = 0x01
	atypeHOST   = 0x03
	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 connect failed: %v", conn.RemoteAddr(), err)
		return
	}
}

func auth(reader *bufio.Reader, conn net.Conn) 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})
	if err != nil {
		return fmt.Errorf("write failed: %w", err)
	}
	return nil
}

func connect(reader *bufio.Reader, conn net.Conn) 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]
	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)

	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 代理服务器。它监听在本地的 1080 端口,并接受来自客户端的连接。对每个连接,它会尝试进行 SOCKS5 认证,然后根据客户端请求连接到目标服务器,并将客户端与目标服务器之间的数据进行转发。

现在,我来逐步解释代码的主要部分:

  1. 首先,定义了常量 socks5VercmdBindatypeIPV4atypeHOST,这些常量分别表示 SOCKS5 协议版本号、连接请求命令、目标地址类型(IPv4 和 域名)等。
  2. main() 函数启动了 SOCKS5 代理服务器,监听在 127.0.0.1:1080 端口上。在主循环中,它接受客户端的连接,并为每个客户端连接启动一个 goroutine 处理。
  3. process() 函数是每个客户端连接的处理函数,它首先调用 auth() 函数进行 SOCKS5 认证,然后调用 connect() 函数连接到目标服务器,并实现数据转发。
  4. auth() 函数用于进行 SOCKS5 认证。它从客户端读取协议版本号和支持的认证方法数量,并向客户端返回认证成功的响应。
  5. connect() 函数用于处理客户端的连接请求,并连接到目标服务器。它首先解析客户端的请求,获取目标地址和端口。然后,它连接到目标服务器,并通过 goroutine 进行数据转发。
  6. 在进行数据转发时,使用了 context.Context 来管理 goroutine。每个连接都创建了一个上下文 ctx 和一个 cancel 函数。当客户端或目标服务器的连接关闭时,调用 cancel() 函数通知其他 goroutine 结束转发,并返回错误。

测试

  1. 启动程序
  2. 打开命令行窗口输入curl --socks5 127.0.0.1 1080 -v http://www.baidu.com以测试

image.png