GO语言工程实践课后作业(一):实现思路、代码以及路径记录| 豆包MarsCode AI 刷题

197 阅读14分钟

1.猜数字

1.1 整个游戏逻辑分为三块:
  1. 设置数字
  2. 用户输入数字
  3. 比较数字大小 先贴一个完整代码
package main
import (
   "bufio"
   "fmt"
   "math/rand"
   "os"
   "strconv"
   "strings"
   "time"
)
func main(){
   maxnum := 100
   rand.Seed(time.Now().UnixNano())
   secrenumber :=  rand.Intn(maxnum)
   // fmt.Println("the maxnum is :",secrenumber)
   for {
   	// 读入
   	fmt.Println("please input your guess num")
   	reader := bufio.NewReader(os.Stdin)
   	input,err := reader.ReadString('\n')
   	if err != nil {
   		fmt.Println("an error occured while reading input. please try again",err)
   		continue
   	}
   	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("your guess num is :",guess)
   	// 判断
   	if guess == secrenumber {
   		fmt.Println("you win!")
   		return
   	}else if guess > secrenumber{
   		fmt.Println("your guess num is bigger than this num")
   		continue
   	}else {
   		fmt.Println("your guess num is smaller than this num")
   		continue
   	}
   	}
}
1.2设置数字

设置数字最重要的一点就是游戏设置数字是随机的,我们怎么保证数字是随机的呢,我们知道随机数种子固定,那么生成的随机数就是固定的。 所以我们就用时间的纳秒级数字来设定随机数种子,这样可以实现某种意义上的真随机数

rand.Seed(time.Now().UnixNano())

1.3 读入用户输入流

bufio包是对IO的封装,可以操作文件等内容,同样可以用来接收键盘的输入,此时对象不是文件等,而是os.Stdin,也就是标准输入设备

1.3.1 输入流

reader := bufio.NewReader(os.Stdin) 这行代码简单来说就是读入标准输入流os.Stdin然后赋值给reader对象

input,err := reader.ReadString('\n') bufio.Reader 对象的 ReadString 方法,输入流中读取数据,直到遇到指定的分隔符(在这个例子中是换行符 \n)为止。读取的数据作为字符串返回,并存储在 input 变量中(ai解释的很好,直接搬过来)

简单案例_image_1.webp

1.3.2 字符串分割,去除多余字符,以便得到纯净数字

input = strings.Trim(input, "\r\n") 这行代码的作用是去除字符串 input 两端的回车符(\r)和换行符(\n)。

strings.Trim(s, cutset string) string:这个函数会返回一个新的字符串,它是通过移除原始字符串 s 开头和结尾处的所有 cutset 中的字符得到的。如果 cutset 为空,则会移除字符串 s 开头和结尾处的所有空白字符。 在不同的操作系统中,文本文件的行结束符可能不同。在 Windows 系统中,通常使用 \r\n 作为行结束符;而在 Unix/Linux 系统中,通常使用 \n 作为行结束符。

结合上下文,这行代码的目的是确保用户输入的字符串是干净的,没有多余的空白字符,特别是在处理用户输入时,去除这些字符可以避免因输入格式不一致而导致的问题。

简单案例_image_2.webp guess,err := strconv.Atoi(input) 这个代码就是把input的内容转化为整形变量,类型一致才好对比

1.4 课后作业 使用fmt.Scanf简化代码实现

func Scanf(format string, a ...interface{}) (n int, err error) 使用scanf可以直接读取整数,不用再转化了,但是在使用时要注意,格式化输入为("%d \n")

 package main
