WebSocket用Redis實時雙工通信

最近在梳理一些知識點,已脫敏並去除公司實現,做一些自己理解上的實踐。

結構

本次打算模擬下一個實時雙工交互的業務實踐,先來張圖。

模式結構圖

可以看出,實時雙工通信的基礎在於Redis部分,核心就在於Pub/Sub模型,其餘部分在此基礎上豐富了交互內容。

  • Server端 ,用於模擬平時業務機器,對來自客戶端的Request給予Response
  • WebSocket Server端,比如直播業務中在直播間內聊天,肯定要用websocket來維繫鏈接狀態,這裏可以做到語言無關,既可以用Java,也可以用golang。原理都是類似的。根據雙工的特徵,websocket服務器與客戶端發生信息交互的場景無非主動和被動,場景如下:
    • 主動觸發, 指的是來自另一個客戶端的action,觸發了websocket服務器的push行爲。
    • 被動觸發,比如定時器觸發,狀態檢查等行爲,都屬於被動觸發
  • APP端,一般團隊都會以APP形式落地到終端用戶,web網頁也是類似。在直播場景中,主播、觀衆實際上可以統一虛化爲客戶端

實現

從圖示上來看,每一個模塊都不難,一點點去實現就好了。因爲不想涉及公司業務上的東西,所以會有一些改動,如下:

  • APP端我這裏會用JavaScript來作爲客戶端,簡單模擬下就。
  • WebSocket服務器端,不打算用公司裏Java版本,想試試golang版本。
  • Server端就用PHP簡單模擬下。

websocket服務器端實現

// server.go
package main

import (
	"fmt"
	"log"
	"net/http"

	"time"

	"github.com/garyburd/redigo/redis"
	"golang.org/x/net/websocket"
)

var clients map[*websocket.Conn]string = make(map[*websocket.Conn]string)

// websocket 服務器端測試

func Echo(ws *websocket.Conn) {
	var err error
	if _, ok := clients[ws]; ok != true {
		clients[ws] = "匿名"
	}
	for {
		var reply string
		if err = websocket.Message.Receive(ws, &reply); err != nil {
			fmt.Println("Cannot receive")
			break
		}
		fmt.Println("Current client number: ", len(clients))
		fmt.Println("Received back from client: ", reply)
		msg := "RECEIVED: " + reply
		fmt.Println("Sending to client: " + msg)
		for client, _ := range clients {
			fmt.Println(client)
			if err = websocket.Message.Send(client, msg); err != nil {
				fmt.Println("Sending failed")
				break
			}

		}
	}
}

func tick() {
	for {
		time.Sleep(time.Second * 10)
		fmt.Println("checking ping")
		for key, _ := range clients {
			if key.IsClientConn() == false {
				// delete(clients, key)
			}
		}
		// 對所有客戶端進行訂閱消息的推送
		consume(clients)
	}
}

func consume(clients map[*websocket.Conn]string) {
	client, err := redis.Dial("tcp", "localhost:6379")
	defer client.Close()
	if err != nil {
		fmt.Println(err)
	}
	psc := redis.PubSubConn{Conn: client}
	psc.Subscribe("channel")
	for {
		switch v := psc.Receive().(type) {
		case redis.Message:
			fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
			for client, _ := range clients {
				fmt.Println(client)
				if err = websocket.Message.Send(client, string(v.Data)); err != nil {
					fmt.Println("Sending failed")
					break
				}
			}
		case redis.Subscription:
			fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
		}
	}
}

func main() {
	http.Handle("/", websocket.Handler(Echo))
	go tick()
	if err := http.ListenAndServe(":1234", nil); err != nil {
		log.Fatal("ListenAndServe failed: ", err)
	}
}

app端實現

// client.js
var wsServer = 'ws://localhost:1234';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
    console.log("Connected to WebSocket server.");
};

websocket.onclose = function (evt) {
    console.log("Disconnected");
};

websocket.onmessage = function (evt) {
    console.log('Retrieved data from server: ' + evt.data);
};

websocket.onerror = function (evt, e) {
    console.log('Error occured: ' + evt.data);
};
// 發送消息
websocket.send("hello world!")

server端實現

<?php

$redis = new Redis();
$redis->pconnect("localhost", 6379);
$ret = $redis->publish("channel", "data from PHP");
var_dump($ret);

測試

按照圖例,打算對主動觸發和被動觸發進行下測試。

主動觸發

主動觸發其實就是對websocket基本功能的測試,一般都是開倆客戶端,一段發消息,看看另一端是否能收到就可以了,流程是

1.啓動websocket服務器

→ go run server.go

2.客戶端連接websocket服務器

Chrome 打開調試器console輸入上面的JavaScript代碼即可。

3.交互測試
主動觸發測試

被動觸發

被動觸發一般都是定時任務,如定時器timer來觸發的。在上面golang代碼中,有這麼一段調用。

go tick()
// 內部調用了consume方法,來實現對subscribe消息的消費

1.啓動websocket服務器

➜ go run server.go
checking ping

2.客戶端js鏈接websocket服務器
javascripe鏈接websocket服務器
3.PHP去publish消息

➜ php publish.php
int(1)

4.查看客戶端是否可以收到websocket消費到的數據
查看客戶端的確收到了對應publish的消息

整理

至此,基本上雙工實時通信的demo就結束了。相比於公司Java實現的版本,大體框架是類似的,無非是有沒有業務的支撐。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章