这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。
在线字典
这个小应用的目标是用户输入单词,可以返回该单词的音标及中文解释,效果如下。
实现上并不是去搭建一套完整的字典系统,其实就是通过各大在线翻译网站去拿到其获取数据的接口,然后在 go 对该接口发起请求来获取数据。
可能涉及到的技术点有:网络请求、json 解析、并发编程。下面就简单分步介绍实现方式。
- 找到翻译网站上获取字典数据的接口 这一步要善用 F12 中的网络,目前我也不知道有什么比较好的方式可以快速找到。我的方式是打开 network 中的 fetch 标签,这里面往往都是获取数据的请求。
然后通过“人肉搜索”的方式自己找一下需要的数据(一般这里面请求不会太多,放心)。比如彩云小译这里,我们就可以很轻松地找到想要的数据,也就是下图中 dict 里的 explanations 和 prons。
- 写请求数据的代码 因为这种网络请求的代码如果没有什么定制化要求,大都比较通用。这一步可以使用一些现有的工具来得到代码。 比如使用这个 curlconverter 工具 来生成网络请求代码。我们复制浏览器上的 curl 请求,然后用该工具即可得到代码。
得到的代码我就不放了,可以自行尝试。
- 解析数据 上一步请求到的数据往往并非我们最终想要的,为此还需要解析数据。一般请求到的数据都是 json 格式,因此我们在 go 代码中一般使用结构体来解析,然后这个结构体就是我们想要的数据了。可以使用这个工具 将 json 的字段解析为 go 中的结构体。然后将上一步的数据使用 json 库进行解析,如下:
var caiyunResponse CaiyunResponse // CaiyunResponse 即使用工具生成的结构体
err = json.Unmarshal(bodyText, &caiyunResponse)
if err != nil {
log.Fatal(err)
}
以上三步完成后就可以按照自己想要的方式实现在线字典了(打印、显示到网页上等等)。
- 扩展:使用多个翻译引擎并让并行请求以提升响应速度 使用不同的翻译引擎参照上面三步即可,并行请求则可以使用 waitGroup,实例代码如下:
var wg sync.WaitGroup
wg.Add(3)
go func(word string) {
defer wg.Done()
caiyunQuery(word)
}(word)
go func(word string) {
defer wg.Done()
huoshanQuery(word)
}(word)
go func(word string) {
defer wg.Done()
baiduQuery(word)
}(word)
wg.Wait()
最终结果如下,上面的结果是串行化请求,下面的结果是并行请求。
SOCK5 代理
这个应用是实现一个代理服务器,目的是让客户端的所有请求都要通过该代理服务器进行转发。整个过程大致分为四个阶段:
- 协商阶段,协商使用的协议版本、认证方法等
- 认证阶段,使用上一个阶段协商出的方法进行认证
- 建立连接,本文中使用 tcp 建立连接
- 请求数据
(图来自青训营内部资料,其中省略了认证阶段)
实现上,可能涉及到的技术点有:网络编程、IO操作、并发编程。
首先要监听某个端口,并在有请求到来时开启新 goroutine 来处理,如下所示:
server, err := net.Listen("tcp", "127.0.0.1:7777 ")
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client) // 处理客户端请求
}
处理来自客户端请求时主要分为两个函数,auth 和 connect。其中 auth 负责协商与认证(其实本文为了简单不涉及认证),connect 负责连接服务器并转发消息。
在 auth 函数中,根据如下标准来读取协议版本与认证方法:
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
// VER: 协议版本,占 1 字节,socks5为0x05
// NMETHODS: 支持认证的方法数量,占 1 字节
// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
// X’00’ NO AUTHENTICATION REQUIRED
// X’02’ USERNAME/PASSWORD
因此通过连接按照标准将其读出:
// 读 VER
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)
}
// 读 NMETHODS
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
// 读 METHODS
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
为了通过协商,还需要返回一些简单的信息:
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
通过协商与认证后就可以建立连接并请求数据了,也就是 connect 函数的工作,在这一步中也是需要按照标准来读取数据:
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER 版本号,占 1 字节,socks5的值为0x05
// CMD 占 1 字节,0x01表示CONNECT请求
// RSV 保留字段,值为0x00
// ATYP 目标地址类型,占 1 字节,DST.ADDR的数据对应这个字段的类型。
// 0x01表示IPv4地址,DST.ADDR为4个字节
// 0x03表示域名,DST.ADDR是一个可变长度的域名
// DST.ADDR 一个可变长度的值
// DST.PORT 目标端口,固定2个字节
读取方式和上面一样就不再赘述了。
读到客户端想请求的服务器地址后,代理服务器这里需要与该服务器建立连接:
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
随后代理服务器需要向客户端返回状态,我们这里为了简单就使用零值返回即可。
// +----+-----+-------+------+----------+----------+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X’00’ succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
最后就是代理服务器作为中间代理,把客户端发的数据发给服务端,再把服务端发的数据发给客户端。在这个过程中只要有一方关闭连接我们就认为代理结束,也关闭连接。
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
至此所有实现完毕,可以测试一下代理效果。
代理服务器进行代理:
客户端通过代理服务器得到的请求响应: