package-tracking-app: Golang+RabbitMQ實時包裹跟蹤應用
此應用程式使用車輛資訊提供實時包裹位置資訊,因為車輛攜帶包裹。所以它回答了我的包裹現在在哪裡,它要去哪裡?
架構:
- RabbitMQ與docker-compose
- 帶有echo 框架的Websocket
- 基於Clean的架構模板
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 } |
開源專案點選標題