Go语言项目实践笔记

82 阅读7分钟

猜谜游戏

  • 随机数生成

  • 使用bufio.NewReader读取用户输入,输入以Enter键结束

  • input, err := reader.ReadString('\n')从reader中读取输入,直到遇到\n换行符为止。并返回读取的字符串以及可能的错误。

  • input = strings.TrimSuffix(input, "\n")需要修改为input = strings.TrimSuffix(input, "\r\n")

    因为在Windows中,行结束符可以是\r\n也可以是\n,而strconv会将其视为输入的一部分; 而在Linux/Mac中,行结束符只能是\n,strconv不会将其视为输入的一部分

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
		}
		input = strings.TrimSuffix(input, "\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. 循环以及if的使用

在线词典

前期准备

image-20230517205614616

img

请求创建

req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)

请求头设置

req.Header.Set("Connection", "keep-alive")
req.Header.Set("DNT", "1")
req.Header.Set("os-version", "")
.........

请求体的实现

image-20230517212240673

image-20230517212021425

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
	UserID    string `json:"user_id"`
}
client := &http.Client{}
request := DictRequest{TransType: "en2zh", Source: word}
buf, err := json.Marshal(request)

当我们使用encoding/json将这个结构体序列化为JSON字符串时,会使用json:"字段名"定义的标签作为JSON对象的键:

{
    "trans_type": "en2zh", 
    "source": "hello",
    "user_id": ""
}

然后我们可以将这个JSON字符串作为HTTP请求的body,发送给API。

发起请求

resp, err := client.Do(req)

读取响应

响应结构定义直接利用JSON结构代码自动生成

//响应体结构
type DictResponse struct {
}
bodyText, err := ioutil.ReadAll(resp.Body)

反序列化JSON

var dictResponse DictResponse
//json.Unmarshal函数的作用是反序列化JSON字符串为Go语言中的结构体或map
//将bodyText中的JSON字符串反序列化为dictResponse这个DictResponse结构体实例
err = json.Unmarshal(bodyText, &dictResponse)

知识点总结:

1. 请求代码自动生成,JSON结构自动生成
2. 请求创建以及发起 `client := &http.Client{}`  `resp, err := client.Do(req)`
3. JSON序列化以及反序列化的使用

SOCKES5代理服务器

简单TCP服务器程序的实现

整个程序是一个简单的回显服务器,接收客户端连接,读取客户端发送的数据并原样返回。

  1. Listen函数监听127.0.0.1:1080地址,等待客户端连接。
  2. 当有新的客户端连接时,Accept函数接受连接,并启动goroutine处理该连接。
  3. 在goroutine中,首先定义defer语句,确保函数结束时关闭连接。
  4. 然后创建一个bufio.Reader,用于高效读取数据。
  5. 在for循环中,不断读取客户端发送的单个字节,并直接发送回客户端。
  6. 如果任意一方断开连接,for循环会结束, defer语句关闭连接,函数返回。
package main

import (
	"bufio" // 导入bufio包,提供带缓存的I/O操作
	"log"   // 导入log包,提供日志记录功能
	"net"   // 导入net包,提供网络相关的操作
)

func main() {
    // 监听本地地址127.0.0.1:1080的TCP连接
    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
        }
        // 启动一个新的goroutine处理客户端连接
        go process(client)
    }
}

func process(conn net.Conn) {
    defer conn.Close() // 延迟关闭连接
    reader := bufio.NewReader(conn) // 创建一个带缓存的读取器
    for {
        // 读取单个字节
        b, err := reader.ReadByte()
        if err != nil {
            break
        }
        //println("form server get b=%v",b)
        _, err = conn.Write([]byte{b}) // 将读取到的字节写回客户端
        if err != nil {
            break
        }
    }
}

主要使用了以下包:

  • net - 用于网络连接的监听,读写等操作。
  • bufio - 提供高效的IO函数,这里使用Reader读取数据。
  • log - 用于打印日志信息。

关键函数:- Listen - 监听指定地址,等待连接。

  • Accept - 接受新的客户端连接。
  • ReadByte - 读取一个字节的数据。
  • Write - 将数据写回给客户端。
  • defer conn.Close - 关闭连接。

SOCKS5协议工作原理

  1. 协商(握手)阶段

    第一个握手阶段,浏览器会向Socks5代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,Socks5服务器会选中一个认证方式,返回给浏览器。如果返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程;

  2. 认证阶段

  3. 请求阶段

    认证通过之后浏览器会Socks5服务器发起请求。主要信息包括版本号,请求的类型,一般主要是Connection请求,就代表代理服务器要和某个域名或者某个IP地址某个端口建立TCP连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应

  4. relay阶段

    此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到览器这边。然后实际上代理服务器并不关心流量的细节,可以是HTTP流量,也可以是其它TCP流量。

    image-20230518165530455

Socks5协议中的身份验证过程

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)
	}
	// 如果协议版本号不是Socks5,则返回一个错误
	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)
	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}

Socks5协议中的连接请求过程

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 !=5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", ver)
	}
	addr := ""
	switch atyp {
	case atypIPV4:
		// 解析IPv4地址
		_, 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],[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:
		// 解析IPv6地址
		ipv6Addr := make([]byte, 16)
		_, err = io.ReadFull(reader, ipv6Addr)
		if err != nil {
			return fmt.Errorf("read ipv6Addr failed:%w", err)
		}
		addr = net.IP(ipv6Addr).String()
	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",)
	}

	// 开始进行数据传输
	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
}