Go网络编程(简单的HTTP Web服务器和SOCKS5代理服务器)| 青训营

178 阅读12分钟

Go语言中net标准库

net提供了用于网络通信的基本功能。net包支持TCP、UDP、Unix域套接字和HTTP等协议,使得开发网络应用程序变得简单和高效。下面是net包中一些常用的功能:

1. TCP和UDP网络通信:

  • net.Dial: 用于建立TCP或UDP连接。
  • net.Listen: 用于在指定网络地址上监听连接。
  • net.DialTimeout: 带有超时的连接建立。
  • net.PacketConn: 用于使用UDP协议进行数据包通信。

2. HTTP网络通信:

  • http.Get: 用于发送HTTP GET请求并获取响应。
  • http.Post: 用于发送HTTP POST请求并获取响应。
  • http.ListenAndServe: 用于在指定端口上监听HTTP请求。

3. 基本网络工具:

  • net.IP: 代表一个IPv4或IPv6地址。
  • net.TCPAddrnet.UDPAddr: 分别代表TCP和UDP地址。
  • net.ResolveTCPAddrnet.ResolveUDPAddr: 解析TCP和UDP地址字符串。
  • net.LookupIP: 执行DNS查找并返回与主机名关联的IP地址。

4. Socket操作:

  • net.Conn: 代表通用的网络连接。
  • net.Listener: 代表通用的网络监听器。
  • net.Dialer: 定义用于拨号的参数。
  • net.ListenConfig: 定义用于监听的参数。

简单的HTTP Web服务器编程案例

1. 常见的HTTP请求方法:

  1. GET:用于从服务器获取资源。GET请求是一种幂等的请求方法,意味着多次发送相同的GET请求,服务器的状态不会改变。
  2. POST:用于向服务器提交数据,通常用于创建新的资源。POST请求是非幂等的,多次发送相同的POST请求,服务器可能会创建多个相同的资源。
  3. PUT:用于向服务器更新资源或替换资源。PUT请求是幂等的,多次发送相同的PUT请求,服务器的资源状态始终相同。
  4. DELETE:用于请求服务器删除指定的资源。DELETE请求也是幂等的,多次发送相同的DELETE请求,服务器的状态仍然保持一致。
  5. PATCH:用于部分更新服务器上的资源。与PUT请求不同,PATCH请求只会更新资源的部分内容,而不是整个资源。
  6. HEAD:类似于GET请求,但服务器不会返回实际的数据主体,只返回响应头,通常用于检查资源的元数据或验证资源是否存在。
  7. OPTIONS:用于请求服务器告知支持的请求方法和其他功能,比如CORS(跨域资源共享)时预检请求会使用OPTIONS方法。
  8. CONNECT:用于建立与代理服务器的隧道通信,通常用于SSL加密连接。
  9. TRACE:用于追踪HTTP请求-响应的传输路径,主要用于调试和测试。

2. HTTP服务器编程案例(GET、POST、PUT、DELETE):

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
)

// ResponseData 是用于响应的数据结构
type ResponseData struct {
    Method  string `json:"method"`  // 请求方法
    Message string `json:"message"` // 响应消息
    Name    string `json:"name"`    // 参数 name 的值
    Age     int    `json:"age"`     // 参数 age 的值
}

