GO语言基础 - 小项目| 青训营笔记

111 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

wallhaven-k7lpe6_3840x2160.png

前言

本文主要是记载青训营第一节课在教了go语言基础语法后

老师从易到难,由浅入深的分享了的三个小项目:

  1. 猜数字
  2. 在线词典
  3. socks5代理服务器

因为老师留了课后作业,所以一并把解题思路记录下来

省略了所有的异常处理和一些不太重要的逻辑

猜数字

描述

生成随机数,用户输入随机数,判断输入是否有效

如果是数字,判断与生成数大小,猜中则退出程序,否则继续交互

作业要求:使用Scanf简化用户输入读取


知识点

  1. 随机数的生成

    • rand.Intn()
    • 随机种子的设置
    • 计算机伪随机数
  2. 流程控制语句(if、for)

  3. 输入输出

    • fmt.Scanf、fmt.Println
    • bufio.NewReader(os.Stdin).reader.ReadString()

核心代码

maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)

// reader := bufio.NewReader(os.Stdin)
// 输出 略
var guess int
for {
    // 另一种输入写法,使用bufio,要先NewReader
    // input, _ := reader.ReadString('\n')
    // input = strings.TrimSuffix(input, "\r\n")
    _, err := fmt.Scanf("%d ", &guess)
    if err != nil {
            // 输出 略
            continue
    }
    // 略
    if guess > secretNumber {             // 输出 略
    } else if guess < secretNumber {      // 输出 略
    } else {                              // 输出 略
        break
    }
}

执行效果

image.png

在线词典


描述

读取用户指令输入的单词,调用第三方翻译api,爬取翻译的关键信息输出

作业要求:

1. 使用两种搜素引擎实现结果

2. 使用并发处理两种搜索引擎的请求过程,提高程序性能


知识点

  1. 启动参数的读取

    • os.Args获取启动参数
  2. http请求的发送和响应的获取与处理

    • net/http包
  3. json包的配置

    • Marshal 和 Unmarshal
  4. 如何模拟浏览器上的一个http网络请求?

    1. 打开开发者工具,点击NetWork栏
    2. 在Name栏中找到目标的请求(需要自主筛选,有些请求需要判断)
    3. 右键 Copy As cURL(window需要选择bash格式)
    4. 使用脚本转Go代码的工具,自动生成代码
    5. 截取返回的json字符串,使用json转go结构体的工具,生成结构体
    6. 在代码中编写json解码相关内容,同时选择需要的属性进行处理即可
  5. 并发编程入门 goroutine + sync.WaitGroup 实现并发调用

    • 使用 goroutine 协程
    • 使用 sync.WaitGroup 阻塞等待

核心代码

// 第一种搜索引擎
// 利用返回的json生成,使用工具根据 json 转 go struct
type HuoShanDictRequest struct {}
type HuoShanDictResponse struct {}

// step0: 代码生成器生成请求模拟函数,封装成函数
func HuoShanQuery(word string) {

    // step1: 生成http客户端
    client := &http.Client{}

    // step2: 使用结构体封装请求
    request := HuoShanDictRequest{Language: "en", Text: word}
    buf, _ := json.Marshal(request)

    // step3: 初始化请求(自动生成)
    data := bytes.NewReader(buf)
    req, _ := http.NewRequest("POST", url, data)

    // step4 :设置请求头(自动生成略)
    req.Header.Set("Connection", "keep-alive")
    // ...

    // step5: 执行请求,获取相应(自动生成略)
    resp, _ := client.Do(req)

    // step6: 读出响应体(自动生成略)
    bodyText, _ := ioutil.ReadAll(resp.Body)

    // step7 : 异常分支,校验状态码
    if resp.StatusCode != 200 {
            log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
    }

    // step8 : 结构体封装响应
    var huoShanDictResponse HuoShanDictResponse
    _ = json.Unmarshal(bodyText, &huoShanDictResponse)

    // step9: 输出略
}

// 第二种搜索引擎,实现过程参照上一种即可
type CaiYunDictRequest struct {}
type CaiYunDictResponse struct {}

func CaiYunQuery(word string) {}

