go 语言实战入门案例之实现Socks5

简介: go 语言实战入门案例之实现Socks5

文章和代码已经归档至【Github仓库:https://github.com/timerring/backend-tutorial 】或者公众号【AIShareLab】回复 go 也可获取。

实现 Socks 5 代理

介绍

socks5 协议它虽然是代理协议,但它并不能用来翻,它的协议都是明文传输。
这个协议历史比较久远,诞生于互联网早期。它的用途是, 比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。socks5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻软件,最终暴露的也是一个 socks5 协议的端口。
例如爬,在爬取过程中很容易会遇到 IP 访问频率超过限制。这个时候很多人就会去网上找一些代理 IP 池,这些代理 IP 池里面的很多代理的协议就是 socks5。

先看一下最终写完的代理服务器的效果。启动程序,然后在浏览器里面配置使用这个代理,此时打开网页。代理服务器的日志,会打印出你访问的网站的域名或者 IP ,这说明我们的网络流量是通过这个代理服务器的。也能在命令行去测试我们的代理服务器。我们可以用 curl -socks5 + 代理服务器地址,后面加一个可访问的 URL,如果代理服务器工作正常的话,那么 curl 命令就会正常返回。

原理

了解 socks5 协议的工作原理。正常浏览器访问一个网站,如果不经过代理服务器的话,就是先和对方的网站建立 TCP 连接,然后三次握手,握手完之后发起 HTTP 请求,然后服务返回 HTTP 响应。

如果设置代理服务器之后,流程会变得复杂一些。首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段

  • 第一个握手阶段,浏览器会向 socks 5 代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,socks 5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了。
  • 第三个阶段是请求阶段,认证通过之后浏览器会对 socks 5 服务器发起请求。主要信息包括版本号,请求的类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。
  • 第四个阶段是 relay 阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上代理服务器并不关心流量的细节,可以是 HTTP 流量,也可以是其它 TCP 流量。

TCP echo server

我们先在 go 里面写一个简单的 TCP echo server。为了方便测试, server 的工作逻辑很简单,你给他发送啥,他就回复啥,代码如下。

package main

import (
    "bufio"
    "log"
    "net"
)

func main() {
   
   
    // 首先在 main 函数里面先用 net.listen 去监听一个端口,会返回一个 server
    server, err := net.Listen("tcp", "127.0.0.1:10803")
    if err != nil {
   
   
        panic(err)
    }
    for {
   
   
        // 然后在一个死循环里面,每次去 accept 一个请求,成功就会返回一个连接
        client, err := server.Accept()
        if err != nil {
   
   
            log.Printf("Accept failed %v", err)
            continue
        }
        // 接下来的话我们在一个 process 函数里面去处理这个连接。
        // 这前面会有个 go 关键字,这个代表启动一个 goroutinue, 可以暂时类比为其他语言里面的启动一个子线程。只是这里的 goroutinue 的开销会比子线程要小很多,可以很轻松地处理上万的并发。
        go process(client)
    }
}
// 接下来是这个 process 函数的实现。
func process(conn net.Conn) {
   
   
    // 首先第一步的话会先加一个 defer connection.close(), defer 是 Golang 里面的一个语法,这一行的含义就是代表在这个函数退出的时候要把这个连接关掉,否则会有资源的泄露。
    defer conn.Close()
    // 用 bufio.NewReader 来创建一个带缓冲的只读流
    reader := bufio.NewReader(conn)
    for {
   
   
        // 带缓冲的流的作用是,可以减少底层系统调用的次数,比如这里为了方便是一个字节一个字节的读取,但是底层可能合并成几次大的读取操作。并且带缓冲的流会有更多的一些工具函数用来读取数据。
        // 可以简单地调用 readbyte 函数来读取单个字节。再把这一个字节写进去连接。
        b, err := reader.ReadByte()
        if err != nil {
   
   
            break
        }
        _, err = conn.Write([]byte{
   
   b})
        if err != nil {
   
   
            break
        }
    }
}

我们来简单测试一下我们的第一个 TCP 服务器,然后测试会需要用到 nc 命令。如果没有的话可以进行安装,这里用 nc 127.0.0.1 10803,输入 timerring 然后服务器就会给你返回 timerring。
先运行代码

然后另开一个终端进行测试:

auth

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
)

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

func main() {
   
   
    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)
    }
}