// requestHandler 处理所有类型的HTTP请求
func requestHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // 处理GET请求,从URL的查询参数中读取 name 和 age 参数的值
        name := r.URL.Query().Get("name")
        age := r.URL.Query().Get("age")

        // 构造响应数据
        response := ResponseData{
                Method:  "GET",
                Message: "This is a GET request",
                Name:    name,
        }

        // 将 age 转换为整数
        var err error
        response.Age, err = strconv.Atoi(age)
        if err != nil {
                http.Error(w, "Invalid age value", http.StatusBadRequest)
                return
        }
        sendResponse(w, response)


    case http.MethodPost:
        // 处理POST请求,解析JSON数据
        var requestData map[string]string
        err := json.NewDecoder(r.Body).Decode(&requestData)
        if err != nil {
                http.Error(w, "Invalid request data", http.StatusBadRequest)
                return
        }

        // 构造响应数据
        response := ResponseData{
                Method:  "POST",
                Message: fmt.Sprintf("This is a POST request with data: %v", requestData),
                Name:    requestData["name"],
        }

        // 将 age 转换为整数
        response.Age, err = strconv.Atoi(requestData["age"])
        if err != nil {
                http.Error(w, "Invalid age value", http.StatusBadRequest)
                return
        }
        sendResponse(w, response)


    case http.MethodPut:
        // 处理PUT请求,解析JSON数据
        var requestData map[string]string
        err := json.NewDecoder(r.Body).Decode(&requestData)
        if err != nil {
                http.Error(w, "Invalid request data", http.StatusBadRequest)
                return
        }

        // 构造响应数据
        response := ResponseData{
                Method:  "PUT",
                Message: "This is a PUT request",
                Name:    requestData["name"],
        }

        // 将 age 转换为整数
        response.Age, err = strconv.Atoi(requestData["age"])
        if err != nil {
                http.Error(w, "Invalid age value", http.StatusBadRequest)
                return
        }
        sendResponse(w, response)


    case http.MethodDelete:
        // 处理DELETE请求,从URL的查询参数中读取 name 和 age 参数的值
        name := r.URL.Query().Get("name")
        age := r.URL.Query().Get("age")

        // 构造响应数据
        response := ResponseData{
                Method:  "DELETE",
                Message: "This is a DELETE request",
                Name:    name,
        }

        // 将 age 转换为整数
        var err error
        response.Age, err = strconv.Atoi(age)
        if err != nil {
                http.Error(w, "Invalid age value", http.StatusBadRequest)
                return
        }
        sendResponse(w, response)

    default:
        // 对于不支持的请求方法,返回"Method not allowed"错误
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

// sendResponse 发送JSON响应
func sendResponse(w http.ResponseWriter, response ResponseData) {
    // Content-Type是HTTP响应头的一个字段,用于指示服务器返回的数据的类型。通过设置Content-Type为`application/json`,服务器通知客户端响应数据将会是JSON格式的数据
    w.Header().Set("Content-Type", "application/json")
    
    // 设置HTTP响应的状态码,状态码用于表示服务器对请求的处理结果
    // 常见的HTTP状态码包括200(OK)、404(Not Found)、500(Internal Server Error)等。其中,`http.StatusOK`是一个常量,表示状态码200,它表示服务器成功处理了请求。
    w.WriteHeader(http.StatusOK)
    
    // 调用`Encode(response)`方法,编码器将`response`结构体(或数据类型)转换为JSON格式的数据,并将其写入到`http.ResponseWriter`中。这样,服务器就能够将JSON数据作为HTTP响应返回给客户端。
    json.NewEncoder(w).Encode(response)
}