import (
    // "bufio"
    "fmt"
    "math/rand"
    // "os"
    // "strconv"
    // "strings"
    "time"
)
func main(){
    maxnum := 100
    rand.Seed(time.Now().UnixNano())
    secrenumber :=  rand.Intn(maxnum)
    // fmt.Println("the maxnum is :",secrenumber)
    
    for {
        fmt.Println("please input your guess num")
        var input_num int
        
        _,err := fmt.Scanf("%d \n",&input_num)
        if err != nil{
            fmt.Println("an error occured while reading input. please try again:",err)
            continue
        }
        guess := input_num
        fmt.Println("your guess num is :",guess)
        
        // 判断
        if guess == secrenumber {
            fmt.Println("you win!")
            return
        }else if guess > secrenumber{
            fmt.Println("your guess num is bigger than this num")
            continue
        }else {
            fmt.Println("your guess num is smaller than this num")
            continue
        }
        }
}

2.在线词典

2.1 请求与响应
  1. 创建请求结构体
  2. 序列化请求
  3. 创建响应结构体
  4. 获取响应并反序列化

本次小项目是一个在线词典,实际上就是请求+响应,网络编程的内容,不过我也没怎么学过,只是大致了解。

先贴代码,下面解析抓包信息

package main
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    // "strings"
)
// 根据之前的json或者抓包的格式构建一个字典请求结构体
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 {
    } `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) {
    // 使用 http.Client 结构体创建一个 HTTP 客户端实例,用于发送 HTTP 请求
    client := &http.Client{}
    // 创建一个 JSON 格式的字符串,包含了翻译的类型和要翻译的文本
    // var data = strings.NewReader(`{"trans_type":"en2zh","source":"nice"}`)
    request := DictRequest{TransType: "en2zh",Source:word}
    // 使用 json.Marshal 将 DictRequest 实例编码为 JSON 格式的字节数组
  buf,err := json.Marshal(request)
    if err!= nil{
      log.Fatal(err)

    }
    // fmt.Println("buf:",buf)
    // 创建一个 bytes.Reader 对象,用于读取 JSON 数据
    var data = bytes.NewReader(buf)
    // fmt.Println("data:",data)
    // 使用 http.NewRequest 函数创建一个 HTTP POST 请求,请求的 URL 是彩云小译的 API 地址,请求的主体是data
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set("accept", "application/json, text/plain, */*")
    //此处省略n多


    // 使用 client.Do 方法发送请求,并接收响应
    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)
    }
    // 检查状态码
    if resp.StatusCode !=200{
        log.Fatal("没有得到正确响应,响应码为:",resp.StatusCode,"body:",string(bodyText))
    }
    // fmt.Printf("%s\n", bodyText)


    // 定义响应结构体dictResponse接收bodyText的内容
    var dictResponse DictResponse

    // 解析 JSON 编码的数据,并将结果存储在 v 指向的值中。如果 v 为 nil 或不是指针,则返回 [InvalidUnmarshalError]
    // 所以这里将json解码反序列化后存储到响应结构体dictresponse中
    err = json.Unmarshal(bodyText,&dictResponse)
    if err !=nil{
        log.Fatal(err)
    }

    // fmt.Printf("%#v\n",dictResponse)
    fmt.Println("UK:",dictResponse.Dictionary.Prons.En,"US:",dictResponse.Dictionary.Prons.EnUs)
    fmt.Println("反义词:")
    for _,words := range dictResponse.Dictionary.Antonym{
        fmt.Println(words)
    }
    fmt.Println("中文释义:")
    for _,item := range dictResponse.Dictionary.Explanations{
        fmt.Println((item))
    }
}
func main(){
    if len(os.Args) != 2{
        fmt.Fprintf(os.Stderr,`usage: simpleDict WORD
