如何基于WebSocket实现协同?

 

说到WebSocket想比大家不会陌生,如果用一句话概括,那就是...





到WebSocket想比大家不会陌生,如果用一句话概括,那就是:



WebSocket protocol是HTML5一种新的协议,它实现了浏览器与服务器全双工通信。
相比较传统那些服务器,WebSocket的推送技术简直好了太多。

有了它,我们可以挥手向comet、长轮询这些技术说再见了,庆幸我们生活在拥有HTML5的时代。

得益于WebSocket,“移动+”在它的基础上实现了协同开发功能。

下面将分三个部分讲解“移动+”的协同开发功能:协同开发功能介绍,移动+协同原码解析,开发中踩过的坑。
1
协同开发功能介绍


“移动+”支持多人同时开发一个app,那么就涉及锁、合并、同步的问题。

还有一个最常见的问题是,如果1个以上的开发人员同时开发一个页面,那么可能其中一个人的修改会被另一个人的修改冲掉。

因此,我们提出了使用websocket来解决协同开发引发的这一系列问题。


功能目标
1、当1个以上用户同时开发同一个应用时,以页面为单位实现锁定,只允许1个用户编辑一个页面。其它用户被通知页面被锁定,并禁止编辑该页面。

2、当正在编辑的用户完成编辑保存时,通知其它用户页面开放 ,并更新被修改过的页面。


实现效果


页面没有编辑操作时,所有用户界面均无变化
当其中1个用户对页面进行操作时,其它用户的相应界面被锁定
当前用户操作完成保存后,其它用户的相应界面被修改
2
移动+协同功能原码解析


移动+ webSocket 后台是基于node开发的,主要使用ws的模块。

安装 :npm install ws


官方示例代码
启动一个WebSocket服务

var WebSocketServer = require('ws').Server

, wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {    console.log('received: %s', message);

});

ws.send('something');
});



前端Client js代码

var WebSocket = require('ws');var ws = new WebSocket('ws://www.host.com/path');

ws.on('open', function open() {

ws.send('something');
});

ws.on('message', function(data, flags) {

// flags.binary will be set if a binary data is received.

// flags.masked will be set if the data was masked.});



ws模块使用很简单,启动服务设下message监听数据即可,主要的逻辑就是在message是对消息的封装、解包。


“移动+”coffee后台代码
ws-server.js 文件websocket 服务关键代码

引入ws模块

ws = require('ws')

WsServer = ws.Server

定义SocketServer 服务类:以server服务类为启动服务,这样可以统一http端口,避免前端跨域问题,path配置主要是生产中nginx代理拦截作用。

verifyClient 连接验证回调。

SocketServer = (server) ->

_this = this

this.wss = new WsServer

server: server

clientTracking: true

path: '/ws'

# 连接验证

verifyClient: (info) ->

     # params = _this.getParams info.req.url

return true



connection连接成功事件中监听message,判断数据流根据操作标识operate处理client连接。

数据流结构是自定义的,具体可以根据前端业务定义,这里主要定义了operate的四种状态:login,lock,look,close。

四种状态对应用户对应用户操作流程,具体看下面的流程图:

this.wss.on'connection', (ws) ->

ws.on 'message', (data) ->

return unless this.upgradeReq

_data = JSON.parse data

# 操作流

switch _data.operate

       when "login"

list = {}

return unless list

# 发送协作清单

this.send JSON.stringify list

       when "lock"

this.info = _data

_this.broadcast (JSON.stringify _data), this.app_id, this.user_id

when "look"

this.info = _data

_this.broadcast (JSON.stringify _data), this.app_id, this.user_id

when "close"

_this.broadcast (JSON.stringify _data), this.app_id, this.user_id

# 连接关闭

ws.on 'close', (code, message) ->

     _data = this.info

return unless _data

_data.operate = 'close'

_this.broadcast (JSON.stringify _data), this.app_id, this.user_id

# 连接错误

ws.on 'error', (code, message) ->

     _data = this.info

return unless _data

_data = JSON.parse _data

_data.operate = 'close'

_this.broadcast (JSON.stringify _data), this.app_id, this.user_id

return this

# 广播信息

SocketServer.prototype.broadcast = (data) ->

 this.wss.clients.forEach (client) =>

client.send(data)

启动node http服务使用http server启动ws

 WsService = require 'ws-service'

http = require 'http'

express = require 'express'

app = express()

server = http.createServer(app)

server.listen process.env.PORT || 3000 new WsService(server)



ws-client.js 关键代码对应服务端数据包

function _init() {

var host = window.document.location.host;

this.ws = new WebSocket('ws://'+host+'/ws?id=xx&uid=xx');

this.ws.onmessage = this.onmessage.bind(this);

this.ws.onclose = function() {

return null;

console.info('WsClient 关闭....');

this.intervalId = setInterval(function() {

// this.ws.Reconnect();

         //console.info("WsClient 尝试重连");

       }.bind(this), 5000)

}.bind(this);

this.ws.onopen = function() {

if (this.intervalId) {

clearInterval(this.intervalId);

this.intervalId = 0;

}

// 发送登录

       this.send({operate: "login"});

}.bind(this);

this.ws.onerror = function(err) {

console.error(err);

}

}

function onmessage(event) {

var data = event.data;

if (!data)

return ;

data = JSON.parse(data);

switch(data.operate){

case "setcookie":

setCookie(data.key, data.value, 24*60);

break;

case "close”:

var _temp = _.findWhere(this.listData.items, {page_id: data.page_id, user_id: data.user_id});

this.listData.items.splice(_index,1);

break;

case "look":

case "lock":

         // 更新or新增用户状态

 var _temp = _.findWhere(this.listData.items, {page_id: data.page_id, user_id: data.user_id});

if (_temp){

_temp.operate = data.operate;

} else {

this.listData.items.push(data);

}

break;

case "unlock":

this.operate = "look"

var _page = this.props.app.pages.findWhere({id: data.page_id});

break;

case "list”:

//下拉列表

this.listData = data;

this.checkPageStateSend();

break;

}

}

function send(data) {

if (!this.ws) return;

this.ws.readyState == this.ws.OPEN && this.ws.send(JSON.stringify(data));

}



在整个流程中,主要逻辑都是在message监听中完成。ws-server负责管理所有协同数据的分发,ws-client负责当前用户的数据封装和解包服务推送的指令更新界面状态。
3
踩过的坑


跨域
这个问题估计做web前端开发都会遇到过,这是常见却经常踩的坑啊。

刚开始websockt服务是独立端口的,虽然是同一个域名下,但是不同的端口也是跨域;在移动+的协同开发中结合express框架,启动服务的时候统一在一个端口解决。

location /ws {

proxy_pass http://appbricks;

proxy_redirect off;

proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;

proxy_set_header Connection "upgrade";

}



部署生产nginx代理问题
部署生产nginx代理问题,nginx5.x以后就支持,网上搜一下配置解决,主要是启动统一拦截(/ws)匹配还是花了点时间,主要是ws模块二级目录pathg添加之后连接不成功,可能是与跨域问题滚在一起了,最后解决跨域之后二级目录就可以了。


    关注 用友创新中心


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册