動態

詳情 返回 返回

PHP轉Go系列 | ThinkPHP與Gin框架之打造基於WebSocket技術的消息推送中心 - 動態 詳情

大家好,我是碼農先森。

在早些年前客户端想要實時獲取到最新消息,都是使用定時長輪詢的方式,不斷的從服務器上獲取數據,這種粗暴的騷操作實屬不雅。不過現如今我也還見有人還在一些場景下使用,比如在 PC 端掃描二維碼,然後使用長輪詢的方式從服務端獲取最新的掃碼信息,來判斷用户是否已經掃碼完成,諸如這種場景還有不少。其實大家都知道長輪詢的方式不好,那為什麼還有人使用呢?

我想最直接的原因就是「開發起來簡單明瞭」,人性決定了人類都是趨易避難的高級物種,那個容易上手就用那個。但是我想表達的是除了長輪詢的方式外,WebSocket 技術其實也不難,只不過對於從來沒有接觸過長連接的人來説,剛開始上手時會有一些思維上的障礙。這次我分享的內容是基於 WebSocket 技術的消息推送中心,看起來很高大上,其實也就是通過一些小的例子來演示,從服務端推送數據到客户端的這個過程,接下來的例子簡單明瞭容易上手,我們趕緊開始吧。

話不多説,開整!我們先來看一下整體的項目目錄結構,內容主要分為 PHP 和 Go 兩部分。

[manongsen@root php_to_go]$ tree -L 2
.
├── go_websocket
│   ├── app
│   │   ├── controller
│   │   |    |── message.go
│   │   │   └── websocket.go
│   │   └── route.go
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── php_websocket
│   ├── app
│   │   ├── controller
│   │   |    |── Push.php
│   │   │   └── Worker.php
│   ├── composer.json
│   ├── composer.lock
│   ├── config
│   │   |── worker_server.php
│   │   └── worker.php
│   ├── route
│   │   └── app.php
│   ├── think
│   ├── vendor
│   └── .env

ThinkPHP

使用 composer 創建基於 ThinkPHP 框架的 php_websocket 項目。

## 當前目錄
[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/php_websocket

## 安裝 ThinkPHP 框架
[manongsen@root php_websocket]$ composer create-project topthink/think php_websocket
[manongsen@root php_websocket]$ cp .example.env .env

## 安裝 Composer 依賴包
[manongsen@root php_websocket]$ composer require topthink/think-worker
[manongsen@root php_websocket]$ composer require predis/predis

使用 php think make:controller Worker 命令創建 Worker.php 控制器。這個控制器中主要實現了 onWorkerStart 這個方法,首先添加了一個 Timer 異步定時器,然後從 Redis 隊列中讀取消息,最後將消息推送到客户端,這個定時器會每間隔一秒鐘調度一次。

// ./php_to_go/php_websocket/app/controller/Worker.php
<?php
declare (strict_types = 1);

namespace app\controller;

use think\Request;
use think\worker\Server;
use Workerman\Lib\Timer;
use think\facade\Cache;
use think\facade\Env;

class Worker extends Server
{
    protected $socket = 'websocket://0.0.0.0:2345';
    protected static $connections = [];

    public function onWorkerStart($worker) {
        // 添加一個異步定時器任務
        Timer::add(1, function () use ($worker) {
            // 從消息中心隊列中讀取消息
            $redis = Cache::store('redis')->handler();
            $content = $redis->rpop(Env::get("MESSAGE_CENTER_KEY"));

            // 發送消息到客户端
            foreach ($worker->connections as $connection) {
                if (!empty($content)) {
                    $connection->send("PHP語言消息中心: " . $content);
                }
            }
        });
    }

    public function onWorkerReload($worker) {
    }

    public function onConnect($connection) {
    }

    public function onMessage($connection, $data){
    }

    public function onClose($connection) {
    }

    public function onError($connection, $code, $msg) {
    }
}

使用 php think make:controller Push 命令創建 Push.php 控制器。這個控制器的主要作用是接收外部的消息內容,然後推送到 Redis 消息隊列中,這裏提供的是 API 接口,這個接口可以在外部的後台系統調用。

// ./php_to_go/php_websocket/app/controller/Push.php
<?php

namespace app\controller;

use app\BaseController;
use think\facade\Cache;
use think\facade\Env;

class Push extends BaseController
{
    public function msg()
    {
        // 接收 GET 參數
        $params = $this->request->param();
        if (empty($params["content"])) {
            return json(["code" => -1, "msg" => "內容不能為空"]);
        }
        $content = $params["content"];

        // 推送消息到消息中心隊列
        $redis = Cache::store('redis')->handler();
        $redis->lpush(Env::get("MESSAGE_CENTER_KEY"), $content);

        return json(["code" => 0, "msg" => "success"]);
    }
}

先運行 php think worker 啓動 HTTP 服務,再運行 php think worker:server 啓動 WebSocket 服務,最後來測試一波。

Gin

通過 go mod 初始化 go_websocket 項目。

## 當前目錄
[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/go_websocket

## 初始化項目
[manongsen@root go_websocket]$ go mod init go_websocket

## 安裝第三方依賴庫
[manongsen@root go_websocket]$ go get github.com/gin-gonic/gin
[manongsen@root go_websocket]$ go get github.com/gorilla/websocket

在 go_websocket 項目中創建 websocket 控制器。這個控制器會將客户端連接存儲到指定的 Map 數據結構中,其次還提供了 WaitMessage 等待消息的方法,如果從 MsgQueue 通道中讀取到了消息,則把消息推送給所有的客户端。

// ./php_to_go/go_websocket/app/controller/websocket.php
package controller

import (
    "fmt"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
)

// 定義一個消息傳輸通道
var MsgQueue = make(chan string, 10)

// 定義一個存儲客户端連接的 Map
var Clients = make(map[*websocket.Conn]bool)

// 將 HTTP 協議升級至 WebSocket 協議
var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // 允許所有來源
    },
}