example: simpleDict hello       `)
    os.Exit(1)
    }
    word := os.Args[1]
    query(word)
}
2.2 抓包解析

抓包看到状态码200的dict就是了,在这里可以看到请求体内容,我们就根据这里的键值对去建立请求体结构体,复制为curl(bash)然后去网站转go语言代码就能得到请求体的内容 简单案例_image_3.png

查看这个预览和响应,我们可以看到他们的内容,这些就是我们响应体所需要的内容,在这里可以看到响应体内容,我们就根据这里的键值对去建立响应体结构体

简单案例_image_4.png

简单案例_image_5.png

最终结果:

简单案例_image_6.png

2.3 课后作业

完整代码贴一下 增加了调用百度API以及并行运行

package main
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strings"
    "sync"
)
// 根据之前的json或者抓包的格式构建一个字典请求结构体
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 {
    } `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) {

    // 使用 http.Client 结构体创建一个 HTTP 客户端实例,用于发送 HTTP 请求
    client := &http.Client{}
    // 创建一个 JSON 格式的字符串,包含了翻译的类型和要翻译的文本
    // var data = strings.NewReader(`{"trans_type":"en2zh","source":"nice"}`)

    request := DictRequest{TransType: "en2zh",Source:word}

    // 使用 json.Marshal 将 DictRequest 实例编码为 JSON 格式的字节数组
    buf,err := json.Marshal(request)

    if err!= nil{
        log.Fatal(err)

    }
    // fmt.Println("buf:",buf)

    // 创建一个 bytes.Reader 对象,用于读取 JSON 数据
    var data = bytes.NewReader(buf)
    // fmt.Println("data:",data)

    // 使用 http.NewRequest 函数创建一个 HTTP POST 请求,请求的 URL 是彩云小译的 API 地址,请求的主体是data
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)

    if err != nil {

        log.Fatal(err)
    }

    req.Header.Set("accept", "application/json, text/plain, */*")

    req.Header.Set("accept-language", "zh")

    // 使用 client.Do 方法发送请求,并接收响应
    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)
    }
    // 检查状态码
    if resp.StatusCode !=200{
        log.Fatal("没有得到正确响应,响应码为:",resp.StatusCode,"body:",string(bodyText))

    }
    // fmt.Printf("%s\n", bodyText)
    // 定义响应结构体dictResponse接收bodyText的内容

    var dictResponse DictResponse

 
    // 解析 JSON 编码的数据,并将结果存储在 v 指向的值中。如果 v 为 nil 或不是指针,则返回 [InvalidUnmarshalError]
    // 所以这里将json解码反序列化后存储到响应结构体dictresponse中
    err = json.Unmarshal(bodyText,&dictResponse)
    if err !=nil{
        log.Fatal(err)
    }

    // fmt.Printf("%#v\n",dictResponse)
    fmt.Println("UK:",dictResponse.Dictionary.Prons.En,"US:",dictResponse.Dictionary.Prons.EnUs)
    fmt.Println("反义词:")
    for _,words := range dictResponse.Dictionary.Antonym{
        fmt.Println(words)
    }
    fmt.Println("来自彩云的中文释义:")
    for _,item := range dictResponse.Dictionary.Explanations{
        fmt.Println((item))
    }
}

func baid_quey(word string){
    type Baidu_Response struct {
        Errno int `json:"errno"`
        Data []struct {
            K string `json:"k"`
            V string `json:"v"`
        } `json:"data"`
        Logid int `json:"logid"`
    }
    client := &http.Client{}
        // 将变量格式化成json形式
        queryString := fmt.Sprintf(`kw=%s`, word)
        var data = strings.NewReader(queryString)
        req, err := http.NewRequest("POST", "https://fanyi.baidu.com/sug", data)
        if err != nil {
            log.Fatal(err)
        }
        req.Header.Set("Accept", "*/*")
        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")

        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)
        }
        // fmt.Printf("%s\n", string(bodyText))
        var response Baidu_Response
        err = json.Unmarshal(bodyText,&response)
        if err!= nil{
            log.Fatal(err)
        }
        fmt.Println("来自百度的中文释义:",response.Data[0])

    }

func main(){

    if len(os.Args) != 2{
        fmt.Fprintf(os.Stderr,`usage: simpleDict WORD
example: simpleDict hello       `)
    os.Exit(1)
    }
    word := os.Args[1]

    // 加锁
    var wg sync.WaitGroup
    wg.Add(2)
    go func()  {
        defer wg.Done()
        query(word)
    }()
    go func()  {
        defer wg.Done()
        baid_quey(word)
    }()
    wg.Wait()

}

