基于websocket的多人协作文档编辑实现

文章目录

  • 写在前面
    • 1.什么是协同编辑
    • 2.可选的解决方案
    • 3.仅仅使用websocket
    • 4.示例项目框架
  • 实现步骤
    • 1.思路
    • 2.示例代码
      • (1)前端
        • 全局函数
        • websocket初始化
        • 监听修改
      • (2)后端
  • 编辑冲突极限测试
    • 形象地解释冲突的产生
  • 踩坑

写在前面

1.什么是协同编辑

多人同时编辑一个文档,能够在不刷新页面的情况下实时地看到他人的编辑。典型例子有石墨文档、腾讯文档、Google Docs等。
主要需要解决的技术难点有:

  1. 实时通信
  2. 编辑冲突

2.可选的解决方案

  1. 实时通信有long pull或者websocket
  2. 编辑冲突解决有合并算法、上编辑锁以及Google Docs采用的分布式OT。
  3. 编辑冲突问题交给用户,当前用户实时地看到了别人正在编辑,那么当前用户就自觉性地停止编辑。

3.仅仅使用websocket

本文旨在为敏捷开发中快速实现多人在线协同编辑的需求提供一种只使用websocket的简单方法,其特点可以总结为以下几点:

  1. 代码量少并且容易理解,实际运行效果也较好,适用于小项目敏捷开发为了赶ddl快速达成展示效果
  2. 后端甚至直接与数据库分离,作为一个仅仅用于实时通信的后端服务,舍弃了实时保存的服务,提高实时性。
  3. 将编辑冲突问题交给了用户,实际的编辑同步并发性基于websocket的响应速度。

4.示例项目框架

前端Vue+后端Flask+geventwebsocket+数据库MySql
文档编辑器采用mavon-editor

实现步骤

1.思路

因为仅仅使用了websocket,因此思路十分明确:

  1. 用户打开文档编辑页面,与后端建立长连接。
  2. 后端将当前用户加入当前文档编辑用户列表。
  3. 前端监听用户对于文档内容的修改,每一次修改将整个修改内容发送给后端。
  4. 后端接收到信息,不做任何处理,直接将文本信息发送给文档编辑用户列表中其他的所有用户。
  5. 前端收到后端的文本信息直接覆盖掉当前文档内容。

基于websocket的多人协作文档编辑实现_第1张图片
在websocket请求中存储有用户连接信息,发送接收是同一地址,可以使用request.environ.get('wsgi.websocket')获取连接信息存入userlist中,直接进行监听以及发送即可。
在文档内容之外如果需要开发出其他的实时更新的消息可以使用json进行传输,标注各个信息在后端进行处理后再传输。当然处理多了会降低响应性。
经过实际操作,websocket传输似乎只支持字符串,所以前后端传输的必须是json格式的字符串,在发送前与接收后都需要相应的处理。

  • 前端将json对象转为json字符串的语句是JSON.stringify(jsonobj)
  • 前端将json字符串转为json对象的语句是JSON.parse(data)
  • 后端(Python)将json对象转为json字符串的语句是json.dumps(obj)
  • 后端(Python)将json字符串转为json对象的语句是json.loads(str)

实际运行发现后端json对象转字符串存在格式问题,前端不可读,所以直接手写json字符串解决。反正东西少,手写不烦

2.示例代码

实现了实时更新正在编辑的用户以及文本内容
并没有处理连接断开操作
每次传送的值包括:
“username”:当前用户用户名(数据库用户名唯一)
“content”:修改后的文档内容

(1)前端

全局函数

function contains(arr, obj) {  
  var i = arr.length;  
    while (i--) {  
      if (arr[i].username=== obj) {  
        return true;  
      }  
    }  
  return false;  
}

判断用户列表有没有收到的用户,如果进行修改的是新用户,加入正在编辑的用户列表

websocket初始化

写在methods中

