go - websocket

2019/10/15 go

go websocket 使用与封装

WebSocket

websocket 协议

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

Web 中前端获取后端实时信息的方法

前端通过对 api 进行轮询 (pull 的方式)

现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

websocket

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

前端技术

可以用来测试 websocket 后端接口的网址 http://www.websocket-test.com

WebSocket 的属性

属性 作用
readyState 连接状态
bufferedAmount 放入 队列 但还没发出的字节数

代码

<!DOCTYPE HTML>
<html>
   <head>
   <meta charset="utf-8">
   <title>testWebsocket</title>
    
      <script type="text/javascript">
         var ws = null;
         var message = "";
         function connect(){
            if(ws != null) {
            } else {
               var inputValue = document.getElementById("socketurl").value;
               ws = new WebSocket(inputValue);
               console.log(ws)
            }

            ws.onmessage = function (evt) 
            { 
               message +=  "\nreceived : " + evt.data;
               document.getElementById("receive").value = message;
            };

            message +=  "\nconnect : " + ws.url;
            document.getElementById("receive").value = message;
         }

         function disconnect() {
            console.log(ws)
            if(ws != null) {
               ws.close()
            } 
            message +=  "\nclose ";
            document.getElementById("receive").value = message;
         }

         function send() {
            var inputValue = document.getElementById("inputtest").value;
            ws.send(inputValue);
            message +=  "\nsend : " + inputValue;
            document.getElementById("receive").value = message;
         }

      </script>
        
   </head>
   <body>
      <div>
         Socket Url 
         <div>
            <input id="socketurl" value="ws://localhost:8080/ws" />
            <a href="javascript:connect()"> 连接 </a>
            <a href="javascript:disconnect()"> 断开 </a>
         </div>
      </div>
      <div id="sse">
         <input id="inputtest" />
         <a href="javascript:send()">发送</a>
      </div>
      <div>
         <textarea id="receive" rows="10" cols="30" readonly=true>
         </textarea>
      </div>
      
   </body>
</html>

后端技术

这里,我们使用 go 来构建后端程序

原理

websocket 是基于 tcp 协议实现的双向通信协议。

  • 通过 http 协议进行连接的建立,然后升级到 websocket 协议。

  • 连接建立成功后,前后端通过 websocket 进行通信

1 - 首先,启动一个 web 服务

package main

import (
    "net/http"
)

func ws(res http.ResponseWriter, r *http.Request) {
    res.Write([]byte("helloworld"))
}

func main() {
    http.HandleFunc("/ws", ws)
    http.ListenAndServe(":8080", nil)
}

访问 http://localhost:8080/ws 可以获取返回值

helloworld

2 - 进行协议的转换,由 http 升级为 websocket

package main

import (
    "fmt"
    "github.com/gogf/gf/third/github.com/gorilla/websocket"
    "net/http"
)

func websocketTest(res http.ResponseWriter, r *http.Request) {
    var (
        // websocket 的连接
        conn *websocket.Conn
        // 错误
        err error
        // 声明 upgrader,用于协议升级
        upgrader = websocket.Upgrader{
            // 用于进行跨域检测
            CheckOrigin: func(r *http.Request) bool {
                return true
            },
        }
    )
    // 这里进行协议的升级,由 res,r 进行转换为 websocket.Conn
    if conn, err = upgrader.Upgrade(res, r, nil); err != nil {
        return
    }

    for ; ; {
        var msgType int
        var data []byte
        var err error
        // 接收消息
        if msgType, data, err = conn.ReadMessage(); err != nil {
            conn.Close()
            return
        }
        fmt.Println(msgType)
        fmt.Println(data)
        // 回写消息
        if err = conn.WriteMessage(msgType, data); err != nil {
            conn.Close()
            return
        }
    }
}

func main() {
    http.HandleFunc("/ws", websocketTest)
    http.ListenAndServe(":8080", nil)
}

继续访问上面给出的网址 http://localhost:8080/ws 我们获得

