odoo 集成websocket的实践

碰到一个需求,即在某个对象被创建的时候,现实对某些正在浏览该对象的列表视图的人进行实时的通知,并播放声音进行提醒。这里的业务场景是,工厂的操作员需要某个技术员进行协助的时候,发起一个协助请求,技术员在接收到通知后去操作员那里进行协助。

技术分析

最容易想到的解决方案就是在技术员打开的列表视图加入轮询式的脚本,不停地刷新,有新数据产生的时候进行通知。实现这种功能的技术也非常成熟了,就是利用http协议中的轮询、长连接等方式实现。但这样做的缺点有两个:

  1. HTTP轮询和长连接带来的性能开销
  2. 用户界面定时刷新不是那么简洁,而且如果数据超过一页,当用户去看第二页的时候就会被翻页返回第一页

所以就想到了用websocket方式来替代HTTP,Python对websocket的实现有多种,比较著名的有:Autobahn、Django Channel、Flask-SocketIO、Websocket-client、Crossbar.io等,但搜了一下odoo对websocket的支持,貌似目前还没有,虽然官方IM模块实现了实施通讯的功能,但底层实现的技术没有细究。这里我们采用Python的另一个著名框架tornado来实现,tornado原生就支持了websocket,所以我们不需要再多安装什么。

Websocket 与 HTTP的前世今生

Http诞生之时,被设计为了被动、无状态的,意思就是request和response总是成对出现的,并且sever端不能主动向client端发送消息,且一次交互过后server端就不再保存client端的信息(即拔*无情),再次连接还得重新握手重新建立连接。因为这种无状态的特性,催生了cookie和session等保持用户身份的技术,也是因为这种被动的特性,催生一个新协议的诞生——websocket。websocket是一种基于http的全双工协议,即建立连接后,server端既可主动向客户端发送消息,客户端也可以主动向server端发送消息,这种方式性能开销小,不像http那样每次都要重新握手进行连接,也不像长连接那样需要server端一直hold住连接从而节省了server端的性能开销。

Tornado的Websocket Server实现

用tornado是实现一个webserver的服务端非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from tornado.options import define, options, parse_command_line
define("port", default=8888, help="run on the given port", type=int)
# we gonna store clients in dictionary..
clients = dict()
class EchoWebSocket(tornado.websocket.WebSocketHandler):
def open(self):
print("WebSocket opened")
def on_message(self, message):
self.write_message(u"You said: " + message)
def on_close(self):
print("WebSocket closed")
app = tornado.web.Application([
(r'/', WebSocketHandler),
])
if __name__ == '__main__':
parse_command_line()
app.listen(options.port)
tornado.ioloop.IOLoop.instance().start()

client端,可以利用HTML5原生的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的浏览器支持 WebSocket!");
// 打开一个 web socket
var ws = new WebSocket("ws://localhost:9998/echo");
ws.onopen = function()
{
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
alert("数据已接收...");
};
ws.onclose = function()
{
// 关闭 websocket
alert("连接已关闭...");
};
}
else
{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">运行 WebSocket</a>
</div>
</body>
</html>

可以写一个简单的脚本测试一下

与odoo的结合

我们的最终目的是要利用websocket和odoo实现我们开头描写的场景,现在的情况是python的技术有了,那么如何结合到odoo当中呢?

首先,我们创建一个新模块,在init文件中引入tornadao,由于我们是要在odoo中另开一个端口进行监听,所以这里要开启一个多线程:

1
2
3
4
5
6
7
8
9
10
11
12
#coding:utf-8
import tornado
from wserver import EchoWebSocket
app = tornado.web.Application([
(r'/', EchoWebSocket),
])
app.listen(9000)
import threading
threading._start_new_thread(tornado.ioloop.IOLoop.instance().start,())

然后我们在wserver.py中,加入我们的逻辑,即创建rl_mrp.adjust.report这个对象时对客户端进行通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#coding:utf-8
import logging
from odoo import models,api
import tornado.ioloop
import tornado.web
import tornado.websocket
_logger = logging.getLogger(__name__)
class EchoWebSocket(tornado.websocket.WebSocketHandler):
live_web_sockets = set()
def check_origin(self, origin):
return True
def open(self):
_logger.info("连接打开")
self.set_nodelay(True)
self.live_web_sockets.add(self)
self.write_message("你已经连接上WS服务")
def on_message(self, message):
self.write_message(u"你发送的数据: " + message)
@classmethod
def send_message(cls, message):
removable = set()
for ws in cls.live_web_sockets:
if not ws.ws_connection or not ws.ws_connection.stream.socket:
removable.add(ws)
else:
ws.write_message(message)
for ws in removable:
cls.live_web_sockets.remove(ws)
def on_close(self):
_logger.info("连接关闭")
class MixClass(models.Model):
_inherit = "mrp.adjust.report"
@api.model
def create(self,val):
res = super(MixClass,self).create(val)
EchoWebSocket.send_message('reload')
return res

前端接收通知进行reload操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
odoo.define('ws_tree.autofresh', function (require) {
"use strict";
var core = require('web.core');
var Model = require('web.Model');
var Widget = require('web.Widget');
var Dialog = require('web.Dialog');
var Session = require('web.session');
var ListView = require('web.ListView');
var _t = core._t;
ListView.include({
init: function () {
this._super.apply(this, arguments);
},
load_list: function () {
var self = this;
var r = this._super.apply(this, arguments);
var tree = this.$('.ws-tree');//.hasClass('ws-tree')
if (tree) {
if ("WebSocket" in window) {
//建立ws连接
var host = "ws://" + window.location.hostname + ":9000";
var ws = new WebSocket(host);
ws.onmessage = function (e) {
var msg = e.data;
console.log('接收到数据...' + msg);
if (msg == "reload") {
//播放声音
$.playSound("http://www.evidenceaudio.com/wp-content/uploads/2014/10/monsterslap.mp3");
setTimeout(function () {
window.location.reload();
}, 5000);
}
}
}
else {
alert("您的浏览器版本过低,请升级到最新版本!");
}
}
return r
},
});
});

好了,到这里我们就跟odoo的结合完成。