func main() {
    if len(os.Args) != 2 {
        // 获取启动参数
        fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
        `)
        os.Exit(1)
    }
    word := os.Args[1]

    // 用于阻塞等待的结构,Add添加任务数,Done减少任务数,查看源码发现Done的本质是Add(-1)
    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
        HuoShanQuery(word)
        wg.Done()
    }()

    go func() {
        CaiYunQuery(word)
        wg.Done()
    }()
    // 等待任务数清零结束
    wg.Wait()
}


执行结果

image.png


SOCKS5代理


描述

实现一个简易不需要认证的socks5代理服务器


知识点

soks5协议的工作原理

正常浏览器不经过代理访问网站,会想和对方服务器建立TCP连接,执行三次握手建立HTTP请求,然后交互

设置代理之后,流程变为用户先和代理服务器建立连接,然后代理服务器再和实际服务器建立连接,然后用户和代理进行交互,代理和实际服务器交互,代理再负责转发用户和实际服务器的数据。

可以分成四个阶段:握手阶段、认证阶段、请求阶段、relay阶段。

image.png


第一阶段 - 握手

浏览器会向socks5代理发送请求,包的内容包括一个协议的版本号,支持的认证种类,socks5服务器会选中一个认证方式,返回给浏览器。

第二阶段 - 认证

如果socks5服务器返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程。

本次简单实现一个不需要认证的代理,就不对如认证赘述了。

第三阶段 - 请求

认证过后浏览器会和对代理发送请求,主要信息包扣版本号,请求类型,主机,ip地址端口号等等。

这代表着如果代理截取到这些数据,就可以确定实际请求服务器,代理就可以和实际服务器建立TCP连接,返回响应。

第四个阶段 - relay

此时浏览器会发送一些请求获取数据,代理收到请求会转发到实际的服务器上,实际服务器返回响应也会发送到代理上,由代理转发到浏览器。


核心代码

package main

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

// 常量,
const (
	socks5Ver = 0x05
	cmdBind   = 0x01
	atypIPV4  = 0x01
	atypeHOST = 0x03
	atypeIPV6 = 0x04
)

func main() {
	server, err := net.Listen("tcp", "127.0.0.1:1080")
	// 异常处理略
	for {
		client, err := server.Accept()
		// 异常处理略
		go process(client)
	}
}

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	err := auth(reader, conn)
	// 异常处理略
	err = connect(reader, conn)
	// 异常处理略
}

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, _ := reader.ReadByte()
	// 异常处理略
	methodSize, _ := reader.ReadByte()
	method := make([]byte, methodSize)
	_, _ = io.ReadFull(reader, method)

	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	_, _ = conn.Write([]byte{socks5Ver, 0x00})
	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)
	_, _ = io.ReadFull(reader, buf)
	ver, cmd, atyp := buf[0], buf[1], buf[3]
	// 异常处理,略
	addr := ""
	switch atyp {
	case atypIPV4:
		_, _ = io.ReadFull(reader, buf)
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		hostSize, _ := reader.ReadByte()
		host := make([]byte, hostSize)
		_, _ = io.ReadFull(reader, host)
		addr = string(host)
	default:
		return errors.New("invalid atyp or no supported yet")
	}
	_, _ = io.ReadFull(reader, buf[:2])
	port := binary.BigEndian.Uint16(buf[:2])

	dest, _ := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
	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
	_, _ = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

        // 从用户读数据到host
	go func() {
		_, _ = io.Copy(dest, reader)
		cancel()
	}()
        // 从host读数据到用户
	go func() {
		_, _ = io.Copy(conn, dest)
		cancel()
	}()
        
        // 阻塞等待上述两个协程任意一个触发cancel,结束程序
	<-ctx.Done()
	return nil
}

小结

三个小实验,知识面覆盖到了golang基础语法,io处理,http请求,json解析,并发编程等多个知识面。

对于刚接触go语言的人来说,有不小的挑战,也非常的有意义,希望能给看到的人带来一定的帮助。

同时,在整理的代码中,省略了一些不必要的流程,还需要读者自己具体实验。