使用Golang和AWS SNS来创建一个HTTP客户端和服务器API

389 阅读4分钟

在这个例子中,我们将创建一个应用程序到应用程序(A2A)的通信。两个应用都是HTTP APIs。客户端(发布者)发送事件(图片上传或下载),服务器(订阅者)监听事件以处理它们。我们有两种事件类型,分别是uploaddownload 。这两种类型都与image 主题相关。

我们从主题过滤器中受益,因为:

  • 订阅者在语义上是相互关联的。

  • 订阅者消费类似类型的事件。

  • 订阅者应该在主题上共享相同的访问权限。

步骤

  1. 创建一个主题。

  2. 运行服务器。

  3. 订阅服务器到主题。

  4. 客户端产生事件。

创建一个主题

你将在以后的客户端中使用下面的ARN:

$ aws --profile localstack --endpoint-url http://localhost:4566 sns create-topic --name image
{
    "TopicArn": "arn:aws:sns:eu-west-1:000000000000:image"
}

运行服务器

这就是应用程序的结构。确保应用程序正在运行,因为下一步将向它发送一个 "订阅确认 "请求:

├── internal
│   └── image
│       ├── image.go
│       └── request.go
└── main.go
main.go
package main

import (
	"log"
	"net/http"

	"github.com/you/server/internal/image"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
)

func main() {
	// Instantiate AWS session
	ses, err := session.NewSessionWithOptions(
		session.Options{
			Config: aws.Config{
				Credentials:      credentials.NewStaticCredentials("test", "test", ""),
				Region:           aws.String("eu-west-1"),
				Endpoint:         aws.String("http://localhost:4566"),
				S3ForcePathStyle: aws.Bool(true),
			},
			Profile: "localstack",
		},
	)
	if err != nil {
		log.Fatalln(err)
	}

	// Instantiate image controller.
	img := image.Image{Session: ses}

	// Instantiate HTTP router.
	rtr := http.NewServeMux()
	rtr.HandleFunc("/api/v1/images", img.Handle)

	// Instantiate HTTP server.
	log.Fatalln(http.ListenAndServe(":8080", rtr))
}
request.go
package image

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

type RequestType string

const (
	SubscriptionConfirmation RequestType = "SubscriptionConfirmation"
	Notification             RequestType = "Notification"
)

type EventValue string

const (
	Upload   EventValue = "upload"
	Download EventValue = "download"
)

type SubscriptionConfirmationRequest struct {
	Type              RequestType
	MessageId         string
	TopicArn          string
	Message           string
	Timestamp         string
	SignatureVersion  string
	SigningCertURL    string
	SubscribeURL      string
	Token             string
	MessageAttributes MessageAttribute
}

type NotificationRequest struct {
	Type              RequestType
	MessageId         string
	TopicArn          string
	Message           string
	Timestamp         string
	SignatureVersion  string
	SigningCertURL    string
	MessageAttributes MessageAttribute
}

type MessageAttribute struct {
	Event Event
}

type Event struct {
	Type  string
	Value EventValue
}

func (s *SubscriptionConfirmationRequest) Bind(r *http.Request) error {
	return json.NewDecoder(r.Body).Decode(s)
}

func (n *NotificationRequest) Bind(r *http.Request) error {
	return json.NewDecoder(r.Body).Decode(n)
}
image.go
package image

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sns"
)

type Image struct {
	Session *session.Session
}

// POST /api/v1/images
func (i Image) Handle(w http.ResponseWriter, r *http.Request) {
	data, _ := httputil.DumpRequest(r, true)
	fmt.Println(string(data))

	var err error
	switch r.Header.Get("X-Amz-Sns-Message-Type") {
	case string(SubscriptionConfirmation):
		err = i.confirm(r)
	case string(Notification):
		err = i.handle(r)
	default:
		log.Println("invalid message type")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	if err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}

// Subscription confirmation. Takes place only once. Once confirmed, dupplicated
// CLI subscription command executions will not trigger another request.
func (i Image) confirm(r *http.Request) error {
	var req SubscriptionConfirmationRequest
	if err := req.Bind(r); err != nil {
		return fmt.Errorf("request binding: %w", err)
	}

	ctx, cancel := context.WithTimeout(r.Context(), time.Second*5)
	defer cancel()

	if _, err := sns.New(i.Session).ConfirmSubscriptionWithContext(ctx, &sns.ConfirmSubscriptionInput{
		Token:    aws.String(req.Token),
		TopicArn: aws.String(req.TopicArn),
	}); err != nil {
		return fmt.Errorf("subscription confirmation: %w", err)
	}

	log.Println("confirming subscription ...")

	return nil
}

// Consumes published events. Called as many times as the client publishes.
func (i Image) handle(r *http.Request) error {
	var req NotificationRequest
	if err := req.Bind(r); err != nil {
		return fmt.Errorf("request binding: %w", err)
	}

	switch req.MessageAttributes.Event.Value {
	case Upload:
		log.Printf("uploading %s ...", req.Message)
	case Download:
		log.Printf("downloading %s ...", req.Message)
	default:
		return fmt.Errorf("unknwon event value")
	}

	return nil
}

订阅服务器到该主题

该命令使用 "image-topic-attributes.json "文件,其内容如下:

{
  "FilterPolicy":"{\"Event\":[\"upload\",\"download\"]}"
}
$ aws --profile localstack --endpoint-url http://localhost:4566 sns subscribe --topic-arn arn:aws:sns:eu-west-1:000000000000:image --protocol https --notification-endpoint https://1fd011d7aa1z.ngrok.io/api/v1/images --attributes file://image-topic-attributes.json
{
    "SubscriptionArn": "arn:aws:sns:eu-west-1:000000000000:images:ac2e0e62-a51a-4ee2-a859-e54018350e14"
}

如上所述,我正在使用Ngrok将我的应用程序暴露给互联网。这方面的命令是./ngrok http -host-header=rewrite localhost:8080

一旦服务器订阅了这个主题,它应该会收到一个类似下面的请求。服务器会识别这个并确认订阅:

POST /api/v1/images HTTP/1.1
Host: localhost:8080
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 682
Content-Type: text/plain
User-Agent: Amazon Simple Notification Service Agent
X-Amz-Sns-Message-Type: SubscriptionConfirmation
X-Amz-Sns-Subscription-Arn: arn:aws:sns:eu-west-1:000000000000:image:5ff05082-f52f-4319-bc97-dedb6831baf0
X-Amz-Sns-Topic-Arn: arn:aws:sns:eu-west-1:000000000000:image
X-Forwarded-For: 2.28.157.27
X-Forwarded-Proto: https
X-Original-Host: 1fd011d7aa1z.ngrok.io

{
  "Type": "SubscriptionConfirmation",
  "MessageId": "745bc6e1-214e-4070-868d-950359c0ac27",
  "TopicArn": "arn:aws:sns:eu-west-1:000000000000:image",
  "Message": "You have chosen to subscribe to the topic arn:aws:sns:eu-west-1:000000000000:image.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
  "Timestamp": "2021-03-28T18:01:46.542Z",
  "SignatureVersion": "1",
  "Signature": "EXAMPLEpH+..",
  "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem",
  "SubscribeURL": "http://localhost:4566/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:eu-west-1:000000000000:image&Token=7194c9aa",
  "Token": "7194c9aa"
}

你也应该在日志中看到2021/03/28 19:01:48 confirming subscription ...

客户端产生事件

你将使用的SNS方法如下:

func (s SNS) Publish(ctx context.Context, message, topicARN string, msgAttrs map[string]string) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Second*5)
	defer cancel()

	attrs := make(map[string]*sns.MessageAttributeValue, len(msgAttrs))
	for key, val := range msgAttrs {
		attrs[key] = &sns.MessageAttributeValue{DataType: aws.String("string"), StringValue: aws.String(val)}
	}

	res, err := s.client.PublishWithContext(ctx, &sns.PublishInput{
		Message:           &message,
		TopicArn:          aws.String(topicARN),
		MessageAttributes: attrs,
	})
	if err != nil {
		return "", fmt.Errorf("publish: %w", err)
	}

	return *res.MessageId, nil
}

对于手动测试,使用下面的例子:

attrs := map[string]string{"Event": "upload"}

Publish(r.Context(), "https://www......image.jpeg", "arn:aws:sns:eu-west-1:000000000000:image", attrs)
attrs := map[string]string{"Event": "download"}

Publish(r.Context(), "https://www......image.jpeg", "arn:aws:sns:eu-west-1:000000000000:image", attrs)

当你发布其中一个消息时,服务器的输出会像下面这样:

POST /api/v1/images HTTP/1.1
Host: localhost:8080
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 451
Content-Type: text/plain
User-Agent: Amazon Simple Notification Service Agent
X-Amz-Sns-Message-Type: Notification
X-Amz-Sns-Subscription-Arn: arn:aws:sns:eu-west-1:000000000000:image:5ff05082-f52f-4319-bc97-dedb6831baf0
X-Amz-Sns-Topic-Arn: arn:aws:sns:eu-west-1:000000000000:image
X-Forwarded-For: 2.28.157.27
X-Forwarded-Proto: https
X-Original-Host: 1fd011d7aa1z.ngrok.io

{
  "Type": "Notification",
  "MessageId": "23309108-d583-4145-bdc8-0ad7f710a428",
  "TopicArn": "arn:aws:sns:eu-west-1:000000000000:image",
  "Message": "https://www......image.jpeg",
  "Timestamp": "2021-03-28T18:03:03.129Z",
  "SignatureVersion": "1",
  "Signature": "EXAMPLEpH+..",
  "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem",
  "MessageAttributes": {
    "Event": {
      "Type": "string",
      "Value": "upload"
    }
  }
}

服务器也将输出2021/03/28 19:06:04 uploading https://www......image.jpeg ... 日志。上面的MessageId ,将与发布消息时客户端产生的一致。