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