Go WebSocket 的使用

WebSocket在 HTML5 游戏和网页消息推送都使用比较多。WebSocket 是 HTML5 的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信。
WebSocket 具体的特性和 http 的区别这里就不多说,可以去自己查一下。

Go 官方没有提供对 WebSocket 的支持,必须选择第三方提供的包。《Go Web 编程》一书中的例子使用了 golang.org/x/net 下的 websocket 包。 另外一个使用比较多的是 gorilla/websocket ,我接触的项目是使用的这个。下面我就以 gorilla/websocket 来写一个简单的通信示例。

gorilla/websocket 的资料参考:
GitHub:https://github.com/gorilla/websocket
Doc:https://godoc.org/github.com/gorilla/websocket

gorilla/websocket 简述

Upgrader

Upgrader 用于升级 http 请求,把 http 请求升级为长连接的 WebSocket。结构如下:

type Upgrader struct {
    // 指定升级 websocket 握手完成的超时时间
    HandshakeTimeout time.Duration

    // 指定 io 操作的缓存大小,如果不指定就会自动分配。
    ReadBufferSize, WriteBufferSize int

    // 写数据操作的缓存池,如果没有设置值,write buffers 将会分配到链接生命周期里。
    WriteBufferPool BufferPool

    //按顺序指定服务支持的协议,如值存在,则服务会从第一个开始匹配客户端的协议。
    Subprotocols []string

    // 指定 http 的错误响应函数,如果没有设置 Error 则,会生成 http.Error 的错误响应。
    Error func(w http.ResponseWriter, r *http.Request, status int, reason error)

    // 请求检查函数,用于统一的链接检查,以防止跨站点请求伪造。如果不检查,就设置一个返回值为true的函数。
    // 如果请求Origin标头可以接受,CheckOrigin将返回true。 如果CheckOrigin为nil,则使用安全默认值:如果Origin请求头存在且原始主机不等于请求主机头,则返回false
    CheckOrigin func(r *http.Request) bool

    // EnableCompression 指定服务器是否应尝试协商每个邮件压缩(RFC 7692)。 
    // 将此值设置为true并不能保证将支持压缩。 
    // 目前仅支持“无上下文接管”模式
    EnableCompression bool
}

func (*Upgrader) Upgrade

Upgrade 函数将 http 升级到 WebSocket 协议。定义如下:

// responseHeader包含在对客户端升级请求的响应中。 
// 使用responseHeader指定cookie(Set-Cookie)和应用程序协商的子协议(Sec-WebSocket-Protocol)。
// 如果升级失败,则升级将使用HTTP错误响应回复客户端
// 返回一个 Conn 指针,拿到他后,可使用 Conn 读写数据与客户端通信。
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error)

使用实例

type WsServer struct {
    ......
    // 定义一个 upgrade 类型用于升级 http 为 websocket
    upgrade  *websocket.Upgrader
}

func NewWsServer() *WsServer {
    ws.upgrade = &websocket.Upgrader{
        ReadBufferSize:  4096,//指定读缓存区大小
        WriteBufferSize: 1024,// 指定写缓存区大小
        // 检测请求来源
        CheckOrigin: func(r *http.Request) bool {
            if r.Method != "GET" {
                fmt.Println("method is not GET")
                return false
            }
            if r.URL.Path != "/ws" {
                fmt.Println("path error")
                return false
            }
            return true
        },
    }
    return ws
}

func (self *WsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ......
    // 收到 http 请求后 升级 协议
    conn, err := self.upgrade.Upgrade(w, r, nil)
    if err != nil {
        fmt.Println("websocket error:", err)
        return
    }
    fmt.Println("client connect :", conn.RemoteAddr())
    go self.connHandle(conn)

}

http 服务

启动一个 http 服务有多种方法

  • 第一种
func (self *WsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/v1" {
        httpCode := http.StatusInternalServerError
        reasePhrase := http.StatusText(httpCode)
        fmt.Println("path error ", reasePhrase)
        http.Error(w, reasePhrase, httpCode)
        return
    }
}

