protoc 插件如何写?

protoc 插件如何写?

如何编写一个 protoc 的插件

我们在编写 protocol 文件去生成.pb.go 文件的时候你都知道它是如何做到的吗?
整个过程很简单,其实我们之前生成的 go 文件是用的 protoc-gen-go 插件,可以去 github 上看看这个插件的代码 https://github.com/protocolbuffers/protobuf-go/blob/master/cmd/protoc-gen-go/main.go 里面主要用到的库是 google.golang.org/protobuf/compiler/protogen

其实插件的工作原理就是从标准输出读取和输入,具体可以看 https://protobuf.dev/reference/other/ 然后根据你的命令行中的 –{NAME}_out 去找 protoc-gen-{NAME} 这个插件然后生成文件具体请看官方文档
今天我们就一起来实现一个插件吧!

首先我们先来生成一个最基本的.pb.go 文件:

  1. 在生成之前我们都需要去下载一个二进制文件

    1
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

    这个会存在你的$GOPATH/bin 目录下

  2. 编写一个简单的 proto

    1
    2
    3
    4
    5
    6
    7
    syntax = "proto3";
    package simple;
    option go_package = "./simple";

    message hello {
    string world = 1;
    }
  3. 生成 .pb.go 文件
    为了后续方便,就直接写一个 makefile

    1
    2
    gen:
    protoc --go_out=. --go_opt=paths=source_relative ./simple/simple.proto

    执行 make gen 就能输出 .pb.go 文件了

插件编写

  1. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package main

    import (
    "flag"
    "fmt"
    "google.golang.org/protobuf/compiler/protogen"
    "os"
    )

    func main() {
    protogen.Options{
    ParamFunc: flag.CommandLine.Set,
    }.Run(func(gen *protogen.Plugin) error {
    for _, f := range gen.Files {
    if f.Generate {
    GenerateFile(gen, f)
    }
    }

    return nil
    })
    }

    func GenerateFile(gen *protogen.Plugin, f *protogen.File) {
    filename := f.GeneratedFilenamePrefix + "_plugin.pb.go"
    g := gen.NewGeneratedFile(filename, f.GoImportPath) // 初始化一个需要生成的文件
    g.P("// Code generated by protoc-gen-simple. DO NOT EDIT.")
    g.P("// source: ", f.Desc.Path())
    g.P()
    g.P("package ", f.GoPackageName) //包名
    for _, message := range f.Messages {
    _, _ = fmt.Fprint(os.Stderr, message) // 主要用来调试
    g.P("var _ = &", message.GoIdent, "{}") // 输出
    }
    }
    • gen.NewGeneratedFile 实例化一个生成文件的结构体

    • g.P(…) 写入文件操作

    • f.Messages 就是里面的 message 定义 还有 service 等等

    • _, _ = fmt.Fprint(os.Stderr, message)

      1
      至于这一段是为了调试,由于需要编译无法进行单步调试我就将需要获取的调试信息打印在了os.Stderr ,至于为啥不能是标准输出呢,是因为插件就是用标准输出来通信的,前面也有讲到过
  2. 编译

    1
    go install

    然后你就能看到你的$GOPATH/bin 目录下会有一个 protoc-gen-go-simple 二进制文件

  3. 使用插件
    在之前的命令加上一句 –go-simple_out=. –go-simple_opt=paths=source_relative

    1
    2
    3
    4
    5
    gen:
    protoc \
    --go_out=. --go_opt=paths=source_relative \
    --go-simple_out=. --go-simple_opt=paths=source_relative \
    ./simple/simple.proto

    make gen 你就能看到有两个.pb.go 文件了

以上的过程还是很简单的,就是调试起来很费劲也可能还有很简单的调试办法我不知道,还有一点就是文档不太友好,还得去看源码。基本上 proto 文件中所定义的里面的 *protogen.File 都能够拿到。所以可以干很多的骚操作,赶紧学起来吧~

String

String

Redis 数据结构 ———— String

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value 其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。

内部实现

SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
  • SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

字符串对象的内部编码(encoding)有 3 种 :intrawembstr

如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void*转换成 long),并将字符串对象的编码设置为 int。

如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为 embstr, embstr 编码是专门用于保存短字符串的一种优化编码方式:

webhook搭建

webhook搭建

WebHook

网页开发中的网络钩子(Webhook)是一种通过自定义回调函数来增加或更改网页表现的方法。这些回调可被可能与原始网站或应用相关的第三方用户及开发者保存、修改与管理。术语“网络钩子”由杰夫·林德塞(Jeff Lindsay)于 2007 年通过给计算机编程术语“钩子”(Hook)加上前缀得来

为什么我需要搭建一个 webhook

那可要从搭建这个博客系统说起了,由于该博客使用 hexo 搭建,所以每次更新博客需要部署一下然后才能生效。而我又不愿意每次写完都去部署,这样就埋下了一个 webhook 的坑!其实 webhook 就是一个回调地址每次推送都访问下那个地址然后做部署

搭建 webhook

其实搭建这个有很多第三方的库已经支持了,但是偏偏我选择了 gitee(可恶啊!!!)

1. 设置 gitee 的签名


如果你是 java 或者 python 搭建 webhoook gitee 有事例代码
下面使用 go 语言实现

1
2
3
4
5
6
7
8
9
// Sign secret 为你在gitee设置的秘钥,time为请求头中的X-Gitee-Timestamp 也可以用当前收到请求的时间戳 但与请求调用时间误差不能超过1小时
func Sign(secret string, time string) string {
stringToSign := time + "\n" + secret
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(stringToSign))
signData := h.Sum(nil)
return base64.StdEncoding.EncodeToString(signData)
}

2. 搭建后端服务

使用 gin 搭建 webhook 的后端服务

1
2
3
4
5
func main() {
r := gin.Default()
router.RegisterRouter(r) // 就是你需要注册的路由
r.Run(":3000") // 监听并在 0.0.0.0:8080 上启动服务
}

router 中的代码以及目录结构

其实我就是简单的分了一下目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
webhook
├─Dockerfile // 打包
├─Makefile // 部署
├─boot.sh // sh脚本
├─main.go // 程序入口
├─utils
| ├─response // 通用响应体
├─internal
| ├─router
| | ├─gitee.go // gitee 的路由
| | └router.go // 总路由
| ├─handler // 处理函数
| | ├─blog.go
| | └webhook.go
├─gitee
| ├─errors.go // 错误常量码
| ├─event.go //一些事件的常量
| ├─gitee.go //gitee包
| ├─push_event_payload.go //推送事件的请求数据结构体
| ├─sign //签名包

3. 编写处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
type GiteeService struct {
hook *gitee.Webhook
}

// New 创建一个新的服务
func NewGitee() *GiteeService {
hook, _ := gitee.New(gitee.WithSecret(GITEESECRET))
return &GiteeService{
hook: hook,
}
}

func (g *GiteeService) Blog(c *gin.Context) {
payload, err := g.hook.Parse(c.Request, gitee.PushEvents)
if err != nil {
if err == gitee.ErrEventNotFound {
// ok event wasn;t one of the ones asked to be parsed
fmt.Printf("ErrEventNotFound %s\n", err)
response.Err(c, err)
return
}
// ok event wasn;t one of the ones asked to be parsed
fmt.Printf("unkonw error %s\n", err)
response.Err(c, err)
return
}
switch payload.(type) {
case gitee.PushEventPayload:
//release := payload.(gitee.PushEventPayload)
go func() {
cmd := exec.Command("sh", "/root/app/blog/webhook.sh")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(string(output))
fmt.Printf("command error %s\n", err)
response.Err(c, err)
return
}
fmt.Println(string(output))
}()
response.Ok(c, nil)
}

}

上面就是一个简单的处理函数,但是值得说的一个点是 为什么需要使用携程去处理 由于打包部署的时间比较久但是 gitee 的 webhook 请求时长为 5s
所以我们这个请求得开一个携程去处理否则请求将请求超时

4. 部署 webhook

由于我需要执行其他需要自动部署的 app 的 shell 脚本,然而我有不想讲整个目录软连接进去所以我就直接二进制部署了就没有使用 docker, 可以看到 Dockerfile 都还在 呜呜呜

下面是我的部署 shell 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
path=/root/app/webhooks

cd $path
git pull

port=3000
# 找到占用 3000 端口的进程 ID
pid=$(lsof -t -i:$port)

if [ -n "$pid" ]; then
# 杀死进程
echo "Killing process $pid"
kill -9 $pid
fi

outlog="$path/out.log"
if [ -e "$outlog" ]; then
rm "$outlog"
fi

rm -rf $path/main
go build -o $path/main .
nohup $path/main >> $outlog &

先将之前的服务停止,然后再将日志清除,最后再后台运行。

5. gitee 配置 webhook

这个就简单了就将你的后台路由填进去然后测试一把就行

总结
以上就是搭建 webhook 的整个流程咯,实际上就是写个后端服务,然后当你出发 git 某个操作就会请求这个 url 然后执行你需要执行的代码。

