这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
关于 Go 语言基础不做过多赘述,笔记只记录相关实战课程项目
今日实战项目总共三个:
- 猜数字游戏
- 简易词典(类似于这个的,做了一个漫画下载器)
- 代理(SOCKS5)
前两个可能就洒洒水,最后一个相对来说难度最高
小项目
猜数字
大致流程:
- 随机数生成
- 获取用户输入
- 比较并判断
- 若相等则退出,否则又从第二步开始执行
代码地址:go-by-example/main.go at master · wangkechun/go-by-example (github.com)
主要有两个点需要注意:
首先是随机数的生成,假如不设置 seed,每次执行都会是相同结果。这里采用了比较常见的取时间戳当 seed,如果为了更好的随机性还可以获取对应硬件的相关数据用作 seed。
其次是关于读入方式,老师解释是由于后续课程中会频繁使用 bufio 所以提前使用混个眼熟,当然改成 fmt.Scan 等类似方法会更简单,比如:
var num int
n, err := fmt.Scan(&num)
n 为读入变量个数,方法参数必须使用指针保证读取到的值正确写入变量
简易词典
还是先分析一遍流程:
- 获取用户输入的文字
- 将文字放入请求参数中,向翻译网站发送请求
- 获取响应并解析
- 打印结果
可以看到,核心步骤就是二三步,只要做好请求收发问题就迎刃而解了
代码地址:go-by-example/main.go at master · wangkechun/go-by-example (github.com)
感谢 go 丰富的标准库,json 的序列化反序列化都相当轻松,我们要做的就是根据 json 格式创建合适的结构体。项目代码中的结构体声明中出现了类似这样的片段:
type T struct {
A struct{
B int
C string
}
}
这实际上是匿名结构体,虽然结构看上去更紧凑了,但是在需要使用匿名结构体的类型时只能将其拆分
另外,我们看见在请求成功后有一行 if resp.StatusCode != 200,这也是为了防止出现没能成功获取到正确响应的情况,在写爬虫的时候这种情况可能会多一些,高频请求触发网站的保护机制可能会导致各种奇奇怪怪的响应错误。
既然 go 为我们提供了如此方便的 http 库及配套标准库,那么其实不只是简易词典,我们还可以做更多有趣的东西:
漫画下载器
这次下手的对象是这个漫画网站:
虽然网站本身提供了下载 api,但是有相关限制。所以用的是其它野生接口,平台本身部分数据做了 AES 加密,解密后可获得相关数据
照葫芦画瓢的,我们可以改为向漫画网站发起搜索请求,解析返回的搜索结果数据,用户选择漫画后再次向网站请求获取响应,得到漫画数据并摘取每一页的图片下载至本地。当然,既然 go 语言有如此优秀的并发能力,我们下载也会并发拉取图片
和上面的小项目类似的,这个项目也写了很多结构体类型便于解析响应,并且在判断响应的 http status 时,若非 200 OK 则会进行至多三次的重新下载,保证图片尽可能下载成功
最终效果:
该学习项目源码地址为:PICOF/CopyComicDownloader
代理(SOCKS5)
代码地址:go-by-example/main.go at master · wangkechun/go-by-example (github.com)
关于 SOCKS5
socks是一种互联网协议,它通过一个代理服务器在客户端和服务端之间交换网络数据。简单来说,它就是一种代理协议,扮演一个中间人的角色,在客户端和目标主机之间转发数据。
不代理时的 HTTP 请求流程
-
建立 TCP 连接
主机首先与服务器建立 TCP 连接方便后续数据传输
-
发起 HTTP 请求
主机通过建立的 TCP 连接发送请求
-
服务器返回响应
使用 SOCKS5 代理的请求流程
-
主机和 SOCKS5 代理建立 TCP 连接
存在代理的情况下,主机只负责与中间代理进行交互,对于远程服务器的请求由中间代理完成
-
协商阶段
在主机正式向 SOCKS5 服务器发起请求之前,双方需要协商,包括协议版本,支持的认证方式等,双方需要协商成功才能进行下一步。
-
请求阶段
协商成功后,主机向socks5代理发起一个请求,请求中包含主机要访问服务器的地址端口等信息
-
SOCKS5 relay 阶段
SOCKS5收到浏览器请求后,解析请求内容,然后向目标服务器建立TCP连接。
-
数据传输阶段
一条由主机到 SOCKS5 代理到远程服务器的连接被成功建立,可以在此基础上传输信息了
所以,SOCKS5 主要是中间人与主机之间的传输协议,代理主机的请求并使用正确格式传回主机
关于协商
在老师的示例代码中写的很清楚,首先主机要和中间人统一相关方法:
| 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
收到主机的请求,中间人选择一种 METHOD 返回,之后会根据选择的 METHOD 进行相应验证操作,而本例中返回的是 0x00,不需要验证操作
| VER | METHODS |
|---|---|
| 1 | 1 |
与远程服务器之间的连接
在协商完毕之后,主机就可以发送代理请求了,格式如下:
| 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个字节
本着礼尚往来的思想,SOCKS5 也会在与远程服务器成功建立连接后返回:
| VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|---|---|---|---|---|---|
| 1 | 1 | X'00' | 1 | Variable | 2 |
VER socks版本,这里为0x05
REP Relay field,内容取值如下
- X’00’ succeeded
- X’01’ general SOCKS server failure
- X’02’ connection not allowed by ruleset
RSV 保留字段
ATYPE 地址类型
BND.ADDR 服务绑定的地址
BND.PORT 服务绑定的端口
由于我们主机和代理是同一台,BND.ADDR 和 BND.PORT 就设置为 0
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
关于这段代码,dest 是中间人与远程建立的连接,reader 读取浏览器的发送的数据,conn 是与浏览器建立的连接。这一段代码实现了数据双向交换,并且任意一个 go routine 出现问题都会提前中止操作。
最后测试一下:
参考: