package-tracking-app: Golang+RabbitMQ實時包裹跟蹤應用

banq 發表於 2022-07-13
Go RabbitMQ

此應用程式使用車輛資訊提供實時包裹位置資訊,因為車輛攜帶包裹。所以它回答了我的包裹現在在哪裡,它要去哪裡?
架構:



Websocket 處理程式:
一旦客戶端和伺服器都傳送了他們的握手,並且如果握手成功,那麼資料傳輸部分就開始了。這是一個雙向通訊通道,每一方都可以獨立於另一方隨意傳送資料。

func (p *PackageHandler) TrackByVehicleID(c echo.Context) error {
    wsConn, err := p.upgrader.Upgrade(c.Response(), c.Request(), nil)
    if err != nil {
        return err
    }

    ctx, cancelFunc := context.WithCancel(context.Background())

    go func() {
        _, _, err = wsConn.ReadMessage()
        if err != nil {
            cancelFunc()
        }
    }()

    for {
        select {
        case <-ctx.Done():
            wsConn.Close()
            return nil
        default:
            p, err := p.PUsecase.TrackByVehicleID(ctx, c.Param("vehicleId"))
            if err != nil {
                c.Logger().Error(err)
                continue
            }

            err = wsConn.WriteJSON(p)
            if err != nil {
                c.Logger().Error(err)
            }
        }
    }
}

upgrader.Upgrade將HTTP伺服器連線升級為WebSocket協議。它檢查握手過程;例如,它檢查請求頭是否正確,如 upgrade=Websocket 和 connection=Upgrade 等。

為了處理WebSocket的關閉握手,我使用wsConn.ReadMessage() 當客戶端導航到另一個頁面或類似ReadMessage()返回非零的錯誤。當錯誤不為零時,我就呼叫上下文的取消函式。在這樣做的時候,上下文的完成通道被關閉,所以我們可以關閉底層的TCP連線,並從我們的處理程式中返回,如下圖中的第一個選擇情況。

在選擇語句預設狀態下,我們可以監聽我們的package_status佇列;當新的包裹狀態到達時,我們可以將其資訊傳遞給WebSocket。注意:p.PUseCase.TrackByVehicleID(ctx, vehicleID)這個方法是基於<-chan amqp.Delivery . 當一個新的訊息到達我們的佇列時,我們從這個通道獲得包裹資訊。

包裹用例

func (p *packageUsecase) TrackByVehicleID(ctx context.Context, id string) (*domain.Package, error) {
    bytes, err := p.pc.ConsumeByVehicleID(ctx, id)
    if err != nil {
        return nil, err
    }

    var res domain.Package
    err = json.Unmarshal(bytes, &res)
    return &res, err
}

在我們的用例中,那裡沒有特定的規則。因此,我們可以從 RabbitMQ 客戶端獲得位元組訊息,並將我們的包結構格式化。

RabbitMQ 客戶端
我開啟一個 TCP 連線和其中的一個通道(虛擬 AMQP 連線)。通道是全雙工的,這意味著一個通道可以同時用於釋出和消費訊息。

我用宣告關鍵字配置佇列--如果不存在則建立,否則繼續--並在其上註冊消費者通道。

我在(c *rabbitmqClient) ConsumeByVehicleID方法中持續監聽。我使用message_id屬性來區分訊息。

注意:我在main方法上使用(c *rabbitmqClient) Publish方法,只是為了測試。

package client

import (
    "context"
    "errors"
    "fmt"

    "github.com/Abdulsametileri/package-tracking-app/domain"
    amqp "github.com/rabbitmq/amqp091-go"
)

const (
    QueueName = "package_status"
)

type rabbitmqClient struct {
    conn          *amqp.Connection
    ch            *amqp.Channel
    connString    string
    packageStatus <-chan amqp.Delivery
}

func NewRabbitMQClient(connectionString string) (*rabbitmqClient, error) {
    c := &rabbitmqClient{}
    var err error

    c.conn, err = amqp.Dial(connectionString)
    if err != nil {
        return nil, err
    }

    c.ch, err = c.conn.Channel()
    if err != nil {
        return nil, err
    }

    err = c.configureQueue()

    return c, err
}

func (c *rabbitmqClient) ConsumeByVehicleID(ctx context.Context, vehicleID string) (byte, error) {
    for msg := range c.packageStatus {
        if msg.MessageId == vehicleID {
            return msg.Body, nil
        }
    }
    return nil, errors.New("err when getting package status on channel")
}

func (c *rabbitmqClient) Publish(p domain.Package) {
    jsonStr := fmt.Sprintf(`{ "from": %q, "to": %q, "vehicleId": %q }`, p.From, p.To, p.VehicleID)

    _ = c.ch.Publish("", QueueName, false, false, amqp.Publishing{
        ContentType: "application/json",
        MessageId:   p.VehicleID,
        Body:        byte(jsonStr),
    })
}

func (c *rabbitmqClient) Close() {
    c.ch.Close()
    c.conn.Close()
}

func (c *rabbitmqClient) configureQueue() error {
    _, err := c.ch.QueueDeclare(
        QueueName,
        true,
        false,
        false,
        false,
        nil,
    )
    if err != nil {
        return err
    }

    c.packageStatus, err = c.ch.Consume(
        QueueName,
        "",
        true,
        false,
        false,
        false,
        nil,
    )
    return err
}


開源專案點選標題