go语言实战案例三 | 青训营笔记

72 阅读3分钟

对于第三个实战项目的一些分析

说实话第一遍看第三个项目时并没有太看懂所以想写一篇笔记记录一下并逐句分析下代码

image.png 首先是主函数里面

sever, err := net.Listen("tcp", "localhost:1080")

调用 net.Listen() 方法来启动一个 TCP 服务端,指定监听地址为 "localhost:1080"。 返回一个监听器和错误。

client, err := sever.Accept()

通过该监听器,可以调用 Accept() 方法来接受客户端连接请求。Accept() 方法返回一个 net.Conn 类型的值,该类型表示一个 TCP 连接。通过该连接,客户端可以向服务器发送数据。

go process(client)

如果成功接收到客户端连接请求,则创建一个新的 goroutine 来处理该连接。在该 goroutine 中,调用 process() 方法来处理客户端发送的数据。这里可以理解为一个新的线程去处理链接

image.png bufio.NewReader()方法从conn参数中创建一个bufio.Reader对象。这个读取器被用于从客户端连接中读取数据。 在process()中重要的代码有两行

err := auth(reader, conn)
该函数使用`reader``conn`参数进行身份验证检查。如果身份验证失败,则函数记录错误消息并返回
err = connect(reader, conn)
该函数使用`reader``conn`参数建立TCP连接。如果连接失败,则函数记录错误消息并返回

image.png

image.png 在老师的文档中说明了数据中前几个字节的作用

ver, err := reader.ReadByte()

首先使用ReadByte()方法读取一个字节作为版本号,并将其存储在变量ver中。

methodSize, err := reader.ReadByte()
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)

使用ReadByte()方法读取一个字节支持认证方法数量,并将其存储在变量methodSize中。然后,函数使用make()函数创建一个长度为methodSize的字节切片method,并使用io.ReadFull()方法从reader中读取所有内容到该切片中。

_, err = conn.Write([]byte{socks5Ver, 0x00})

向连接写入一个字节作为版本号(socks5Ver)和一个零值(0x00),以表示身份验证请求的开始。如果写入失败,则函数返回一个fmt.Errorf类型的错误消息,指出写入身份验证请求失败。

connect部分代码

_, err = io.ReadFull(reader, buf)
ver, cmd, atyp := buf[0], buf[1], buf[3]
switch atyp {
case atypIPV4:
   _, err := io.ReadFull(reader, buf)
   if err != nil {
      return fmt.Errorf("read: atyp failed: %w", err)
   }
   addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
   hostSize, err := reader.ReadByte()
   if err != nil {
      return fmt.Errorf("read: failed: %w", err)
   }
   host := make([]byte, hostSize)
   _, err = io.ReadFull(reader, host)
   if err != nil {
      return fmt.Errorf("read host: failed: %w", err)
   }
   addr = string(host)
case atypIPV6:
   return errors.New("ipv6:no supported yet")
default:
   return errors.New("invalid atyp")
}

这段代码主要去判断从缓冲区中读取服务器地址(以IPv4、IPv6或主机名形式)

_, err = io.ReadFull(reader, buf[:2])
port := binary.BigEndian.Uint16(buf[:2])

从缓冲区中读取端口号

dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))

使用net.Dial()方法建立TCP连接到服务器地址和端口号。如果连接失败,则返回一个fmt.Errorf()类型的错误消息,指出连接失败的原因。

go func() {
   _, _ = io.Copy(dest, reader)
}()
go func() { _, _ = io.Copy(conn, dest) }()

一旦连接成功使用两个goroutine来交替发送和接收数据。其中,发送方从reader中读取数据并将其写入dest,而接收方从dest中读取数据并将其写入conn

ctx, cancel := context.WithCancel(context.Background())
<-ctx.Done()

在所有数据传输完成后,该函数使用context.WithCancel()函数创建一个取消上下文对象,并在一段时间后取消该对象

<-ctx.Done()是一个闭包表达式,它用于从一个上下文对象中获取取消信号并在函数中取消该上下文对象。在这个函数中,<-ctx.Done()用于在两个goroutine完成数据传输后取消上下文对象。

具体来说,在该函数的最后几行代码中,它使用context.WithCancel()函数创建了一个取消上下文对象,并将其存储在一个变量ctx中。然后,它使用<-ctx.Done()来从该上下文对象中获取取消信号并在需要时取消该对象。这可以确保在所有数据传输完成后,能够正确地关闭连接并释放资源。