websocket


阅读目录

一、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

1.4 python实现示例

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文档,更多可以了解下面两部分内容

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 - 协作编辑及实时聊天室

posted @ 2020-02-29 23:30  初遇ぃ  阅读(511)  评论(0编辑  收藏  举报
//一下两个链接最好自己保存下来,再上传到自己的博客园的“文件”选项中 //一下两个链接最好自己保存下来,再上传到自己的博客园的“文件”选项中