func (w *WsServer) Start() (err error) {
    // 使用 net.Listen 监听端口服务
    w.listener, err = net.Listen("tcp", w.addr)
    if err != nil {
        fmt.Println("net listen error:", err)
        return
    }
    // 启动 http 服务, w 参数类型需要 实现  Handler 接口,也就是 ServeHTTP 函数
    err = http.Serve(w.listener, w)
    if err != nil {
        fmt.Println("http serve error:", err)
        return
    }
    return nil
}
  • 第二种
import (
    "fmt"
    "io"
    "net/http"
)
func serveHandle(w http.ResponseWriter, r *http.Request) {
    buf := make([]byte, 1024)
    r.Body.Read(buf)
    fmt.Println("request body", string(buf))
    io.WriteString(w, "hello http server 1")
}
func main() {
    http.HandleFunc("/v1", serveHandle)
    // 直接使用 http 包的 ListenAndServe 函数监听服务
    http.ListenAndServe(s.addr, nil)
}

WebSocket 完整代码

http 服务 + Upgrade 实现 WebSocket

package main
import (
    "fmt"
    "net"
    "net/http"
    "time"
    "github.com/gorilla/websocket"
)
type WsServer struct {
    listener net.Listener
    addr     string
    upgrade  *websocket.Upgrader
}
func NewWsServer() *WsServer {
    ws := new(WsServer)
    ws.addr = "0.0.0.0:10215"
    ws.upgrade = &websocket.Upgrader{
        ReadBufferSize:  4096,
        WriteBufferSize: 1024,
        CheckOrigin: func(r *http.Request) bool {
            if r.Method != "GET" {
                fmt.Println("method is not GET")
                return false
            }
            if r.URL.Path != "/ws" {
                fmt.Println("path error")
                return false
            }
            return true
        },
    }
    return ws
}

func (self *WsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/ws" {
        httpCode := http.StatusInternalServerError
        reasePhrase := http.StatusText(httpCode)
        fmt.Println("path error ", reasePhrase)
        http.Error(w, reasePhrase, httpCode)
        return
    }
    conn, err := self.upgrade.Upgrade(w, r, nil)
    if err != nil {
        fmt.Println("websocket error:", err)
        return
    }
    fmt.Println("client connect :", conn.RemoteAddr())
    go self.connHandle(conn)

}
func (self *WsServer) connHandle(conn *websocket.Conn) {
    defer func() {
        conn.Close()
    }()
    stopCh := make(chan int)
    go self.send(conn, stopCh)
    for {
        conn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(5000)))
        _, msg, err := conn.ReadMessage()
        if err != nil {
            close(stopCh)
            // 判断是不是超时
            if netErr, ok := err.(net.Error); ok {
                if netErr.Timeout() {
                    fmt.Printf("ReadMessage timeout remote: %v\n", conn.RemoteAddr())
                    return
                }
            }
            // 其他错误,如果是 1001 和 1000 就不打印日志
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
                fmt.Printf("ReadMessage other remote:%v error: %v \n", conn.RemoteAddr(), err)
            }
            return
        }
        fmt.Println("收到消息:", string(msg))
    }
}

//测试一次性发送 10万条数据给 client, 如果不使用 time.Sleep browser 过了超时时间会断开
func (self *WsServer) send10(conn *websocket.Conn) {
    for i := 0; i < 100000; i++ {
        data := fmt.Sprintf("hello websocket test from server %v", time.Now().UnixNano())
        err := conn.WriteMessage(1, []byte(data))
        if err != nil {
            fmt.Println("send msg faild ", err)
            return
        }
        // time.Sleep(time.Millisecond * 1)
    }
}

