码迷,mamicode.com
首页 > Web开发 > 详细

websocket

时间:2020-03-01 00:44:09      阅读:76      评论:0      收藏:0      [点我收藏+]

标签:ike   its   rtb   管道   浏览器   input框   city   key   contain   


阅读目录

一、websockt

二、实战应用

1 websocket

1.1 简述

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

1.2 原理

技术图片

1.2.1 一个小例子熟悉websocket原理

a.启动服务端,等待客户端连接
b.小强来连接,服务端允许
c.小强立即发送一个“握手信息”

    GET /xxxx HTTP/1.1\r\n
    Host: 127.0.0.1:8002\r\n
    Connection: Upgrade\r\n
    Pragma: no-cache\r\n
    Cache-Control: no-cache\r\n
    Upgrade: websocket\r\n
    Origin: http://localhost:63342\r\nSec-WebSocket-Version: 13\r\n
    User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36\r\n
    Accept-Encoding: gzip, deflate, br\r\n
    Accept-Language: zh-CN,zh;q=0.9\r\n
    Cookie: csrftoken=ojyruuaF3Tk0OToIrXy1sRSdSk3SeDgd6Ti3jocEXAuEExaMtxjhJglpenj6Iq8F\r\n
    Sec-WebSocket-Key: 4NZY2fTOr691upgWe2yq7w==\r\n ########  这里 ########
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n

d.服务端接收握手信息后需要对数据进行加密,给客户端返回

     - 4NZY2fTOr691upgWe2yq7w + magic_string(魔法字符串) 
     - sha1
     - base64 
        response_tpl = "HTTP/1.1 101 Switching Protocols\r\n"               "Upgrade:websocket\r\n"               "Connection: Upgrade\r\n"               "Sec-WebSocket-Accept: 加密之后的结果\r\n"               "WebSocket-Location: ws://127.0.0.1:8002\r\n\r\n"
    通信建立,双方可以进行互相通信:

e.小强给服务端发送消息:

   读取第二个字节的后7位
        127:10,4,数据
        126:4,4,数据
      <=125: 2,4,数据

f.服务端给客户端发送消息:

    token = b"\x81"
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

1.3 socket.io与websocket

  • https://www.jianshu.com/p/2ec3d20341ab

1.4 python实现示例

  • flask-socketio文档: https://flask-socketio.readthedocs.io/en/latest/

1.5 golang实现示例

1.5.1 后端

package main

import (
    "errors"
    "fmt"
    "net/http"
    "github.com/gorilla/websocket"
    "sync"
    "time"
)