func process(conn net.Conn) {
   
   
    defer conn.Close()
    reader := bufio.NewReader(conn)
    // 我们实现一个空的 auth 函数,在 process 函数里面调用,再来编写 auth 函数的代码。
    err := auth(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    log.Println("auth success")
}

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预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD

    // 先用 read bytes 来把版本号读出来
    ver, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read ver failed:%w", err)
    }
    // 然后如果版本号不是 socket 5 的话直接返回报错
    if ver != socks5Ver {
   
   
        return fmt.Errorf("not supported ver:%v", ver)
    }
    // 接下来我们再读取 method size ,也是一个字节。
    methodSize, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read methodSize failed:%w", err)
    }
    // 然后需要我们去 make 一个相应长度的一个 slice ,用 io.ReadFull 把它去填充进去。
    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    |
    // +----+--------+
    // 此时,代理服务器还需要返回一个response, 返回包包括 两个字段,一个是 version 一个是 method,也就是我们选中的鉴传方式,我们当前只准备实现不需要鉴传的方式,也就是00。
    _, err = conn.Write([]byte{
   
   socks5Ver, 0x00})
    if err != nil {
   
   
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

我们回忆一下认证阶段的逻辑,首先第一步的话,浏览器会给代理服务器发送一个包,然后这个包有三个字段,

  • 第一个字段, version 也就是协议版本号,固定是 5
  • 第二个字段 methods,认证的方法数目
  • 第三个字段每个 method 的编码, 0代表不需要认证, 2 代表用户名密码认证
    我们用 curl 命令测试一下当前版本的效果。此时 curl 命令肯定是不成功的,因为我们的协议还没实现完成。

但是我们看日志会发现, version 和 method 可以正常打印,说明当前我们的实现是正确的。

请求阶段

接下来我们开始做第三步,实现请求阶段,我们试图读取到携带 URL 或者 IP 地址+端口的包,然后把它打印出来。

package main

import (
    "bufio"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)

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

func main() {
   
   
    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)
    }
}

func process(conn net.Conn) {
   
   
    defer conn.Close()
    reader := bufio.NewReader(conn)
    err := auth(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    // 我们实现一个和 auth 函数类似的 connect 函数,同样在 process 里面去调用。再来实现 connect 函数的代码。
    err = connect(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
}

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预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD

    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)
    }

    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    _, err = conn.Write([]byte{
   
   socks5Ver, 0x00})
    if err != nil {
   
   
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   
   
    // 我们来回忆一下请求阶段的逻辑。浏览器会发送一个包,包里面包含如下6个字段
    // +----+-----+-------+------+----------+----------+
    // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER 版本号,socks5的值为0x05。version 版本号, 还是 5
    // CMD 0x01表示CONNECT请求。CMD 代表请求的类型,我们只支持 connection 请求,也就是让代理服务建立新的TCP连接。
    // RSV 保留字段,值为0x00,不理会。
    // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。可能是 IPV4 IPV6 或者域名。
    //   0x01表示IPv4地址,DST.ADDR为4个字节
    //   0x03表示域名,DST.ADDR是一个可变长度的域名
    // DST.ADDR 一个可变长度的值,这个地址的长度是根据 atype 的类型而不同的,port 端口号,两个字节, 我们需要逐个去读取这些字段。
    // DST.PORT 目标端口,固定2个字节

    // 后面这四个字段总共四个字节,我们可以一次性把它读出来。我们定义一个长度为 4 的 buffer 然后把它读满。
    buf := make([]byte, 4)
    _, err = io.ReadFull(reader, buf)
    if err != nil {
   
   
        return fmt.Errorf("read header failed:%w", err)
    }
    // 读满之后,然后第0 个、第1个、第3个、分别是 version cmd 和 type
    ver, cmd, atyp := buf[0], buf[1], buf[3]
    // version 需要判断是 socket 5
    if ver != socks5Ver {
   
   
        return fmt.Errorf("not supported ver:%v", ver)
    }
    // cmd 需要判断是 1,这里cmdBind是在前面定义好的
    if cmd != cmdBind {
   
   
        return fmt.Errorf("not supported cmd:%v", cmd)
    }
    // 下面的 atype,可能是 ipv4 ,ipv6,或者是 host。
    addr := ""
    switch atyp {
   
   
    // 如果 IPV4 的话,我们再次读满这个 buffer,因为这个 buffer 长度刚好也是4个字节
    case atypeIPV4:
        _, err = io.ReadFull(reader, buf)
        if err != nil {
   
   
            return fmt.Errorf("read atyp failed:%w", err)
        }
        // 然后逐个字节打印成 IP 地址的格式保存到 addr 变量。
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
    // 如果是个 host 的话
    case atypeHOST:
        // 需要先读它的长度
        hostSize, err := reader.ReadByte()
        if err != nil {
   
   
            return fmt.Errorf("read hostSize failed:%w", err)
        }
        // 再 make 一个相应长度的buf 填充它。
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        if err != nil {
   
   
            return fmt.Errorf("read host failed:%w", err)
        }
        // 再转换成字符串保存到 addr 变量。
        addr = string(host)
    // IPV6 用得比较少,我们就暂时先不支持。
    case atypeIPV6:
        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 ,我们读取它,然后按协议规定的大端字节序转换成数字。
    // 由于上面的 buffer 已经不会被其他变量使用了,我们可以直接复用之前的内存,建立一个临时的 slice ,长度是2用于读取,这样的话最多会只读两个字节回来。
    port := binary.BigEndian.Uint16(buf[:2])
    // 接下来我们把这个地址和端口打印出来用于调试。
    log.Println("dial", addr, port)
    // 收到浏览器的这个请求包之后,我们需要返回一个包,这个包有很多字段,但其实大部分都不会使用。

    // +----+-----+-------+------+----------+----------+
    // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER socks版本,这里为0x05,第一个是版本号还是 socket 5。
    // REP Relay field,内容取值如下 X’00’ succeeded,第二个,就是返回的类型,这里是成功就返回0。
    // RSV 保留字段,第三个是保留字段填 0。
    // ATYPE 地址类型,第四个 atype 地址类型填 1。
    // BND.ADDR 服务绑定的地址,第五个,第六个暂时用不到,都填成 0。
    // BND.PORT 服务绑定的端口DST.PORT

    // 一共 4 + 4 + 2 个字节,后面6个字节都是 0 填充。
    _, err = conn.Write([]byte{
   
   0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    if err != nil {
   
   
        return fmt.Errorf("write failed: %w", err)
    }
    return nil
}

现在我们来测试一下当前阶段的成果, 简单 curl 一下。

此时请求还是会失败,我们现在已经能看到正常打印出来访问的 IP 地址和端口,这说明我们当前的实现正常,这样我们就可以做最后一步,我们真正和这个端口建立连接,双向转发数据。

我们直接用 net.dial 建立一个 TCP 连接,建立完连接之后,我们同样要加一个 defer 来关闭连接。接下来需要建立浏览器和下游服务器的双向数据转发。

标准库的 io.copy 可以实现一个单向数据转发,双向转发的话,需要启动两个 goroutinue。

现在有一个问题,connect 函数会立刻返回,返回的时候连接就被关闭了。需要等待任意一个方向 copy 出错的时候,再返回 connect 函数。这里可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个 context。

在最后等待 ctx.Done() ,只要 cancel 被调用, ctx.Done 就会立刻返回。然后在上面的两个 goroutinue 里面调用一次 cancel 即可。

完整代码

package main

import (
    "bufio"
    "context"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)

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

func main() {
   
   
    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)
    }
}

func process(conn net.Conn) {
   
   
    defer conn.Close()
    reader := bufio.NewReader(conn)
    err := auth(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    err = connect(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
}

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预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD

    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)
    }

    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    _, err = conn.Write([]byte{
   
   socks5Ver, 0x00})
    if err != nil {
   
   
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   
   
    // +----+-----+-------+------+----------+----------+
    // |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个字节

    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 := ""
    switch atyp {
   
   
    case atypeIPV4:
        _, 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:
        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)

    // +----+-----+-------+------+----------+----------+
    // |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)
    }
    // 现在有一个问题,connect 函数会立刻返回,返回的时候连接就被关闭了。需要等待任意一个方向copy出错的时候,再返回 connect 函数。
    // 可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个context。
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    // 然后在两个 goroutinue 里面 调用一次 cancel 即可。
    go func() {
   
   
        _, _ = io.Copy(dest, reader)
        cancel()
    }()
    go func() {
   
   
        _, _ = io.Copy(conn, dest)
        cancel()
    }()
    // 在最后等待 ctx.Done() , 只要 cancel 被调用, ctx.Done就会立刻返回。
    <-ctx.Done()
    return nil
}

