Go語言實作簡單的webhooks練習。
範例環境:
- Go 1.18
事前要求
參考「Golang 建立網頁伺服器 Web Server」建立一個簡單的Go網路應用程式。
注意本範例server的webhooks及註冊webhooks的client的程式皆在同一個Go專案,分別以目錄server
及client
區隔。
Server Webhhooks
Webhooks需要:
- 提供client註冊/訂閱webhooks的API endpoint
- 儲存client註冊webhooks的URL。
- 提供client設計接收webhooks發送請求的資料格式
在專案根目錄建立server
目錄用於存放server端的程式碼。
建立server/route.go
如下。
/regsiter
轉交WebhooksHandler.Register()
;
/greeting
轉交WebhooksHandler.Greeting()
。
server/route.go
package server
import "net/http"
func Route(wh *WebhooksHandler) {
http.HandleFunc("/register", wh.Register)
http.HandleFunc("/greeting", wh.Greeting)
}
建立server/handler.go
如下。
WebhooksHandler.Register()
的case http.MethodPost
(POST|/register
)處理client註冊webhooks的請求URL。註冊時提供名稱及URL的JSON request body,由WebhooksRegisterRequest
接收後轉交WebhooksService.Save()
儲存起來,並返回webhooks的請求格式文件events.yaml
給client。
WebhooksHandler.Register()
的case http.MethodGet
(GET|/register
)調用WebhooksService.GetRegisteredUrls()
取得client註冊webhooks的URL清單。
WebhooksHandler.Greeting()
的case http.MethodGet
(GET|greeting
)為server的一簡單服務,執行時會以http.Post()
發送請求給client註冊的URL,發送的請求JSON格式即為client註冊webhooks成功時返回的yaml文件。
server/handler.go
package server
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
)
type WebhooksHandler struct {
WebhooksService *WebhooksService
}
func NewWebhooksHandler(ws *WebhooksService) *WebhooksHandler {
return &WebhooksHandler{
WebhooksService: ws,
}
}
//go:embed resources/events.yaml
var events string
func (wh *WebhooksHandler) Register(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var req WebhooksRegisterRequest
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&req)
if err != nil {
panic(err)
}
wh.WebhooksService.Save(req)
rw.Write([]byte(events))
case http.MethodGet:
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(wh.WebhooksService.GetRegisteredUrls())
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (wh *WebhooksHandler) Greeting(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
fmt.Println("Good day")
dtos := wh.WebhooksService.GetRegisteredDtos()
if len(dtos) == 0 {
fmt.Println("webhooks has no registered urls")
return
}
for _, dto := range wh.WebhooksService.GetRegisteredDtos() {
data := fmt.Sprintf("{\"name\": \"%s\"}", dto.Name)
resp, err := http.Post(dto.URL, "application/json", bytes.NewBuffer([]byte(data)))
if err != nil {
fmt.Printf("send event failed")
}
b, err := io.ReadAll(resp.Body)
msg := string(b)
if msg == "success" {
fmt.Println("send event success")
}
}
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
建立server/request.go
如下。WebhooksRegisterRequest
用於接收POST|/register
client註冊webhooks的JSON資料。
sever/request.go
package server
type WebhooksRegisterRequest struct {
Name string
URL string
}
server/resources/events.yaml
為client呼叫POST|/register
註冊webhooks成功時返回的webhooks發送請求格式文件。格式為OpenAPI,可以Swagger Editor開啟。
server/reources/events.yaml
openapi: 3.0.0
info:
contact:
name: "菜鳥工程師肉豬-Golang Webhooks簡單實作"
url: "https://matthung0807.blogspot.com/2022/04/go-webhooks-simple-impl.html"
title: "Webhooks Events API"
version: "1.0.0"
paths:
"/hello":
post:
description: Hello
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Hello"
required: true
responses:
"200":
description: Success
content:
text/plain:
schema:
type: string
example: success
tags:
- Hello
components:
schemas:
Hello:
type: object
required:
- name
properties:
name:
type: string
建立server/service.go
如下。
WebhooksService.Save()
負責把WebhooksHandler.Register()
POST送來的client註冊請求轉為WebhooksRegisterDto
儲存在模擬資料庫的registered
map變數中。
GetRegisteredDtos()
方法從registered
取得client註冊webhooks的資料用以在觸發webhooks時將請求送給client註冊的URL。
GetRegisteredUrls()
方法取得client註冊webhooks的url清單。
server/service.go
package server
import (
"sync"
"time"
)
type WebhooksService struct {
registered map[string]WebhooksRegisterDto
rwmu sync.RWMutex
}
func NewWebhooksService() *WebhooksService {
return &WebhooksService{
registered: make(map[string]WebhooksRegisterDto),
rwmu: sync.RWMutex{},
}
}
func (ws *WebhooksService) Save(req WebhooksRegisterRequest) {
ws.rwmu.Lock()
defer ws.rwmu.Unlock()
ws.registered[req.URL] = WebhooksRegisterDto{
Name: req.Name,
URL: req.URL,
CreatedAt: time.Now(),
}
}
func (ws *WebhooksService) GetRegisteredDtos() []WebhooksRegisterDto {
ws.rwmu.RLock()
defer ws.rwmu.RUnlock()
dtos := make([]WebhooksRegisterDto, 0)
for _, v := range ws.registered {
dtos = append(dtos, v)
}
return dtos
}
func (ws *WebhooksService) GetRegisteredUrls() []string {
urls := make([]string, 0)
for _, dto := range ws.GetRegisteredDtos() {
urls = append(urls, dto.URL)
}
return urls
}
建立server/dto.go
如下。WebhooksRegisterDto
為server儲存client註冊webhooks資料的資料物件。
server/dto.go
package server
import "time"
type WebhooksRegisterDto struct {
Name string
URL string
CreatedAt time.Time
}
Client
在專案根目錄建立client
目錄用於存放client端的程式碼。
建立client/route.go
如下。/hello
轉交HelloHandler()
。
client/route.go
package client
import "net/http"
func Route() {
http.HandleFunc("/hello", HelloHandler)
}
建立client/handler.go
如下。
HelloHandler()
的case http.MethodPost
(POST|/hello
)為向server註冊的URL。當server的webhooks觸發時會呼叫此endpoints。接收的請求資料HelloRequest
格式為client註冊webhooks成功時返回的yaml文件。
client/handler.go
package client
import (
"encoding/json"
"fmt"
"net/http"
)
func HelloHandler(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var req HelloRequest
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&req)
if err != nil {
panic(err)
}
fmt.Printf("Hello %s\n", req.Name)
rw.Write([]byte("success"))
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
建立client/request.go
如下。HelloRequest
為接收webhooks請求的資料物件。
client/request.go
package client
type HelloRequest struct {
Name string
}
主程式main.go
啟動server及client。
main.go
package main
import (
"net/http"
c "abc.com/demo/client"
s "abc.com/demo/server"
)
func main() {
server()
client()
http.ListenAndServe(":8080", nil)
}
func server() {
s.Route(s.NewWebhooksHandler(s.NewWebhooksService()))
}
func client() {
c.Route()
}
測試
完成以上server webhooks及client後,接著以client的角度測試webhooks的效果。
啟動專案。使用curl呼叫server的POST|/register
註冊webhooks並以JSON提供註冊的名稱及URL http://localhost:8080/hello
。成功後返回webhooks的請求格式文件。
$ curl -X POST "http://localhost:8080/register" \
> -H 'content-type: application/json' \
> -d '{"url": "http://localhost:8080/hello", "name": "john"}'
openapi: 3.0.0
info:
contact:
name: "菜鳥工程師肉豬-Go Webhooks簡單實作"
url: "https://matthung0807.blogspot.com/2022/04/go-webhooks-simple-impl.html"
title: "Webhooks Events API"
version: "1.0.0"
paths:
"/hello":
post:
description: Hello
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Hello"
required: true
responses:
"200":
description: Success
content:
text/plain:
schema:
type: string
example: success
tags:
- Hello
components:
schemas:
Hello:
type: object
required:
- name
properties:
name:
type: string
呼叫GET/register
即返回剛註冊的http://localhost:8080/hello
。
$ curl -X GET "http://localhost:8080/register"
["http://localhost:8080/hello"]
呼叫GET/greeting
觸發webhooks。
$ curl -X GET "http://localhost:8080/greeting"
此時在console會印出以下。其中Hello john
為client GET/hello
被server webhooks觸發的結果。
Good day
Hello john
send event success
觸發webhooks循序圖。
┌────────┐ ┌────────┐
│ client │ │ Server │
└────┬───┘ └────┬───┘
│ │
│ GET|/greeting │
┌┼─────────────────────►┌┤
││ ││
││ ││
││ │┼─print "Good day"
││ ││
││ ││
││ │┼─retrive webhooks registered urls
││ ││
││ POST|/hello ││
│┤◄─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┼┼─send webooks event request
││ ││
Print "Hello john"─┼│ ││
││ ││
└┤ └┼─print "send event success"
│ │
│ │
▼ ▼
以上即為webhooks的簡單範例,但通常註冊時需要提供token做為client驗證。
沒有留言:
張貼留言