var wsUpgrader = websocket.Upgrader{
    //允许所有的cors跨域请求
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

type wsMessage struct {
    messageType int
    data []byte
}

type wsConnection struct {
    wsSocket *websocket.Conn //底层websocket
    inChan chan *wsMessage   //读队列
    outChan chan *wsMessage  //写队列

    mutex sync.Mutex   //避免重复关闭管道
    isClosed bool
    closeChan chan byte  //关闭通知
}


func(wsConn *wsConnection) wsReadLoop(){
    for{
        //读取一个message
        mesType,data,err := wsConn.wsSocket.ReadMessage()
        if err != nil{
            goto error
        }
        req := &wsMessage{messageType:mesType, data:data}

        //放入请求队列
        select{
        case wsConn.inChan<- req:
        case <-wsConn.closeChan:
            goto closed
        }
    }

    error:
        wsConn.wsClose()
    closed:
}

func(wsConn *wsConnection)wsWriteLoop(){
    for{
        select{
        //读取一个应答
        case msg := <- wsConn.outChan:
            if err:= wsConn.wsSocket.WriteMessage(msg.messageType, msg.data); err != nil{
                goto error
            }
        case <- wsConn.closeChan:
            goto closed
        }
    }
    error:
        wsConn.wsClose()
    closed:
}

func(wsConn *wsConnection)procLoop(){
    //启动一个goroutine发送心跳
    go func(){
        for{
            time.Sleep(time.Second * 2)
            if err:= wsConn.wsWrite(websocket.TextMessage, []byte("*heartbeat from serve")); err!= nil{
                wsConn.wsClose()
                break
            }
        }
    }()

    // 这是一个同步处理模型(只是一个例子),如果希望并行处理可以每个请求一个goroutine,注意控制并发goroutine的数量!!!
    for{
        mes, err:= wsConn.wsRead()
        if err != nil{
            fmt.Println("read fail...")
            break
        }
        fmt.Println(string(mes.data))

        err = wsConn.wsWrite(mes.messageType, mes.data)
        if err != nil{
            fmt.Println("write fail...")
            break
        }
    }
}


func wsHandler(res http.ResponseWriter, req *http.Request,){
    wsSocket, err := wsUpgrader.Upgrade(res, req, nil)
    if err != nil{
        return
    }
    wsConn := &wsConnection{
        wsSocket:wsSocket,
        inChan:make(chan *wsMessage, 1000),
        outChan:make(chan *wsMessage, 1000),
        closeChan:make(chan byte),
        isClosed:false,
    }

    go wsConn.procLoop()
    go wsConn.wsReadLoop()
    go wsConn.wsWriteLoop()
}

func(wsConn *wsConnection)wsWrite(messageType int, data []byte) error {
    select{
    case wsConn.outChan <- &wsMessage{messageType, data}:
    case <- wsConn.closeChan:
        return errors.New("websocket closed")
    }
    return nil
}

func(wsConn *wsConnection)wsRead()(*wsMessage, error){
    select {
    case msg := <-wsConn.inChan:
        return msg,nil
    case <- wsConn.closeChan:
    }
    return nil, errors.New("websocket closed")
}


func(wsConn *wsConnection)wsClose(){
    wsConn.wsSocket.Close()
    wsConn.mutex.Lock()

    defer wsConn.mutex.Unlock()
    if !wsConn.isClosed{
        wsConn.isClosed = true
        close(wsConn.closeChan)
    }
}

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

1.5.2 前端

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script>
        window.addEventListener("load", function(evt) {
            var output = document.getElementById("output");
            var input = document.getElementById("input");
            var ws;
            var print = function(message) {
                var d = document.createElement("div");
                d.innerHTML = message;
                output.appendChild(d);
            };
            document.getElementById("open").onclick = function(evt) {
                if (ws) {
                    return false;
                }
                ws = new WebSocket("ws://localhost:8000/ws");
                ws.onopen = function(evt) {
                    print("OPEN");
                }
                ws.onclose = function(evt) {
                    print("CLOSE");
                    ws = null;
                }
                ws.onmessage = function(evt) {
                    print("RESPONSE: " + evt.data);
                }
                ws.onerror = function(evt) {
                    print("ERROR: " + evt.data);
                }
                return false;
            };
            document.getElementById("send").onclick = function(evt) {
                if (!ws) {
                    return false;
                }
                print("SEND: " + input.value);
                ws.send(input.value);
                return false;
            };
            document.getElementById("close").onclick = function(evt) {
                if (!ws) {
                    return false;
                }
                ws.close();
                return false;
            };
        });
    </script>
</head>
<body>
<table>
    <tr><td valign="top" width="50%">
            <p>Click "Open" to create a connection to the server,
                "Send" to send a message to the server and "Close" to close the connection.
                You can change the message and send multiple times.
            </p>
            <form>
                <button id="open">Open</button>
                <button id="close">Close</button>
                <input id="input" type="text" value="Hello world!">
                <button id="send">Send</button>
            </form>
        </td><td valign="top" width="50%">
            <div id="output"></div>
        </td></tr></table>
</body>
</html>

2 实战应用

2.1 协作编辑

2.1.1 简介

多人实时操作一个资源,如钉钉协作excel文档,更多可以了解下面两部分内容

  • https://www.cnblogs.com/powertoolsteam/p/onlineexcel.html
  • https://www.jianshu.com/p/a3b350063cb5

2.1.2 需求实现

  • web端多人在线协作编辑表格,python+flask+vue+socket.io实现

2.1.3 前端

<div id="app">
    <div class="c-title">
        <h3>协作编辑</h3>
    </div>
    <div class="table-container">
        <div class="params-table-item">
            <div class="param-key" >名称</div>
            <div class="param-key" >年龄</div>
            <div class="param-key" >性别</div>
            <div class="param-key" >爱好</div>
            <div class="param-key" >技能</div>
        </div>
        <div class="params-table-item" v-for="(item, index) in tables_info">
            <div class="param-value">
                <input  v-model="item.name" class="text-width " @change="inputChange(index, item.name, 'name')"/>
            </div>
            <div class="param-value">
                <input  v-model="item.age" class="text-width " @change="inputChange(index, item.age, 'age')"/>
            </div>
            <div class="param-value">
                <input  v-model="item.gender" class="text-width " @change="inputChange(index, item.gender, 'gender')"/>
            </div>
            <div class="param-value">
                <input  v-model="item.hobby" class="text-width " @change="inputChange(index, item.hobby, 'hobby')"/>
            </div>
            <div class="param-value">
                <input  v-model="item.skill" class="text-width " @change="inputChange(index, item.skill, 'skill')"/>
            </div>
        </div>
    </div>
</div>
<script>
    const host = "127.0.0.1:5000"
    const namespace = "/test";
    var app = new Vue({
        el:'#app',
        data:{
            tables_info:[
                {name: "小明", age:18, hobby:"跑步", skill:"跳马", gender:"男"},
                {name: "小花", age:17, hobby:"唱歌", skill:"跳绳", gender:"女"},
                {name: "小小", age:16, hobby:"", skill:"", gender:"女"},
            ],
            socket:null,
        },
        mounted(){
            this.init_socket();     # 页面加载时初始化socket连接
            this.recv_msg()         # 接受input chang后端处理返回的信息
        },
        methods:{
            recv_msg(){
                let _this = this;
                _this.socket.on('input-change', function(msg, cb) {
                    const _index = msg['index']
                    const forms = msg['forms']
                    _this.tables_info[_index][forms] = msg['value']
                });
            },
            init_socket(){
                this.socket = io(host + namespace);
                let _this = this;
                _this.socket.on('connect', function() {
                    _this.socket.emit('my_event', {data: 'I\'m connected!'});
                });

                _this.socket.on('my_response', function(msg, cb) {
                    console.log(msg, cb)
                   if (cb){
                        cb();
                   }
                });
            },
            //监听input框变化事键
            inputChange(index, new_val, forms){
                const params = {
                    "index": index,
                    "value": new_val,
                    "forms": forms
                }
                axios.post('http://127.0.0.1:5000/input-change',params,{
                                'Content-Type':'application/json'
                                }
                )
                .then(function (response) {
                    console.log(response);
                }).catch(function (error) {
                    console.log(error);
                });
            }
        },
    })
</script>

2.1.4 后端

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
CORS(app, supports_credentials=True, resources=r'/*')                              # http跨域问题
socketio = SocketIO(app, async_mode=async_mode, cors_allowed_origins='*')          # cors_allowed_origins解决跨域

@app.route('/input-change', methods=['POST'])   # http协议接受input change信息
def input_change():
    """
    broadcast=True,发送广播,所有连接到socket.io的链接都会接受到信息
    """
    emit('input-change', request.json, broadcast=True, namespace='/test')    
    return "success!"

@socketio.on('connect', namespace='/test')
def mtest_connect():
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(background_thread)
    print("靓仔你来了呀...")
    emit('my_response', {'data': 'Connected', 'count': 0})

@socketio.on('disconnect', namespace='/test')
def mtest_disconnect():
    print('Client disconnected', request.sid)


if __name__ == '__main__':
    socketio.run(app, debug=True)

2.1.5 总结

  • 简单了实现了需求,但是协作编辑远远比这个要复杂的多,代码后端的处理直接选用了广播模式,真实场景肯定会有所偏差。这里推荐一个优秀的前端excel编辑器-葡萄城spreadjs

2.2 实时聊天室

github - 协作编辑及实时聊天室

websocket

标签:ike   its   rtb   管道   浏览器   input框   city   key   contain   

原文地址:https://www.cnblogs.com/zhangliang91/p/12266135.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!