在年初的時候,我們有點兒小迷茫,於是也跟風去做了一些輕娛樂類的小遊戲。
那時為了實戰對戰,想到需要一個實時性很強的技術實現,於是我去實現了一個websocket server,沒想到後來這些小程式沒有成,但是我們的這個web socket server 演化得無處不在。下面介紹一下這個技術實現。
看理論肯定會有點拗口是不是,我們直接上程式碼就得了。我們現在假設有這麼一個使用者付款的邏輯,在寫使用者付款事件時,我們事先並不知道以後還需要加什麼邏輯,於是我們先把這個行為廣播出去。以下是虛擬碼:
req := httplib.Post("https://ws.app.12zan.net/eventcast/user/5905e89db43fec42e3055df05ff72afe")
text, er := zanjson.Encode(order)
if er != nil {
log.Println(ev)
return
}
req.Param("data", string(text))
resp,_ = req.Response()
複製程式碼
好了,現在,每當有使用者付款時,這個使用者系統都會往/eventcast/user/5905e89db43fec42e3055df05ff72afe這個頻道廣播一條訊息。但是很遺憾,目前沒有客戶端訂閱這類訊息,所有的訊息都被丟棄了。
有一天,我們英明神武的老闆決定要加一個通知,每當有一個新的使用者付款時,都給公司的同胞們發一個郵件通知一下,我們獲得了新的付費使用者,好讓大家小開心一把,尤其是第一個試用客戶付費的時候,我們肯定都要開心地跳起來。這時我們如果去改線上執行好的付款系統,還是有點兒風險的,一旦有修改,我們就得走一下測試流程,不然萬一有問題不是影響公司發財了嗎。沒關係,我們之前不是已經把付款事件廣播出來了嗎,我們現在用起來。寫這麼一段js,線上執行起來,就好了。
const webSocket = require(`ws`);
let ws = new webSocket("wss://ws.app.12zan.net/eventcast/user/5905e89db43fec42e3055df05ff72afe");
ws.on(`open`, function open() {
console.log("connected");
});
ws.on(`message`, function incoming(data) {
let user = JSON.parse(data);
Mail.send("一個叫"+user.name+"的好心人支付了"+user.amount+"元,讓主讚美他!");
});
複製程式碼
好了,現在一旦有人付款,我們全公司都能收到一個郵件,及時得到這一好訊息了。讓我們小小地慶祝一下吧。
接下來又過了幾天,我們想改進一下體驗,使用者一旦付款成功,就傳送一條簡訊,告知使用者他的有效期和我們的24小時客服電話;只需要這麼一段程式碼部署起來執行就好了, 之前的任何程式碼都不用動:
const webSocket = require(`ws`);
let ws = new webSocket("wss://ws.app.12zan.net/eventcast/user/5905e89db43fec42e3055df05ff72afe");
ws.on(`open`, function open() {
console.log("connected");
});
ws.on(`message`, function incoming(data) {
let user = JSON.parse(data);
let expiresAt = (zan.Date.now().add("+365 day").format("YYYY-mm-dd"));
SMS.send(user.Mobile,"尊敬的"+user.name+",您成功購買了十二贊旗艦版,有效期至"+expiresAt+",請登陸:https://www.12zan.cn 檢視,如有任何疑問,歡迎致電4006681102");
});
複製程式碼
傳送通知郵件和傳送告知簡訊,都基於使用者付款動作,但是發郵件和發簡訊的程式碼完全隔離,相互之間出完全不知道對方的存在。
是不是很贊?那我們接下來梳理一下邏輯。
概念及主要邏輯
也許我們來不及去翻看websocket的定義,但是我們可以簡單地理解,Websocket是對HTTP協議的一個擴充套件升級,在發起連線時,HTTP部分都是有效的,只是連線成功以後,服務端和客戶端的連線不斷,雙方可以雙向資料傳輸,且服務端可以主動向客戶端推送資料。
我們看一次Websocket發起連線的過程(來自維基百科):
客戶端向服務端發起連線:
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
服務端的返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
在HTTP協議中常見的欄位,如Cookies,Host等,依然有效。
但是具體到我們的應用上,十二讚的這個websocket server實現了兩個小目標【多遺憾了,並沒有賺到兩個億】:
1. 我們實現的是一個廣播系統,一個廣播系統意味著一個地方去傳送資料,n多個接受端來接受資料。要支援非常多的客戶端同時連上資料來實時接受資料。我們最終的server端的實現,全記憶體實現,沒有用redis或是MySQL類似的資料庫,就是為了實現超多客戶端的支援。
2. 我們希望採用最簡單、最通用的文案,並且,非常高效,支援非常多的客戶端同時連線,我們認為http協議更簡單,所以在傳送的時候,我們是走http協議來傳送資料的。並且,沒有任何安全上的設計,如果資料很重要,請自行加密之後傳送。
當然我們也有一些遺憾:
1. 允許資料丟失。有得必有失,我們允許一個比例的資訊丟失。產生資料丟失時,不影響主邏輯。就像剛才的例子,傳送郵件通知我們有新付款的這個事件沒有觸發並沒有關係,我們到下午才發現有新使用者付款,這時再去開香檳也不遲:(。
2. 容忍時序錯亂。像剛才的例子,有新使用者付款時,是先告訴我們全體同事有新付款,還是先給使用者傳送一條簡訊,並不那麼重要。
好了,回到我們的系統,我們給一點點總結。
我們定義,每個websocket的入口,都是一個URL;去掉協議和HOST部分,剩下的PATH部分代表了不同的頻道。比如,發起websocket時連線到ws://ws.app.12zan.net/channel/hello,那麼這個頻道地址就是/channel/hello;所有連線到ws.app.12zan.net/channel/hello的websocket客戶端,他們會收到一模一樣的訊息,我們稱之為訂閱。
同時,為了簡化發起資料的過程,我們還在websocket server中定義:當一個http 的客戶端,以POST方式請求某一個地址時,我們擷取URL中的PATH部分,得到頻道名,並取POST的資料中的data域,作為要廣播的資料,將之廣播到相應的頻道。
在十二讚的應用:
這個廣播系統,在十二讚的整個技術架構中,後來應用的特別廣。
比如,我們的部署系統zeus,在網頁端實現了一個客戶端,當服務端有應用重啟、關閉、啟動時,都會彈出訊息通知。任何在開啟了這個系統的網頁的人都能看到。比如我和同事小王都正在zeus的網頁上,我新建了一個search系統的一個節點,啟動完畢的時候,我和小王會收到通知,在第三號伺服器上新啟了一個search系統的節點。我在操作,很關心這個,所心這時我可以放心去繼續我的工作。小王正要在三號機器上新部署一個系統,他收到這個通知後,覺得這個機器可能會很忙,於是把自己的新例項部署在了四號機器上。
再比如,我們的日誌伺服器,擔負著收集所有伺服器上日誌的使命。但是如果它掛掉了呢?於是我們在這個日誌伺服器上跑了一個定時器,每5秒鐘向某個頻道廣播一條心跳訊息,告訴世界自己還活著。然後另行跑了一個程式,收聽這個頻道的廣播,如果連續30秒沒有收到這個心跳包,證明這個日誌伺服器掛掉了,就發一條報警簡訊,通知同學去看看這個服務。
再比如,我們在日誌服務上的應用,參見這裡:十二贊日誌系統簡介