func (self *WsServer) send(conn *websocket.Conn, stopCh chan int) {
    self.send10(conn)
    for {
        select {
        case <-stopCh:
            fmt.Println("connect closed")
            return
        case <-time.After(time.Second * 1):
            data := fmt.Sprintf("hello websocket test from server %v", time.Now().UnixNano())
            err := conn.WriteMessage(1, []byte(data))
            fmt.Println("sending....")
            if err != nil {
                fmt.Println("send msg faild ", err)
                return
            }
        }
    }
}

func (w *WsServer) Start() (err error) {
    w.listener, err = net.Listen("tcp", w.addr)
    if err != nil {
        fmt.Println("net listen error:", err)
        return
    }
    err = http.Serve(w.listener, w)
    if err != nil {
        fmt.Println("http serve error:", err)
        return
    }
    return nil
}

func main() {
    ws := NewWsServer()
    ws.Start()
}

WebSocket 客户端

  • 更新中
    VILEngine.js 文件
let VIL = (function () {
    let VIL = {
    };

    function DefaultWebSocket(host, call) {
        let _host = host;
        let _isOpen = false;
        let _bufQueue = [];
        let _bufCap = 100;
        let _call = null;
        if("undefined" !== typeof call && call !== null){
            _call = call
        }else{
            _call = {
                onConnect:function (e) {
                    console.log("connect success ", e);
                },
                onDisconnect:function (e) {
                    console.log("disconnect ", e);
                },
                onMsg:function (data) {
                    //console.log("receive message ", data)
                }
            }
        }


        let _socket = new WebSocket(_host);
        _socket.binaryType = "arraybuffer";
        /**
         * 设置发送消息缓存队列的容量
         * @param {number} cap
         * @constructor
         */
        this.setBufferCap = function(cap){
            if("number" !== typeof cap ){
                console.error("parameter type is not number ");
                return ;
            }
            if(cap < 0){
                console.error("parameter value can not less then 0");
                return ;
            }
            _bufCap = cap;
        };

        /**
         * 发送消息
         * @param {string | ArrayBuffer } data
         * @constructor
         */
        this.send = function(data){
            if(_isOpen && _socket){
                _socket.send("");
            }else{
                if (_bufQueue < _bufCap){
                    _bufQueue.push(data);
                }
            }
        };

        this.close = function(){
            _socket.close(1000, "normal");
        };

        _socket.onopen = function(even){
            _isOpen = true;
            _call.onConnect(even);
            while (_bufQueue > 0){
                _socket.send(_bufQueue.shift());
            }
        };

        _socket.onmessage = function(e){
            let data = e.data;
            _call.onMsg(data);
        };

        /**
         * 收到关闭连接
         * @param even
         */
        _socket.onclose = function(even){
            _isOpen = false;
            _call.onDisconnect({host:_host, event:even});
        };

        /**
         * 收到错误
         * @param err
         */
        _socket.onerror = function(err){
            _isOpen = false;
            _call.onDisconnect({host:_host, event:err});
        };
    }

    try{
        VIL.EngineSocket = DefaultWebSocket ;
    }catch (e) {
        console.error("VILEngine error ", e);
    }

    return VIL;
})();

websocket.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
</body>
<script type="text/javascript" src="VILEngine.js"></script>
<script>
    let counter  = 0;
    let isConnect = false;
    let handler = {
        onConnect:function (e) {
            isConnect = true;
            console.log("handler connect success ", e);
            var se = setInterval(function () {
                if(isConnect === false){
                    clearInterval(se);
                }
                console.log("setInterval", Date.now());
                socket.send("web browser setInterval");
            }, 3000)
        },
        onDisconnect:function (e) {
            isConnect = false;
            console.log("handler disconnect ", e);
        },
        onMsg:function (data) {
            counter++;
            if(counter >= 2000){
                counter = 0;
                console.log("handler receive message ", data)
            }
        }
    };
    let socket = new VIL.EngineSocket("ws://127.0.0.1:10215/ws", handler);
</script>
</html>

WebSocket 服务端 和 客户端在 GitHub 上有源码,需要的可以访问
https://github.com/vilsongwei/practiceDemo/tree/master/websockTest

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

推荐阅读更多精彩内容