因为百度API和彩云的不同,只有一个参数,所以就不定义结构体了,使用一个变量存储下来调用就可以了,因为返回响应的差别,所以结构体定义稍有不同,然后.....没什么好说的,其他步骤就是照猫画虎

简单案例_image_7.webp

2.4 并发执行

这里用到了课程的知识点,为了不让主程序结束就退出,使用了sync.WaitGroup,因为是两个协程并发执行,所以输出有可能会出现穿插情况,不过无伤大雅

// 加锁
    var wg sync.WaitGroup
    wg.Add(2)
    go func()  {
        defer wg.Done()
        query(word)
    }()
    go func()  {
        defer wg.Done()
        baid_quey(word)
    }()
    wg.Wait()

运行结果

简单案例_image_8.png

3.socket5代理

3.1 socks5协议
3.1.1 socks5是什么

socks5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源,暴露一个 socks5 协议的端口,让用户通过这个端口去访问某些资源 简单来说socks5可以理解为一个转发服务器

3.1.2 socks5原理

浏览器和 socks5 代理建立 TCP 连接,随后代理和真正的服务器建立 TCP 连接。

  1. 握手阶段
  2. 认证阶段
  3. 请求阶段
  4. relay(中转)阶段

1.握手阶段。浏览器向 socks5 代理发送请求,包的内容包括一个协议的版本号,以及支持的认证的种类。

2.认证阶段。socks5 服务器选中一个认证方式,返回给浏览器。 若返回的是 00 则无需认证,返回其他类型开始会开始认证流程

3.请求阶段,认证通过后,浏览器向 socks5 服务器发起请求。 主要信息包括版本号,请求的类型,一般主要是 connection 请求,代表代理服务器和某个域名或某个 IP 地址某个端口建立 TCP 连接。

代理服务器收到响应之后,与真正后端服务器建立连接,返回一个响应。

4.relay 阶段。 在这个阶段,浏览器发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。真正的服务器返回响应,代理服务器再将请求转发到浏览器。

简单来说socks5可以理解为一个转发服务器

3.2 TCP服务器简单实现

net.Conn 的工作原理:conn 是一个实现了 net.Conn 接口的对象,net.Conn 代表的是一个网络连接,既实现了 io.Reader 接口(读取数据)又实现了 io.Writer 接口(写入数据),所以它能够在网络通信中接收(输入)数据,也发送(输出)数据。

3.2.1 认证阶段

客户端向socks5服务器发送的认证请求数据包 **Client ----> socks5 Server **

|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

代码步骤: 1.读取客户端请求的 Socks5 协议版本。 2.检查客户端版本是否为 0x05,否则返回错误。 3.获取客户端支持的认证方法数量 NMETHODS。 4.读取客户端提供的 METHODS 列表(即具体的认证方法)。 5.响应客户端,告知使用的认证方法(0x00无需认证)。

认证函数 func auth(reader *bufio.Reader,conn net.Conn) (err 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)
    }
    log.Println("ver:",ver,"method:",method)

socks服务器获取报文无错误后,向客户端发送响应进行确认 **socks5 Server ----> Client **

VERMETHOD
1    1    

第一个是版本号,第二个是方法,方法为0x00则不需要认证

    _,err = conn.Write([]byte{socks5Ver,0x00})
    if err != nil{
        return fmt.Errorf("write failed:%w",err)
    }
3.2.2 请求阶段

请求阶段报文

**Client ----> socks5 Server **

