基于golang的websocket

項目中的消息通知用到了websocket卓鹿,感覺比http長連接分塊發(fā)送好用,特此記錄一下留荔。
WebSocket協(xié)議用ws表示吟孙。此外碘勉,還有wss協(xié)議,表示加密的WebSocket協(xié)議,對應(yīng)HTTPs協(xié)議楞件。
完成握手以后墓阀,WebSocket協(xié)議就在TCP協(xié)議之上,開始傳送數(shù)據(jù)

websocket原理及運行機制

WebSocket是HTML5下一種新的協(xié)議拓轻。它實現(xiàn)了瀏覽器與服務(wù)器全雙工通信斯撮,能更好的節(jié)省服務(wù)器資源和帶寬并達(dá)到實時通訊的目的。它與HTTP一樣通過已建立的TCP連接來傳輸數(shù)據(jù)扶叉,但是它和HTTP最大不同是:WebSocket是一種雙向通信協(xié)議吮成。在建立連接后,WebSocket服務(wù)器端和客戶端都能主動向?qū)Ψ桨l(fā)送或接收數(shù)據(jù)辜梳,就像Socket一樣;WebSocket需要像TCP一樣泳叠,先建立連接作瞄,連接成功后才能相互通信。傳統(tǒng)HTTP客戶端與服務(wù)器請求響應(yīng)模式如下圖所示:

WebSocket模式客戶端與服務(wù)器請求響應(yīng)模式如下圖:

上圖對比可以看出危纫,==相對于傳統(tǒng)HTTP每次請求-應(yīng)答都需要客戶端與服務(wù)端建立連接的模式宗挥,WebSocket是類似Socket的TCP長連接通訊模式。一旦WebSocket連接建立后种蝶,后續(xù)數(shù)據(jù)都以幀序列的形式傳輸契耿。在客戶端斷開WebSocket連接或Server端中斷連接前,不需要客戶端和服務(wù)端重新發(fā)起連接請求==螃征。在海量并發(fā)及客戶端與服務(wù)器交互負(fù)載流量大的情況下搪桂,極大的節(jié)省了網(wǎng)絡(luò)帶寬資源的消耗,有明顯的性能優(yōu)勢,且客戶端發(fā)送和接受消息是在同一個持久連接上發(fā)起踢械,實時性優(yōu)勢明顯酗电。

相比HTTP長連接,WebSocket有以下特點:

  • 是真正的全雙工方式内列,建立連接后客戶端與服務(wù)器端是完全平等的撵术,可以互相主動請求。而HTTP長連接基于HTTP话瞧,是傳統(tǒng)的客戶端對服務(wù)器發(fā)起請求的模式嫩与。HTTP長連接中,每次數(shù)據(jù)交換除了真正的數(shù)據(jù)部分外交排,服務(wù)器和客戶端還要大量交換HTTP header划滋,信息交換效率很低。

  • Websocket協(xié)議通過第一個request建立了TCP連接之后个粱,之后交換的數(shù)據(jù)都不需要發(fā)送 HTTP header就能交換數(shù)據(jù)古毛,這顯然和原有的HTTP協(xié)議有區(qū)別所以它需要對服務(wù)器和客戶端都進(jìn)行升級才能實現(xiàn)(主流瀏覽器都已支持HTML5)。

  • 此外還有 multiplexing都许、不同的URL可以復(fù)用同一個WebSocket連接等功能稻薇。這些都是HTTP長連接不能做到的。

  • 連接建立后定期的心跳檢測

在客戶端胶征,new WebSocket實例化一個新的WebSocket客戶端對象塞椎,請求類似 ws://yourdomain:port/path 的服務(wù)端WebSocket URL,客戶端WebSocket對象會自動解析并識別為WebSocket請求睛低,并連接服務(wù)端端口案狠,執(zhí)行雙方握手過程,客戶端發(fā)送數(shù)據(jù)格式類似:

GET /webfin/websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost:8080
Sec-WebSocket-Version: 13

可以看到钱雷,客戶端發(fā)起的WebSocket連接報文類似傳統(tǒng)HTTP報文

  • Upgrade:websocket參數(shù)值表明這是WebSocket類型請求骂铁,

  • Sec-WebSocket-Key是WebSocket客戶端發(fā)送的一個 base64編碼的密文,要求服務(wù)端必須返回一個對應(yīng)加密的Sec-WebSocket-Accept應(yīng)答罩抗,否則客戶端會拋出Error during WebSocket handshake錯誤拉庵,并關(guān)閉連接。

Upgrade: websocket
Connection: Upgrade

這個就是Websocket的核心了套蒂,告訴Apache钞支、Nginx等服務(wù)器進(jìn)行協(xié)議轉(zhuǎn)換

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

首先,Sec-WebSocket-Key 是一個Base64 encode的值操刀,這個是瀏覽器隨機生成的烁挟,告訴服務(wù)器驗證websocket協(xié)議。
然后骨坑,Sec_WebSocket-Protocol 是一個用戶定義的字符串撼嗓,用來區(qū)分同URL下,不同的服務(wù)所需要的協(xié)議。

服務(wù)端收到報文后返回的數(shù)據(jù)格式類似:

HTTP/1.1 101     Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=
Sec-WebSocket-Accept

的值是服務(wù)端采用與客戶端一致的密鑰計算出來后返回客戶端的

HTTP/1.1 101 Switching Protocols表示服務(wù)端接受WebSocket協(xié)議的客戶端連接静稻,經(jīng)過這樣的請求-響應(yīng)處理后警没,兩端的WebSocket連接握手成功, 后續(xù)就可以進(jìn)行TCP通訊了。用戶可以查閱WebSocket協(xié)議棧了解WebSocket客戶端和服務(wù)端更詳細(xì)的交互數(shù)據(jù)格式振湾。

在開發(fā)方面杀迹,WebSocket API 也十分簡單:只需要實例化 WebSocket,創(chuàng)建連接押搪,然后服務(wù)端和客戶端就可以相互發(fā)送和響應(yīng)消息树酪。在WebSocket 實現(xiàn)及案例分析部分可以看到詳細(xì)的 WebSocket API 及代碼實現(xiàn)。

golang中的websokect

github.com/gorilla/websocket

項目中主要使用 github.com/gorilla/websocket 這個包大州。

通過上面對websocket原理的描述可以知道续语,http到websocket有一個協(xié)議轉(zhuǎn)換的過程,重點關(guān)注 Upgrade服務(wù)端協(xié)議轉(zhuǎn)換函數(shù)厦画。

// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
//
// The responseHeader is included in the response to the client's upgrade
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
// application negotiated subprotocol (Sec-Websocket-Protocol).
//
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
// response.
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
    if r.Method != "GET" {
        return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET")
    }
    
    if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
        return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported")
    }
    
    if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
        return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header")
    }
    
    if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
        return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header")
    }
    
    if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
        return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
    }
    
    checkOrigin := u.CheckOrigin
    if checkOrigin == nil {
        checkOrigin = checkSameOrigin
    }
    if !checkOrigin(r) {
        return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed")
    }
    
    challengeKey := r.Header.Get("Sec-Websocket-Key")
    if challengeKey == "" {
        return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank")
    }
    
    subprotocol := u.selectSubprotocol(r, responseHeader)
    
    // Negotiate PMCE
    var compress bool
    if u.EnableCompression {
        for _, ext := range parseExtensions(r.Header) {
            if ext[""] != "permessage-deflate" {
                continue
            }
            compress = true
            break
        }
    }
    
    var (
        netConn net.Conn
        err     error
    )
    
    h, ok := w.(http.Hijacker)
    if !ok {
        return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
    }
    var brw *bufio.ReadWriter
    netConn, brw, err = h.Hijack()
    if err != nil {
        return u.returnError(w, r, http.StatusInternalServerError, err.Error())
    }
    
    if brw.Reader.Buffered() > 0 {
        netConn.Close()
        return nil, errors.New("websocket: client sent data before handshake is complete")
    }
    
    c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)
    c.subprotocol = subprotocol
    
    if compress {
        c.newCompressionWriter = compressNoContextTakeover
        c.newDecompressionReader = decompressNoContextTakeover
    }
    
    p := c.writeBuf[:0]
    p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
    p = append(p, computeAcceptKey(challengeKey)...)
    p = append(p, "\r\n"...)
    if c.subprotocol != "" {
        p = append(p, "Sec-Websocket-Protocol: "...)
        p = append(p, c.subprotocol...)
        p = append(p, "\r\n"...)
    }
    if compress {
        p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
    }
    for k, vs := range responseHeader {
        if k == "Sec-Websocket-Protocol" {
            continue
        }
        for _, v := range vs {
            p = append(p, k...)
            p = append(p, ": "...)
            for i := 0; i < len(v); i++ {
                b := v[i]
                if b <= 31 {
                    // prevent response splitting.
                    b = ' '
                }
                p = append(p, b)
            }
            p = append(p, "\r\n"...)
        }
    }
    p = append(p, "\r\n"...)
    
    // Clear deadlines set by HTTP server.
    netConn.SetDeadline(time.Time{})
    
    if u.HandshakeTimeout > 0 {
        netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
    }
    if _, err = netConn.Write(p); err != nil {
        netConn.Close()
        return nil, err
    }
    if u.HandshakeTimeout > 0 {
        netConn.SetWriteDeadline(time.Time{})
    }
    
    return c, nil
}

通過該函數(shù)可以看到大致流程:

  • 判斷請求方法是否為GET疮茄,不是GET則為非法握手方法
  • 根據(jù)client的請求頭信息,確認(rèn)升級協(xié)議
  • 校驗跨域
  • 填充響應(yīng)頭根暑,響應(yīng)返回客戶端力试,鏈接建立

具體實現(xiàn)

Server端

主要采用Upgrade函數(shù)進(jìn)行協(xié)議轉(zhuǎn)換。指定了ReadBufferSize排嫌、WriteBufferSize畸裳、HandshakeTimeout參數(shù),同時跨域叫為采用默認(rèn)校驗函數(shù)淳地,自定義的校驗函數(shù)總是返回true跳過了跨域校驗

//controller
type MyWebSocketController struct {
    beego.Controller
}