func main() {
    // 用于将请求路径 `/` (根路径) 与特定的处理函数 `requestHandler` 绑定在一起
    // 当用户访问根路径时(例如 `http://localhost:8080/`),服务器将会调用 `requestHandler` 函数来处理该请求
    http.HandleFunc("/", requestHandler)

    fmt.Println("Server started at http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

在上述代码中,requestHandler函数处理所有类型的HTTP请求,并根据请求方法类型返回不同的响应。sendResponse函数用于发送JSON响应,以减少重复代码。

3. HTTP服务器案例使用(GET、POST、PUT、DELETE):

  1. 首先,运行上述代码,启动HTTP服务器,如下图表示服务器启动成功

image.png

  1. 打开一个HTTP请求工具
  • GET: 输入HTTP请求,查询参数nameage指定了需要获取的资源的条件,这些条件被编码在URL中,以便将数据传递给服务器。

image.png

  • POST: 在POST请求中,通常使用JSON格式来更新服务器上的资源。为了发送JSON数据,客户端需要将JSON数据作为请求体的内容,设置请求头的"Content-Type"为"application/json"。

image.png

image.png

  • PUT: 在PUT请求中,通常使用JSON格式来更新服务器上的资源。为了发送JSON数据,客户端需要将JSON数据作为请求体的内容,通常设置请求头的"Content-Type"为"application/json"。

image.png

  • DELETE: 在DELETE请求中,指定参数nameage的条件,这些条件被编码在URL中,以便将数据传递给服务器。

image.png

简单的SOCKS5代理服务器编程案例

以下代码来自于青训营课程资源,加入了我自己的理解。该代码实现了一个简单的SOCKS5代理服务器。SOCKS5是一种网络协议,允许客户端通过代理服务器与远程服务器建立连接,并通过该连接进行数据传输。代理服务器在这里运行于本地,监听在127.0.0.1:1080上,当客户端连接到该代理服务器时,它将建立到目标服务器的连接,实现了一个简单的端口转发功能。

package main

import (
    "bufio" // 用于带缓冲的读写操作
    "context" // 用于传递请求的上下文信息,并在多个 goroutine 之间进行通信和协调
    "encoding/binary" // 用于进行二进制数据编码和解码
    "errors" // 用于处理错误
    "fmt" // 用于格式化输出
    "io" // 用于 I/O 操作
    "log" // 用于日志输出
    "net" // 用于网络相关的操作,包括创建监听器、连接等
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
    // 创建一个 TCP 监听器,监听本地 127.0.0.1 地址的 1080 端口
    server, err := net.Listen("tcp", "127.0.0.1:1080")
    if err != nil {
        panic(err) // 如果创建监听器时发生错误,终止运行并输出错误信息
    }

    for {
        // 等待客户端连接
        client, err := server.Accept()
        if err != nil {
            log.Printf("Accept failed %v", err) // 如果接受连接时发生错误,记录错误信息但不终止运行,继续等待其他客户端连接
            continue
        }

        go process(client) // 在新的 goroutine 中调用 process() 函数处理客户端连接,并继续等待其他客户端连接
    }
}

// process 函数用于处理客户端连接
func process(conn net.Conn) {
    defer conn.Close() // 延迟关闭连接,在处理完成后始终关闭连接,避免资源泄露

    reader := bufio.NewReader(conn) // 创建一个带缓冲的读取器,用于从客户端连接中读取数据

    err := auth(reader, conn) // 调用 auth() 函数进行认证处理
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return // 如果认证失败,记录错误信息,并终止该连接的处理
    }

    err = connect(reader, conn) // 调用 connect() 函数处理客户端的 CONNECT 请求
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return // 如果处理 CONNECT 请求失败,记录错误信息,并终止该连接的处理
    }
}

// auth 函数用于处理客户端的认证请求
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
    // 认证协议格式如下:
    // +----+----------+----------+
    // |VER | NMETHODS | METHODS  |
    // +----+----------+----------+
    // | 1  |    1     | 1 to 255 |
    // +----+----------+----------+
    // VER: 协议版本,socks5为0x05
    // NMETHODS: 支持认证的方法数量
    // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。
    // RFC预定义了一些方法值的含义,其中 0x00 表示不需要认证,0x02 表示用户名/密码认证。

    // 读取并验证协议版本
    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)
    }

    // 读取支持的认证方法数量
    methodSize, err := reader.ReadByte()
    if err != nil {
        return fmt.Errorf("read methodSize failed:%w", err)
    }

    // 读取支持的认证方法
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    if err != nil {
        return fmt.Errorf("read method failed:%w", err)
    }

    log.Println("ver", ver, "method", method)

    // 认证响应格式如下:
    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+

    // 返回认证响应,这里使用无需认证的方法(0x00)
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