easegress

easegress

Easegress

官方架构
Easegress 是一个全功能型的流量调度和编排系统,通过 API 网关技术,可以在不改一行代码的情况下,最大限度的帮助后台服务扩大系统可用性和稳定性,并且可以增加整体的性能。其可以让企业在快速业务增长的同时不用对整个技术架构进行大改造,以赢得并抓住稍瞬即逝的商业机会。官方文档

在这里插入图片描述
官方文档上的介绍以及架构图。

1. 下载源码

1
git clone https://github.com/megaease/easegress.git

源码中有一个 server 端和一个命令行工具
easegress的服务端和命令行启动程序

我们首先得需要编译命令行工具形成一个可执行的二进制文件,之后再用这个可执行文件去对服务端进行操作。当然你如果不需要 debug 的话你也可以编译 server 端。接下来我们直接就编译成两个二进制文件

2. 编译

里面有个 Makefile 文件,我们只需在最外层目录下执行

1
make build

然后他就会给你下载依赖并且编译,会在文件中生成 bin 文件夹,里面会有两个二进制文件

1
2
easegress-server 	//服务端
egctl //命令行工具

二进制文件

3. 运行

1
./easegress-server	//运行服务端

启动
启动成功后我们就需要创建 httpserver 了,这个就是用来对于流量的控制。

4. 创建一个 HttpServer

以下一切操作都在在 bin 目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
echo '
kind: HTTPServer
name: server-demo
port: 10080
keepAlive: true
https: false
rules:
- paths:
- pathPrefix: /pipeline
backend: pipeline-demo
- pathRegexp: ^/user/(.*)$
rewriteTarget: /$1
backend: test-demo '| ./egctl object create

以上的操作是创建了一个 httpserver 监听 10080 端口,并且有两个服务,一个是以/pipeline 为前缀的服务的流量直接打到 pipeline-demo 这个命名的后端上(虽然现在还未创建,我知道你很急,但是你先别急)下面的另一个服务就是 path 是一个正则表达式,所有匹配上的流量都会被打到 test-demo 服务上,rewriteTarget 为啥是/$1 呢,请看下面图片,
在这里插入图片描述
在这里插入图片描述
看官是否能看懂?也就是说这个路径重写是用了 go 标准库中的 ReplaceAllString 方法,然后这个$1 的话就是匹配到的字符串(这个话题就不延伸了,不然我怎么水博客🤪,可能某天就把这个坑填上了)

5. 创建服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
echo '
name: pipeline-demo
kind: Pipeline
flow:
- filter: proxy
filters:
- name: proxy
kind: Proxy
pools:
- servers:
- url: http://127.0.0.1:9095
- url: http://127.0.0.1:9096
- url: http://127.0.0.1:9097
loadBalance:
policy: roundRobin' | ./egctl object create

请听我解释: - servers 你部署的后端服务 (为啥有三个呢?) 你流量大需要分流呗 - loadBalance 你分流总得有个策略吧 - flow 就是 api 编排流量进来会根据你编排的顺序去走相应的流程
好吧下面我要装一波了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
echo '
name: test-demo2
kind: Pipeline
flow:
- filter: proxy
jumpIf:
failureCode: failResponse
- filter: failResponse
- filter: successResponse
filters:
- name: successResponse
kind: ResponseBuilder
protocol: http
template: |
statusCode: 200
headers:
"Content-Type": ["application/json"]
body: "{{.responses.DEFAULT.Body}}"
- name: failResponse
kind: ResponseBuilder
protocol: http
template: |
statusCode: 200
headers:
"Content-Type": ["application/json"]
body: |
{
"code": -1
"data": null
"message": "{{.responses.DEFAULT.JSONBody.message}}"
}
- name: proxy
kind: Proxy
pools:
- servers:
- url: http://127.0.0.1:8001
failureCodes: [400, 404, 408, 429, 500, 501, 503, 504]
loadBalance:
policy: roundRobin' | ./egctl object create

怎么样?是不是脑瓜子嗡嗡的,上面就是对数据的返回格式做了统一。
flow 中有个 jumpIf 这个就是当 failureCode 等于 failureCodes 中的某个值时就直接跳转到 failResponse 过滤器。好了好了我们启动一下看一下吧!

6. 启动

终于启动了

在这里插入图片描述
成功了 xdm,官方文档还有很多操作,比如说 ip 限制啊,跨域啊,鉴权等等…他还支持 K8s Ingress Controller 是不是值得一学?虽然小编学不会k8s但是或许某天我更新了k8s文章可别喷我!!!