var upgrader = websocket.Upgrader{
    ReadBufferSize:   1024,
    WriteBufferSize:  1024,
    HandshakeTimeout: 5 * time.Second,
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

func (c *MyWebSocketController) Get() {

    ws, err := upgrader.Upgrade(c.Ctx.ResponseWriter, c.Ctx.Request, nil)
    if err != nil {
        log.Fatal(err)
    }

    socket.Clients.Set(ws, true)

    _, body, _ := ws.ReadMessage()

    msg := socket.Message{Message: string(body)}
    socket.Broadcast <- msg

}

消息處理及轉(zhuǎn)發(fā)

var (
    Clients   = make(map[*websocket.Conn]bool, 1024)
    Broadcast = make(chan Message, 1024)
)

type Message struct {
    Message string `json:"message"`
}

func init() {
    go handleMessages()
}

//廣播發(fā)送至頁面
func handleMessages() {
    for {
        msg := <-Broadcast

        for client := range Clients {
            err := client.WriteJSON(msg)
            if err != nil {
                client.Close()
                delete(Clients, client)
            }
        }
    }
}

路由注冊(采用beego的注解式路由無法完成協(xié)議轉(zhuǎn)換怖糊,具體原因還未找到)

beego.Router("/ws", &noticeMq.MyWebSocketController{})

go client

采用golang自帶的golang.org/x/net/websocket包發(fā)送消息

package websocket

import (
    "net/url"

    "github.com/astaxie/beego"

    "golang.org/x/net/websocket"
)

type Client struct {
    Host string
    Path string
}

func NewWebsocketClient(host, path string) *Client {
    return &Client{
        Host: host,
        Path: path,
    }
}

func (this *Client) SendMessage(body []byte) error {
    u := url.URL{Scheme: "ws", Host: this.Host, Path: this.Path}

    ws, err := websocket.Dial(u.String(), "", "http://"+this.Host+"/")
    defer ws.Close() //關(guān)閉連接
    if err != nil {
        beego.Error(err)
        return err
    }

    _, err = ws.Write(body)
    if err != nil {
        beego.Error(err)
        return err
    }

    return nil
}

js client

目前主流瀏覽器都支持WebSocket協(xié)議(包括IE 10+)

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>Sample of websocket with golang</title>
    <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>

    <script>
        $(function() {
            var ws = new WebSocket('ws://api.mdevo.com/ws');

            ws.onopen = function(e) {
                $('<li>').text("connected").appendTo($ul);
            }

            ws.onmessage = function(e) {
                $('<li>').text(event.data).appendTo($ul);
            };
            var $ul = $('#msg-list');
        });
    </script>
</head>

<body>
    <ul id="msg-list"></ul>
</body>

</html>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市颇象,隨后出現(xiàn)的幾起案子伍伤,更是在濱河造成了極大的恐慌,老刑警劉巖遣钳,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚷缭,死亡現(xiàn)場離奇詭異,居然都是意外死亡耍贾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門路幸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來荐开,“玉大人,你說我怎么就攤上這事简肴』翁” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長能扒。 經(jīng)常有香客問我佣渴,道長,這世上最難降的妖魔是什么初斑? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任辛润,我火速辦了婚禮,結(jié)果婚禮上见秤,老公的妹妹穿的比我還像新娘砂竖。我一直安慰自己,他們只是感情好鹃答,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布乎澄。 她就那樣靜靜地躺著,像睡著了一般测摔。 火紅的嫁衣襯著肌膚如雪置济。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天锋八,我揣著相機與錄音浙于,去河邊找鬼。 笑死查库,一個胖子當(dāng)著我的面吹牛路媚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播樊销,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼整慎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了围苫?” 一聲冷哼從身側(cè)響起裤园,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎剂府,沒想到半個月后拧揽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡腺占,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年淤袜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衰伯。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡铡羡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出意鲸,到底是詐尸還是另有隱情烦周,我是刑警寧澤尽爆,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站读慎,受9級特大地震影響漱贱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜夭委,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一幅狮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闰靴,春花似錦彪笼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杏死,卻和暖如春泵肄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背淑翼。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工腐巢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玄括。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓冯丙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親遭京。 傳聞我的和親對象是個殘疾皇子胃惜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)哪雕,斷路器船殉,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • 1.OkHttp源碼解析(一):OKHttp初階2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HT...
    隔壁老李頭閱讀 20,824評論 24 176
  • 年初開始忙碌到現(xiàn)在利虫,算是可以告一段落喘口氣了。 和M聊天堡僻,告訴我她又要準(zhǔn)備跳槽了糠惫。記得去年11月才換到事業(yè)單位,她...
    沅辰_chris閱讀 235評論 0 1
  • 能百毒不侵的人钉疫,都曾傷痕累累過硼讽;能笑看風(fēng)云的人,都曾千瘡百孔過陌选。每個自強不息的人理郑,都曾無處可依過;每個看淡...
    我也曾林間過看淡云與月閱讀 680評論 0 3
  • 有時我在想: 撒但有痛感嗎 地獄之火能灼痛他嗎 人的靈魂 硫磺之火 地獄門口轉(zhuǎn)著的刀劍 會灼燒傷害那飄蕩的靈魂嗎 ...
    子興閱讀 1,384評論 7 9