我们可以试着在浏览器里面再测试一下,在浏览器里面测试代理需要安装这个 switchomega 插件,然后里面新建一个情景模式,代理服务器选 socks 5,端口 1080 ,保存并启用。此时你应该还能够正常地访问网站,代理服务器这边会显示出浏览器版本的域名和端口。


学习路线推荐


参考:字节内部课 Go 语言原理与实践

相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
目录
相关文章
|
23小时前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
29 1
|
1天前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
28 1
|
1天前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
17 1
|
1天前
|
消息中间件 Go API
Golang深入浅出之-Go语言中的微服务架构设计与实践
【5月更文挑战第4天】本文探讨了Go语言在微服务架构中的应用,强调了单一职责、标准化API、服务自治和容错设计等原则。同时,指出了过度拆分、服务通信复杂性、数据一致性和部署复杂性等常见问题,并提出了DDD拆分、使用成熟框架、事件驱动和配置管理与CI/CD的解决方案。文中还提供了使用Gin构建HTTP服务和gRPC进行服务间通信的示例。
14 0
|
1天前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
14 5
|
2天前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
17 0
|
2天前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
12 3
|
2天前
|
中间件 Go
Go语言中的中间件设计与实现
【5月更文挑战第4天】Go语言中的中间件在HTTP请求处理中扮演重要角色,提供了一种插入逻辑层的方式,便于实现日志、认证和限流等功能,而不增加核心代码复杂性。中间件遵循`http.Handler`接口,通过函数组合实现。常见问题包括错误处理(确保中间件能正确处理并传递错误)和请求上下文管理(使用`context.Context`共享数据以避免并发问题)。通过理解中间件机制和最佳实践,可以构建更健壮的Web应用。
19 0
|
3天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
15 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
3天前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
16 6