VERCMD RSV  ATYPDST.ADDRDST.PORT
1   1  X'00' 1  Variable   2    
  1. VER 版本号,socks5的值为0x05
  2. CMD 0x01表示CONNECT请求
  3. RSV 保留字段,值为0x00
  4. ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。0x01表示IPv4地址,DST.ADDR为4个字节.0x03表示域名,DST.ADDR是一个可变长度的域名
  5. DST.ADDR 一个可变长度的值
  6. DST.PORT 目标端口,固定2个字节

函数func connect(reader *bufio.Reader,conn net.Conn) (err error)

    buf := make([]byte,4)
    _,err = io.ReadFull(reader,buf)
    if err!= nil{
        return fmt.Errorf("read header failed:%w",err)
    }
    log.Printf("after auth : the len of connect buf:%v ,buf:%v,%v,%v,%v",len(buf),buf[0],buf[1],buf[2],buf[3])

    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 atype failed:%w",err)
        }
        addr = fmt.Sprintf("%d.%d.%d.%d",buf[0],buf[1],buf[2],buf[3])
    case atypeHOST:
        // 如果地址类型是域名(0x03),程序会先读取一个字节,表示域名的长度,然后根据该长度读取域名
        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:not 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])
    // Dial 连接到指定网络上的地址
    dest,err := net.Dial("tcp",fmt.Sprintf("%v:%v",addr,port))
    if err!=nil{
        return fmt.Errorf("dial dest dailed:%w",err)
    }
    defer dest.Close()
    // 代理服务器与目标IP建立连接
    log.Println("dial:",addr,port)

socks5服务器向客户端发送 SOCKS5 协议响应报文,确认目标地址连接请求成功 **socks5 Server ----> Client **

|VER | STATUS | RSV   | BND.ADDR | BND.PORT | |:----:|:----:|:----:|:----:|:----:|:----:| | 1  |   1    |  1    |    4     |    2     |

  1. VER (1 byte): 协议版本号,这里值为 0x05,表示使用 SOCKS5 协议。
  2. STATUS (1 byte): 连接状态码,0x00 表示连接成功。
  3. RSV (1 byte): 保留字段,必须为 0x00。
  4. BND.ADDR (4 bytes): 绑定地址。0.0.0.0,表示没有特定的绑定地址。
  5. BND.PORT (2 bytes): 绑定端口。对于 SOCKS5 的连接请求响应,0x0000表示没有特定的绑定端口。
_,err = conn.Write([]byte{0x05,0x00,0x00,0x01,0,0,0,0,0,0})
    if err!= nil{
        return fmt.Errorf("write failed:%w",err)
    }
3.2.3 relay中转阶段

通过Conn流进行输入输出

  1. **Client ----> socks5 Server ----> Real Server **
  2. **Real Server ----> socks5 Server ----> Client **

从客户端(reader)中的数据复制并发送到 dest中 将目标流dest中的数据通过 TCP 连接(conn)发送给客户端

//ctx 用于在程序中传播取消信号。cancel函数将在适当的时机被调用,以取消所有使用此上下文的 goroutine
ctx,cancel :=  context.WithCancel(context.Background())
    defer cancel()
    go func ()  {
        // 从reader 中的数据复制并发送到 dest中
        _,_ = io.Copy(dest,reader)
        cancel()
    }()
    go func() {
        // 将目标流dest中的数据通过 TCP 连接(conn)发送给客户端
        _,_ = io.Copy(conn,dest)
        cancel()
    }()
    // 阻塞,等待发送完才会结束
    <-ctx.Done()

io.Copy(conn, dest) 将目标流(dest)中的数据传输回 TCP 连接(conn),也就是将代理服务器(socks5)从目标服务器接收到的数据返回给客户端

3.3 总结
  • 客户端到代理服务器:客户端向代理服务器发送数据,认证成功后,进行通信。
  • 代理服务器处理:代理服务器处理请求,将数据从 conn(客户端)通过 reader 读取,经过一些处理后,通过 dest 缓存或传输数据到目标服务器。
  • 目标服务器到客户端:代理服务器将 dest 中的数据通过 conn 写回给客户端,实现数据的双向转发