在早些年前客戶端想要實(shí)時(shí)獲取到最新消息,都是使用定時(shí)長輪詢的方式,不斷的從服務(wù)器上獲取數(shù)據(jù),這種粗暴的騷操作實(shí)屬不雅。不過現(xiàn)如今我也還見有人還在一些場(chǎng)景下使用,比如在 PC 端掃描二維碼,然后使用長輪詢的方式從服務(wù)端獲取最新的掃碼信息,來判斷用戶是否已經(jīng)掃碼完成,諸如這種場(chǎng)景還有不少。其實(shí)大家都知道長輪詢的方式不好,那為什么還有人使用呢?
我想最直接的原因就是「開發(fā)起來簡(jiǎn)單明了」,人性決定了人類都是趨易避難的高級(jí)物種,那個(gè)容易上手就用那個(gè)。但是我想表達(dá)的是除了長輪詢的方式外,WebSocket 技術(shù)其實(shí)也不難,只不過對(duì)于從來沒有接觸過長連接的人來說,剛開始上手時(shí)會(huì)有一些思維上的障礙。這次我分享的內(nèi)容是基于 WebSocket 技術(shù)的消息推送中心,看起來很高大上,其實(shí)也就是通過一些小的例子來演示,從服務(wù)端推送數(shù)據(jù)到客戶端的這個(gè)過程,接下來的例子簡(jiǎn)單明了容易上手,我們趕緊開始吧。
話不多說,開整!我們先來看一下整體的項(xiàng)目目錄結(jié)構(gòu),內(nèi)容主要分為 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 創(chuàng)建基于 ThinkPHP 框架的 php_websocket 項(xiàng)目。
## 當(dāng)前目錄
[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 命令創(chuàng)建 Worker.php 控制器。這個(gè)控制器中主要實(shí)現(xiàn)了 onWorkerStart 這個(gè)方法,首先添加了一個(gè) Timer 異步定時(shí)器,然后從 Redis 隊(duì)列中讀取消息,最后將消息推送到客戶端,這個(gè)定時(shí)器會(huì)每間隔一秒鐘調(diào)度一次。
// ./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) {
// 添加一個(gè)異步定時(shí)器任務(wù)
Timer::add(1, function () use ($worker) {
// 從消息中心隊(duì)列中讀取消息
$redis = Cache::store('redis')->handler();
$content = $redis->rpop(Env::get("MESSAGE_CENTER_KEY"));
// 發(fā)送消息到客戶端
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 命令創(chuàng)建 Push.php 控制器。這個(gè)控制器的主要作用是接收外部的消息內(nèi)容,然后推送到 Redis 消息隊(duì)列中,這里提供的是 API 接口,這個(gè)接口可以在外部的后臺(tái)系統(tǒng)調(diào)用。
// ./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 參數(shù)
$params = $this->request->param();
if (empty($params["content"])) {
return json(["code" => -1, "msg" => "內(nèi)容不能為空"]);
}
$content = $params["content"];
// 推送消息到消息中心隊(duì)列
$redis = Cache::store('redis')->handler();
$redis->lpush(Env::get("MESSAGE_CENTER_KEY"), $content);
return json(["code" => 0, "msg" => "success"]);
}
}
先運(yùn)行 php think worker 啟動(dòng) HTTP 服務(wù),再運(yùn)行 php think worker:server 啟動(dòng) WebSocket 服務(wù),最后來測(cè)試一波。
Gin
通過 go mod 初始化 go_websocket 項(xiàng)目。
## 當(dāng)前目錄
[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/go_websocket
## 初始化項(xiàng)目
[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 項(xiàng)目中創(chuàng)建 websocket 控制器。這個(gè)控制器會(huì)將客戶端連接存儲(chǔ)到指定的 Map 數(shù)據(jù)結(jié)構(gòu)中,其次還提供了 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"
)
// 定義一個(gè)消息傳輸通道
var MsgQueue = make(chan string, 10)
// 定義一個(gè)存儲(chǔ)客戶端連接的 Map
var Clients = make(map[*websocket.Conn]bool)
// 將 HTTP 協(xié)議升級(jí)至 WebSocket 協(xié)議
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 允許所有來源
},
}
// 將客戶端連接存儲(chǔ)到 Map
func HandleConnection(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
fmt.Printf("客戶端連接協(xié)議升級(jí)失敗: %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 項(xiàng)目中創(chuàng)建 message 控制器。這個(gè)控制器的主要作用是接收外部的消息內(nèi)容,然后推送到 MsgQueue 通道中,這里提供的是 API 接口,這個(gè)接口可以在外部的后臺(tái)系統(tǒng)調(diào)用。這里和 PHP 中有一點(diǎn)不同的是,在 Go 中無需引入像 Redis 一樣的第三方組件,而是利用自身的 Channel 特性即可實(shí)現(xiàn)消息的傳遞。
// ./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 參數(shù)
content := c.Query("content")
if len(content) == 0 {
c.JSON(http.StatusOK, gin.H{
"msg": "內(nèi)容不能為空",
"code": -1,
})
return
}
// 往通道推送消息
MsgQueue <- content
c.JSON(http.StatusOK, gin.H{
"msg": "ok",
"code": 0,
})
}
運(yùn)行 go run main.go 啟動(dòng)服務(wù),然后進(jìn)行消息推送測(cè)試。
通過這兩個(gè)簡(jiǎn)單的例子,我相信大家已經(jīng)對(duì) WebSocket 技術(shù)已經(jīng)有所了解吧。從例子中也可以看出來,其實(shí)在 PHP 和 Go 中實(shí)現(xiàn)上有所區(qū)別,PHP 中需要啟動(dòng)兩個(gè)服務(wù),一個(gè)是 HTTP 服務(wù),一個(gè)是 WebSocket 服務(wù),而且兩者服務(wù)直接都是單獨(dú)的進(jìn)程,不能相互通信,需要額外借助第三方中間件 Redis 來實(shí)現(xiàn)數(shù)據(jù)的傳輸。反觀 Go 中直接一個(gè)服務(wù)涵蓋了 HTTP 服務(wù)和 WebSocket 服務(wù),共享一個(gè)進(jìn)程的數(shù)據(jù)資源,通過使用 Channel 通道傳遞消息。
此外,在 PHP 中需要使用 Timer 異步定時(shí)器來讀取 Redis 消息隊(duì)列中的數(shù)據(jù),不能用 for 循環(huán)或者 Redis 的阻塞隊(duì)列,因?yàn)樗鼤?huì)阻塞整個(gè)進(jìn)程的執(zhí)行。而在 Go 中直接開啟一個(gè)協(xié)程,在協(xié)程中等待通道中的消息即可,會(huì)一直阻塞到消息的到來,而且它不會(huì)阻塞整個(gè)進(jìn)程的執(zhí)行,由此可見在這個(gè)例子中 Go 相較于 PHP 的優(yōu)勢(shì)顯著。最后可能有些從來沒有使用過 WebSocket 技術(shù)的朋友,可能看完這篇文章之后也依然會(huì)云里霧里,所以建議這些朋友可以自己親自實(shí)踐一下文中的案例,實(shí)踐過后我相信你會(huì)別有一番技術(shù)體驗(yàn)。
注:本文轉(zhuǎn)載自“碼農(nóng)先森”,如有侵權(quán),請(qǐng)聯(lián)系刪除!