initWebSocket(){ //初始化weosocket
  const wsuri = "ws://ip:端口/conn";
  this.websock = new WebSocket(wsuri);
  this.websock.onmessage = this.websocketonmessage;
  this.websock.onopen = this.websocketonopen;
  this.websock.onerror = this.websocketonerror;
  this.websock.onclose = this.websocketclose;
},
websocketonopen(){ //连接建立之后执行send方法发送数据
  console.log('连接成功!')
},
sendcontent(){
  this.sendjson.content=this.content;/修改后文档内容
  this.sendjson.username=localStorage.getItem('token')//当前用户用户名,存储在本地localStorage中
  this.websocketsend(JSON.stringify(this.sendjson));//传json字符串
},
websocketonerror(){//连接建立失败重连
  this.initWebSocket();//不断尝试重连
},
websocketonmessage(e){ //数据接收
  var jsondata=JSON.parse(e.data.replace(/\n/g,"\\n").replace(/\r/g,"\\r"));//换行处理
  this.content=jsondata.content;//覆盖
  if(contains(this.userList,jsondata.username)){
    console.log("用户已存在")
  }
  else {
    var tmp={
      "username":jsondata.username
    }
    this.userList.push(tmp);//添加新的用户
  }
},
websocketsend(Data){//数据发送
  this.websock.send(Data);
},
websocketclose(){  //关闭
  //console.log('断开连接');
},

监听修改

与methods同级

watch: {
 content() {//content是绑定的文本编辑器中内容
    this.sendcontent();
  },
},

最后在mounted中执行this.initWebSocket();即可

(2)后端

#!/usr/bin/python3
# -*- coding: utf8 -*-
from flask import Flask,request
import json
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
from geventwebsocket.websocket import WebSocket   #这条做语法提示用

app = Flask(__name__)


users = set()
@app.route('/conn')
def index():
    wsock = request.environ.get('wsgi.websocket')//获取连接信息
    print(wsock)
    users.add(wsock)//加入用户集合
    while True:
        message = wsock.receive()
        if message:
            obj=json.loads(message)
            username=obj['username']
            content=obj['content']
            res="{\"username\":\""+username+"\",\"content\":\""+content+"\"}"
            #手写json字符串
            for user in users:
                if user!=wsock:
                    try:
                        print(res)
                        user.send(res)//传送json字符串
                    except:
                        print("用户断开连接") 
                        users.remove(wsock)
if __name__ == '__main__':
    # app.run()
    #在APP外封装websocket
    http_serv = WSGIServer(("0.0.0.0",端口),app,handler_class=WebSocketHandler)
    # 启动服务
    http_serv.serve_forever()

放至服务器上独立运行,注意安装Flask环境与geventwebsocket环境
可以使用nohup保持程序运行

编辑冲突极限测试

用户之间编辑必定会存在时间上的差别,经过测试,当两个用户编辑时差在0.2秒之内时,会造成websocket接受与传送的冲突。即能实现0.2秒之外的多人同时文档编辑,人越多这个时间要求会更长。

形象地解释冲突的产生

因为没上锁,也没有合并,所以两个用户修改内容不同的情况下,在后端响应性上限之外的同步操作必定会产生冲突。
假设当前文档内容只有一个"你"字,用户A输入了“好”,成为”你好“,同时用户B输入了”坏“,成为”你坏“。因为修改必定有时间差,所以我们假设A先修改,那么后端收到”你好“,将”你好“发送给B用户。然而此时”你好“没有覆盖掉B的”你“,B就输入了”坏“,将”你坏“发送给了后端,然后B这边接受到了”你好“,将”你好“覆盖掉了”你坏“。后端接受到”你坏“,发送给A,此时A的”你好“被覆盖成了”你坏“,再次监听到修改再次发送”你坏“给后端。
如此循环,A用户和B用户的文档内容将不断地在”你好“和”你坏“之间切换,造成了冲突,死循环。

踩坑

  • websocket不能放在Flask app中一起运行,会自动将请求识别为http
  • Vue json字符串转对象存在换行问题,使用data.replace(/\n/g,"\\n").replace(/\r/g,"\\r")进行换行处理
  • geventwebsocket比uwsgi好用
  • json对象中存有list转化为字符串有单双引号的错误,python转为双引号,而前端识别要求单引号。示例代码是避免了列表,并且手写了json字符串,应该会有更好的解决方法。

你可能感兴趣的:(全栈,python,vue,websocket,flask)