# SOCKS5代理服务器的实现(下) | 青训营笔记

125 阅读2分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 6 天

本节重点

手动实现 relay 阶段代码,解决双向数据转换问题。

SOCKS5 代理 - relay 阶段

在上一节已经讲完了 SOCKS5 代理服务器至请求阶段的原理和代码,现在是最后一个阶段 —— relay 阶段的原理和实现,真正与 IP 端口建立连接,双向转换数据,代理就算完成了。

首先,用到 net 包的 Dial 函数,简单地去用 TCP 协议往对应的 IP 或域名加端口建立 TCP 连接,建立连接之后,如果没有出错,第一时间还是要加一个 Close 在函数结束的时候关闭连接。

port:=binary.BigEndian.Uint16(buf[:2])
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
   return fmt.Errorf("dial dst failed:%w", err)
}
defer dest.Close()
log.Println("dial", addr, port)

接下来要建立浏览器和下一个服务器的双向数据转换,找到 io 包中有一个 Copy 函数可以实现一个单项数据转换,会把 src 一个只读流中的数据,用一个死循环逐步拷贝到 dst 这个可写流中。

func Copy(dst Writer, src Reader) (written int64, err error)

现在需要实现一个双向数据转换,所以用一个 io.Copy 是不够的,需要启动两个 goroutine。

这里先启动两个 goroutine,然后在里面分别调用 io.Copy,这里要注意!两个拷贝的方向是不同的,一个是从用户浏览器拷贝数据到底层服务器,另一个是底层服务器拷贝数据到用户的浏览器。

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
   _, _ = io.Copy(dest, reader)
   cancel()
}()
go func() {
   _, _ = io.Copy(conn, dest)
   cancel()
}()
<-ctx.Done()

但是现在出现了一个问题,即启动 goroutine 是几乎不耗费时间的,那么直接情况下会跑到后面 return 返回了,那么连接也就被关闭了,说明这时需要等待任何一个方向的 Copy 失败(可能某一方关闭连接了),此时才终止连接,这时,用到标准库里面的一个 context 机制,这是 Golang 标准库里面一个非常重要的内容,这里使用 WithCancel 来创建一个 context,然后在最后等待 ctx.Done,等待这个 context 执行完成,这个执行的时机也是 cancel 函数被调用的时机,那么这时会在任何一个 goroutine 出错时,调用 cancel 函数。然后这里 defer cancel() 也是防御式编程(虽然没有什么意义)。那么现在就实现了,当任何一个方向的 Copy 失败,就返回两个函数,并且把双方的连接都关闭掉。

这样的话我们最后的代理服务器搭建就完工啦!最后的运行结果如下图所示 ~

image.png

阶段小结

本节学习了如何实现 SOCKS5 relay 阶段的代码,并且学习了如何在 goroutine 过短的情况下,等待结束再退出连接 ~