DAY1 实战小项目(附简易漫画爬取下载器) | 青训营笔记

120 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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 库及配套标准库,那么其实不只是简易词典,我们还可以做更多有趣的东西:

漫画下载器

这次下手的对象是这个漫画网站:

image.png

虽然网站本身提供了下载 api,但是有相关限制。所以用的是其它野生接口,平台本身部分数据做了 AES 加密,解密后可获得相关数据

照葫芦画瓢的,我们可以改为向漫画网站发起搜索请求,解析返回的搜索结果数据,用户选择漫画后再次向网站请求获取响应,得到漫画数据并摘取每一页的图片下载至本地。当然,既然 go 语言有如此优秀的并发能力,我们下载也会并发拉取图片

image.png

image.png

和上面的小项目类似的,这个项目也写了很多结构体类型便于解析响应,并且在判断响应的 http status 时,若非 200 OK 则会进行至多三次的重新下载,保证图片尽可能下载成功

最终效果:

image.png

image.png

该学习项目源码地址为: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 主要是中间人与主机之间的传输协议,代理主机的请求并使用正确格式传回主机

关于协商

在老师的示例代码中写的很清楚,首先主机要和中间人统一相关方法:

VERNMETHODSMETHODS
111 to 255

VER: 协议版本,socks5为0x05

NMETHODS: 支持认证的方法数量

METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:

  • X’00’ NO AUTHENTICATION REQUIRED
  • X’02’ USERNAME/PASSWORD

收到主机的请求,中间人选择一种 METHOD 返回,之后会根据选择的 METHOD 进行相应验证操作,而本例中返回的是 0x00,不需要验证操作

VERMETHODS
11

与远程服务器之间的连接

在协商完毕之后,主机就可以发送代理请求了,格式如下:

VERCMDRSVATYPDST.ADDRDST.PORT
11X'00'1Variable2

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 也会在与远程服务器成功建立连接后返回:

VERREPRSVATYPBND.ADDRBND.PORT
11X'00'1Variable2

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.ADDRBND.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 出现问题都会提前中止操作。

最后测试一下:

image.png

参考:

理解socks5协议的工作过程和协议细节 | Bigbyto (wiyi.org)

深入理解Golang之context - 知乎 (zhihu.com)