在年初的時候,咱們有點兒小迷茫,因而也跟風去作了一些輕娛樂類的小遊戲。
那時爲了實戰對戰,想到須要一個實時性很強的技術實現,因而我去實現了一個websocket server,沒想到後來這些小程序沒有成,可是咱們的這個web socket server 演化得無處不在。下面介紹一下這個技術實現。web
看理論確定會有點拗口是否是,咱們直接上代碼就得了。咱們如今假設有這麼一個用戶付款的邏輯,在寫用戶付款事件時,咱們事先並不知道之後還須要加什麼邏輯,因而咱們先把這個行爲廣播出去。如下是僞代碼:redis
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,線上運行起來,就行了。json
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");
});
複製代碼
發送通知郵件和發送告知短信,都基於用戶付款動做,可是發郵件和發短信的代碼徹底隔離,相互之間出徹底不知道對方的存在。bash
是否是很贊?那咱們接下來梳理一下邏輯。服務器
也許咱們來不及去翻看websocket的定義,可是咱們能夠簡單地理解,Websocket是對HTTP協議的一個擴展升級,在發起鏈接時,HTTP部分都是有效的,只是鏈接成功之後,服務端和客戶端的鏈接不斷,雙方能夠雙向數據傳輸,且服務端能夠主動向客戶端推送數據。websocket
咱們看一次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秒沒有收到這個心跳包,證實這個日誌服務器掛掉了,就發一條報警短信,通知同窗去看看這個服務。
再好比,咱們在日誌服務上的應用,參見這裏:十二贊日誌系統簡介