1.猜数字
1.1 整个游戏逻辑分为三块:
- 设置数字
- 用户输入数字
- 比较数字大小 先贴一个完整代码
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解释的很好,直接搬过来)
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 作为行结束符。
结合上下文,这行代码的目的是确保用户输入的字符串是干净的,没有多余的空白字符,特别是在处理用户输入时,去除这些字符可以避免因输入格式不一致而导致的问题。
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 请求与响应
- 创建请求结构体
- 序列化请求
- 创建响应结构体
- 获取响应并反序列化
本次小项目是一个在线词典,实际上就是请求+响应,网络编程的内容,不过我也没怎么学过,只是大致了解。
先贴代码,下面解析抓包信息
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语言代码就能得到请求体的内容
查看这个预览和响应,我们可以看到他们的内容,这些就是我们响应体所需要的内容,在这里可以看到响应体内容,我们就根据这里的键值对去建立响应体结构体
最终结果:
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和彩云的不同,只有一个参数,所以就不定义结构体了,使用一个变量存储下来调用就可以了,因为返回响应的差别,所以结构体定义稍有不同,然后.....没什么好说的,其他步骤就是照猫画虎
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()
运行结果
3.socket5代理
3.1 socks5协议
3.1.1 socks5是什么
socks5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源,暴露一个 socks5 协议的端口,让用户通过这个端口去访问某些资源 简单来说socks5可以理解为一个转发服务器
3.1.2 socks5原理
浏览器和 socks5 代理建立 TCP 连接,随后代理和真正的服务器建立 TCP 连接。
- 握手阶段
- 认证阶段
- 请求阶段
- 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 **
VER METHOD 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 **
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个字节
函数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 |
- VER (1 byte): 协议版本号,这里值为 0x05,表示使用 SOCKS5 协议。
- STATUS (1 byte): 连接状态码,0x00 表示连接成功。
- RSV (1 byte): 保留字段,必须为 0x00。
- BND.ADDR (4 bytes): 绑定地址。0.0.0.0,表示没有特定的绑定地址。
- 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流进行输入输出
- **Client ----> socks5 Server ----> Real Server **
- **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写回给客户端,实现数据的双向转发