Bad Request
Request URL: http://localhost:8080/ws
Request Method: GET
Status Code: 400 Bad Request
Remote Address: [::1]:8080
Referrer Policy: no-referrer-when-downgrade

此时的协议已经由 http 转换为 websocket,所以只能由 ws:// 请求

  • 打开 html 文件,由上面的代码写入 test.html 构成

  • 输入 ws://localhost:8080/ws
  • 可以看到连接建立
  • 输入发送的信息,点击发送
  • 获得服务端回写的信息
  • 最后点击 close
connect : ws://localhost:8080/ws
send : 123
received : 123
close 

进行封装

package impl

import (
    "errors"
    "github.com/gogf/gf/third/github.com/gorilla/websocket"
    "net/http"
    "sync"
)
// 改编于 慕课网课程代码

type Connection struct {
    wsConn *websocket.Conn
    // 接收流
    inChan    chan []byte
    // 输入流
    outChan   chan []byte
    // 关闭流
    closeChan chan byte
    // 锁
    mutex sync.Mutex
    // 是否已关闭
    isClosed  bool
}

func InitConnection(res http.ResponseWriter, r *http.Request) (connNew *Connection, err error) {
    var (
        upgrader = websocket.Upgrader{
            CheckOrigin: func(r *http.Request) bool {
                return true
            },
        }
        conn *websocket.Conn
    )
    if conn, err = upgrader.Upgrade(res, r, nil); err != nil {
        return
    }

    connNew = &Connection{
        wsConn:    conn,
        inChan:    make(chan []byte, 1000),
        outChan:   make(chan []byte, 1000),
        closeChan: make(chan byte, 1),
    }
    go connNew.readLoop()

    go connNew.writeLoop()
    return
}

func (conn *Connection) ReadMessage() (data []byte, err error) {
    select {
    case data = <-conn.inChan:
    case <- conn.closeChan:
        err = errors.New("connection is closed")
    }
    return
}

func (conn *Connection) WriteMessage(data []byte) (err error) {
    select {
    case conn.outChan <- data:
    case <-conn.closeChan:
        err = errors.New("connection is closed")
    }
    return
}

func (conn *Connection) Close() {
    conn.wsConn.Close()
    conn.mutex.Lock()
    if !conn.isClosed{
        close(conn.closeChan)
        conn.isClosed = true
    }
    conn.mutex.Unlock()
}

func (conn *Connection) readLoop() {
    var (
        data []byte
        err  error
    )
    for ; ; {
        if _, data, err = conn.wsConn.ReadMessage(); err != nil {
            conn.Close()
        }
        select {
        case conn.inChan <- data:
        case <-conn.closeChan:
            conn.Close()
            return
        }
    }
    return
}

func (conn *Connection) writeLoop() {
    var (
        data []byte
        err  error
    )
    for ; ; {
        select {
        case data = <-conn.outChan:
        case <-conn.closeChan:
            conn.Close()
        }

        if err = conn.wsConn.WriteMessage(websocket.TextMessage, data); err != nil {
            conn.Close()
        }
    }
    return
}

使用方法

package main

import (
    "net/http"
    "time"
    connImpl "ws/impl"
)

func ws(res http.ResponseWriter, r *http.Request) {
    var (
        data []byte
        err  error
    )

    var connNew *connImpl.Connection
    if connNew, err = connImpl.InitConnection(res, r); err != nil {
        return
    }

    //增加了 heartbeat
    go func() {
        var (
            err error
        )
        for ; ; {
            if err = connNew.WriteMessage([]byte("heart beat")); err != nil {
                return
            }
            time.Sleep(time.Second * 1)
        }
    }()

    for ; ; {
        if data, err = connNew.ReadMessage(); err != nil {
            connNew.Close()
            return
        }
        if err = connNew.WriteMessage(data); err != nil {
            connNew.Close()
            return
        }

    }

}

func main() {
    http.HandleFunc("/ws", ws)

    http.ListenAndServe(":8080", nil)
}

Show Disqus Comments

Search

    Post Directory