// 將客户端連接存儲到 Map
func HandleConnection(c *gin.Context) {
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        fmt.Printf("客户端連接協議升級失敗: %v\n", err)
        return
    }
    Clients[conn] = true
}

// 等待消息中
func WaitMessage() {
    go func() {
        for {
            select {
            case msg, ok := <-MsgQueue:
                if ok {
                    for client := range Clients {
                        err := client.WriteMessage(websocket.TextMessage, []byte("Go語言消息中心: "+string(msg)))
                        if err != nil {
                            fmt.Printf("消息推送失敗: %v\n", err)
                        }
                    }
                }
            default:
                // 避免忙等
                time.Sleep(500 * time.Millisecond)
            }
        }
    }()
}

在 go_websocket 項目中創建 message 控制器。這個控制器的主要作用是接收外部的消息內容,然後推送到 MsgQueue 通道中,這裏提供的是 API 接口,這個接口可以在外部的後台系統調用。這裏和 PHP 中有一點不同的是,在 Go 中無需引入像 Redis 一樣的第三方組件,而是利用自身的 Channel 特性即可實現消息的傳遞。

// ./php_to_go/go_websocket/app/controller/message.php
package controller

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func PushMsg(c *gin.Context) {
    // 接收 GET 參數
    content := c.Query("content")
    if len(content) == 0 {
        c.JSON(http.StatusOK, gin.H{
            "msg":  "內容不能為空",
            "code": -1,
        })
        return
    }

    // 往通道推送消息
    MsgQueue <- content

    c.JSON(http.StatusOK, gin.H{
        "msg":  "ok",
        "code": 0,
    })
}

運行 go run main.go 啓動服務,然後進行消息推送測試。

通過這兩個簡單的例子,我相信大家已經對 WebSocket 技術已經有所瞭解吧。從例子中也可以看出來,其實在 PHP 和 Go 中實現上有所區別,PHP 中需要啓動兩個服務,一個是 HTTP 服務,一個是 WebSocket 服務,而且兩者服務直接都是單獨的進程,不能相互通信,需要額外借助第三方中間件 Redis 來實現數據的傳輸。反觀 Go 中直接一個服務涵蓋了 HTTP 服務和 WebSocket 服務,共享一個進程的數據資源,通過使用 Channel 通道傳遞消息。

此外,在 PHP 中需要使用 Timer 異步定時器來讀取 Redis 消息隊列中的數據,不能用 for 循環或者 Redis 的阻塞隊列,因為它會阻塞整個進程的執行。而在 Go 中直接開啓一個協程,在協程中等待通道中的消息即可,會一直阻塞到消息的到來,而且它不會阻塞整個進程的執行,由此可見在這個例子中 Go 相較於 PHP 的優勢顯著。最後可能有些從來沒有使用過 WebSocket 技術的朋友,可能看完這篇文章之後也依然會雲裏霧裏,所以建議這些朋友可以自己親自實踐一下文中的案例,實踐過後我相信你會別有一番技術體驗。如果有想要獲取完整案例代碼的朋友,可以在公眾號內回覆「2463」即可,希望對大家能有所幫助。

感謝大家閲讀,個人觀點僅供參考,歡迎在評論區發表不同觀點。

歡迎關注、分享、點贊、收藏、在看,我是微信公眾號「碼農先森」作者。

user avatar laoduan 頭像 tpwonline 頭像 skyselang 頭像 huaihuaidehongdou 頭像 kinra 頭像 kubeexplorer 頭像 yian 頭像 runyubingxue 頭像 invalidnull 頭像 sy_records 頭像 mangrandechangjinglu 頭像 meiyoufujideyidongdianyuan 頭像
點贊 34 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.