这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
三个实践项目
猜数字使用了随机数生成/标准IO流/字符串处理/for循环
字典使用了HTTP请求/JSON解析/代码生成
socks5代理使用了net网络连接/goroutine/context机制等
1. 猜数字
1.1 生成随机数
math/rand包实现了伪随机数生成器。
随机数从资源生成。同一包水平的函数都使用的默认的公共资源。该资源会在程序每次运行时都产生确定的序列。如果需要每次运行产生不同的序列,应使用Seed函数进行初始化。
maxNum := 100
rand.Seed(time.Now().UnixNano())//用时间戳来初始化随机数种子
secretNumber := rand.Intn(maxNum)
1.2 bufio实现标准IO流的处理和strings字符串处理的应用
bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。
reader := bufio.NewReader(os.Stdin)//将标准stdin文件转为一个只读的带缓冲的流
input, err := reader.ReadString('\n')//ReadString读取直到第一次遇到指定字节,返回一个包含已读取的数据和delim字节的字符串
if err != nil {//当且仅当ReadString方法返回的切片不以指定字节结尾时,会返回一个非nil的错误
fmt.Println("An error occured while reading input. Please try again", err)
return
}
input = strings.TrimSuffix(input, "\n")//返回将s后端指定字符包含的utf-8码值都去掉的字符串。windows系统为\r\n
guess, err := strconv.Atoi(input)//Atoi是ParseInt(s, 10, 0)的简写,字符串转换为10进制整数
if err != nil {//返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange
fmt.Println("Invalid input. Please enter an integer value")
return
}
1.3 for循环实现功能逻辑
实现思路1
- 设置死循环,当猜测命中时break跳出循环
实现思路2
- 将不命中设置为循环条件,此时需要注意guess变量需要在循环体外声明
2.词典
2.1 HTTP请求
HTTP Web API 的 CLI 封装
client := &http.Client{}//开启一个http客户端(可指定参数如timeout),用于管理HTTP客户端的头域、重定向策略和其他设置
data =strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)//创建一个post请求,设置请求类型/url/数据读取流
/*
设置请求头
*/
resp, err := client.Do(req)//发起请求,返回结果保存到变量response
defer resp.Body.Close()//为防止资源泄露用defer设置关流,函数结束之后从下往上触发
bodyText, err := ioutil.ReadAll(resp.Body)//读取响应
if resp.StatusCode != 200 {//确认返回状态正常,否则后续结果为空难以排查bug
log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
2.2 JSON解析
通过设置结构体DictRequest和DictResponse用于对请求的输入输出流进行序列化和反序列化
2.3 代码生成
使用curlconverter.com/go/
可将curl命令及结果转换为go代码
使用oktools.net/json2go 可将JSON转换为go的结构体
3. socks5代理服务器
3.1 socks5原理
四阶段:协商——认证——请求——relay
此处实现简易版本,是明文传输没有加密,省略认证阶段
3.2 TCP echo server 实现
net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。
可通过Dial、Listen和Accept等函数提供的基本接口和相关的Conn(代表通用的面向流的网络连接)和Listener接口实现简易服务器设置。
go 启动一个子线程
nc 用于对网络连接进行调试和探测
3.3 auth协商阶段
客户端发送代理请求
+----+----------+----------+
|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和METHODS回复给客户端
此时客户端收到服务端的响应请求后,双方握手完成,开始进行协议交互
3.4 connect请求阶段
与auth阶段的函数签名一致,传入只读流和连接
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
- VER 版本号,socks5的值为0x05
- CMD 代理指令
- 0x01表示CONNECT请求,TCP代理时使用
- RSV 保留字段,值为0x00
- ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
- 0x01表示IPv4地址,DST.ADDR为4个字节
- 0x03表示unix域socket类型代理域名,DST.ADDR是一个可变长度的域名
- 0x04: IPv6地址类型
- DST.ADDR 需要连接的目的地址
- DST.PORT 目标端口,固定2个字节
读取到目标地址和端口后使用net.Dial函数与host建立连接
3.5 response 响应阶段
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
其中VER/RSV/ATYP的含义同上,其他字段的意思:
- REP Relay field,内容取值如下
- X’00’ succeeded
- X’01’-X’08’: 失败
- X’09’-X’ff’: 未使用
- BND.ADDR 连接到的远程地址
- BND.PORT 服务绑定的端口
- 0x00: 成功
3.6 双向数据传输
底层服务器与用户浏览器
使用io.copy同时开两个goroutine
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
等待任一方copy失败,某一个连接关闭了才结束程序,使用context机制等待一个cancel被调用才结束。