最近在梳理一些知識點,已脫敏並去除公司實現,做一些自己理解上的實踐。
結構
本次打算模擬下一個實時雙工交互的業務實踐,先來張圖。
可以看出,實時雙工通信的基礎在於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服務器
3.PHP去publish消息
➜ php publish.php
int(1)
4.查看客戶端是否可以收到websocket消費到的數據
整理
至此,基本上雙工實時通信的demo就結束了。相比於公司Java實現的版本,大體框架是類似的,無非是有沒有業務的支撐。