基于golang的websocket

项目中的消息通知用到了websocket,感觉比http长连接分块发送好用,特此记录一下。
WebSocket协议用ws表示。此外,还有wss协议,表示加密的WebSocket协议,对应HTTPs协议。
完成握手以后,WebSocket协议就在TCP协议之上,开始传送数据

websocket原理及运行机制

WebSocket是HTML5下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。它与HTTP一样通过已建立的TCP连接来传输数据,但是它和HTTP最大不同是:WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样;WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。传统HTTP客户端与服务器请求响应模式如下图所示:

WebSocket模式客户端与服务器请求响应模式如下图:

上图对比可以看出,==相对于传统HTTP每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求==。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。

相比HTTP长连接,WebSocket有以下特点:

  • 是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。

  • Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。

  • 此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能。这些都是HTTP长连接不能做到的。

  • 连接建立后定期的心跳检测

在客户端,new WebSocket实例化一个新的WebSocket客户端对象,请求类似 ws://yourdomain:port/path 的服务端WebSocket URL,客户端WebSocket对象会自动解析并识别为WebSocket请求,并连接服务端端口,执行双方握手过程,客户端发送数据格式类似:

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

可以看到,客户端发起的WebSocket连接报文类似传统HTTP报文

  • Upgrade:websocket参数值表明这是WebSocket类型请求,

  • Sec-WebSocket-Key是WebSocket客户端发送的一个 base64编码的密文,要求服务端必须返回一个对应加密的Sec-WebSocket-Accept应答,否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。

Upgrade: websocket
Connection: Upgrade

这个就是Websocket的核心了,告诉Apache、Nginx等服务器进行协议转换

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

首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器验证websocket协议。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。

服务端收到报文后返回的数据格式类似:

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

的值是服务端采用与客户端一致的密钥计算出来后返回客户端的

HTTP/1.1 101 Switching Protocols表示服务端接受WebSocket协议的客户端连接,经过这样的请求-响应处理后,两端的WebSocket连接握手成功, 后续就可以进行TCP通讯了。用户可以查阅WebSocket协议栈了解WebSocket客户端和服务端更详细的交互数据格式。

在开发方面,WebSocket API 也十分简单:只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息。在WebSocket 实现及案例分析部分可以看到详细的 WebSocket API 及代码实现。

golang中的websokect

github.com/gorilla/websocket

项目中主要使用 github.com/gorilla/websocket 这个包。

通过上面对websocket原理的描述可以知道,http到websocket有一个协议转换的过程,重点关注 Upgrade服务端协议转换函数。

// 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
}

通过该函数可以看到大致流程:

  • 判断请求方法是否为GET,不是GET则为非法握手方法
  • 根据client的请求头信息,确认升级协议
  • 校验跨域
  • 填充响应头,响应返回客户端,链接建立

具体实现

Server端

主要采用Upgrade函数进行协议转换。指定了ReadBufferSize、WriteBufferSize、HandshakeTimeout参数,同时跨域叫为采用默认校验函数,自定义的校验函数总是返回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

}

消息处理及转发

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

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

func init() {
    go handleMessages()
}

//广播发送至页面
func handleMessages() {
    for {
        msg := <-Broadcast

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

路由注册(采用beego的注解式路由无法完成协议转换,具体原因还未找到)

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

go client

采用golang自带的golang.org/x/net/websocket包发送消息

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() //关闭连接
    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协议(包括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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 1.OkHttp源码解析(一):OKHttp初阶2 OkHttp源码解析(二):OkHttp连接的"前戏"——HT...
    隔壁老李头阅读 20,804评论 24 176
  • 年初开始忙碌到现在,算是可以告一段落喘口气了。 和M聊天,告诉我她又要准备跳槽了。记得去年11月才换到事业单位,她...
    沅辰_chris阅读 234评论 0 1
  • 能百毒不侵的人,都曾伤痕累累过;能笑看风云的人,都曾千疮百孔过。每个自强不息的人,都曾无处可依过;每个看淡...
    我也曾林间过看淡云与月阅读 674评论 0 3
  • 有时我在想: 撒但有痛感吗 地狱之火能灼痛他吗 人的灵魂 硫磺之火 地狱门口转着的刀剑 会灼烧伤害那飘荡的灵魂吗 ...
    子兴阅读 1,366评论 7 9