// connect 函数用于处理客户端的 CONNECT 请求
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
    // CONNECT 请求格式如下:
    // +----+-----+-------+------+----------+----------+
    // |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个字节

    // 读取 CONNECT 请求头部信息
    buf := make([]byte, 4)
    _, err = io.ReadFull(reader, buf)
    if err != nil {
        return fmt.Errorf("read header failed:%w", err)
    }

    ver, cmd, atyp := buf[0], buf[1], buf[3]
    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    if cmd != cmdBind {
        return fmt.Errorf("not supported cmd:%v", cmd)
    }

    // 解析目标地址和端口
    addr := ""
    // 根据目标地址类型 atyp 进行不同的处理
    switch atyp {
    case atypeIPV4:
        // 当目标地址类型为 IPv4 地址时,读取 4 个字节数据,解析为一个 IPv4 地址
        _, 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 hostSize 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 atypeIPV6:
        // 当目标地址类型为 IPv6 地址时,返回不支持 IPv6 的错误
        return errors.New("IPv6: no supported yet")
    default:
        // 如果目标地址类型不在上述三种类型之内,则表示为无效的地址类型,返回错误
        return errors.New("invalid atyp")
    }
    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
        return fmt.Errorf("read port failed:%w", err)
    }
    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)

    // 返回 CONNECT 响应,这里固定为成功(0x00)
    _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    if err != nil {
        return fmt.Errorf("write failed: %w", err)
    }

    // 创建一个可取消的Context对象ctx,和一个用于主动取消ctx的cancel函数。
    ctx, cancel := context.WithCancel(context.Background())

    // 使用defer确保在函数返回时调用cancel函数,及时释放资源。
    defer cancel()

    // 在两个goroutine中进行数据传递。
    // 第一个goroutine从reader中复制数据到dest,同时在传输完成后主动调用cancel函数取消ctx。
    go func() {
        _, _ = io.Copy(dest, reader)
        cancel()
    }()

    // 第二个goroutine从dest中复制数据到conn,同时在传输完成后主动调用cancel函数取消ctx。
    go func() {
        _, _ = io.Copy(conn, dest)
        cancel()
    }()

    // <-ctx.Done()是一个阻塞操作,等待ctx.Done()通道关闭。
    // 当其中一个goroutine完成数据传递并调用了cancel函数后,会导致ctx.Done()的通道关闭。
    // 这时<-ctx.Done()将不再阻塞,程序会继续往下执行。
    // 通过这种方式,可以等待所有goroutine完成数据传递操作后再继续执行后续的代码,确保所有goroutine都已完成。
    <-ctx.Done()
    return nil
}
  1. 主函数main:在127.0.0.1:1080上创建一个TCP监听器server,接受来自客户端的连接请求。每当有客户端连接时,启动一个process协程来处理该连接
  2. process函数:处理客户端连接的主要逻辑。
  • 进行认证阶段:根据SOCKS5协议规定,客户端与代理服务器在连接建立后,首先要进行认证阶段。客户端发送其支持的认证方法给代理服务器,然后代理服务器从中选择一种认证方法,告知客户端。这里的代理服务器仅支持无需认证和用户名/密码认证两种方式。首先读取客户端发送的认证方法,然后向客户端回复选择的认证方法。
  • 进行连接阶段:认证成功后,客户端请求与目标服务器建立连接。客户端发送一个特定的请求给代理服务器,请求连接到目标服务器的指定地址和端口。代理服务器解析该请求,然后建立与目标服务器的连接,并返回连接状态给客户端。目标服务器的地址可以是IPv4地址、IPv6地址或者域名。
  • 进行数据传输:当连接阶段成功后,代理服务器与目标服务器建立了连接。接下来,代理服务器使用两个协程来处理数据传输。一个协程从客户端读取数据并将其写入到目标服务器,另一个协程从目标服务器读取数据并将其写入到客户端。这样就实现了数据在代理服务器中的转发。
  • 结束连接:一旦数据传输完成或发生错误,代理服务器关闭与客户端和目标服务器的连接。