最近在完成项目中需要用到实时技术,项目需求是将后端的一个文件内容实时读取然后发送到前端.这里主要涉及到两个技术.一个是后端如何实时读取一直在更新中的数据,另一点是如何保证web前后端的通讯,能将读取到的数据实时传送给前端.
由于主要是进行后端开发,前端涉及的少,趁这个机会刚好学习了一下前端的一些知识.
一.Ajax轮询
最开始解决实时通讯Google后使用了ajax的轮询技术,如果说从要求上来看基本满足要求,代码也十分简单,核心代码段如下:
- var getting = {
- url:"{% url '...' %}",
- type:'GET',
- dataType: 'text',
- success: function(data) {
- $("#output").val(data);
- //sleep(1000);
- $.ajax(getting)
- }
- }
- };
- $.ajax(getting);
也就是ajax异步请求成功后再次调用自己发送get请求,这样可以满足"实时"获取到后端一直在更新的文件的最新内容.但是,有个很严重的问题就是前端一直在get请求,导致极大的占用带宽,占用服务器的处理资源.后来加入修改,将每次发送的请求间隔1s,发现仍然是十分浪费带宽资源.
后来发现有ajax长轮询技术,就是在每次发送请求后,如果后台有数据,则将数据返回,前端拿到后继续发送请求,如果后台没有数据,就不会response.可以很好的优化ajax的轮询缺陷,但是感觉该方法还是不够好.
传统的request必须有一个response,对于实时通讯,这样是很差劲的体验.服务器端在建立连接后更新到新数据时无法主动给前端发送response,不够理想.
二.弃用ajax轮询,选择websocket
后来Google到一个稍微新一点的技术,websocket,刚开始以为是socket的孪生表弟,后来才发现它俩关系跟java和JavaScript一样.有一篇文章讲的很好,入门了解一看便知,websocket基础
websocket相比较传统http的优势很明显,借阮老师的一张图来看:
一张图就明白了它的优势有多大.接下来就是开干
先看看前端主要部分代码:
- var socket = new WebSocket("ws://" + window.location.host + "{% url '...' %}");
- window.s = socket;
-
- window.s.onopen = function () {
- ...
- console.log('websocket conneted!')
- };
-
- window.s.onmessage = function (event) {
- ...
- }
- };
- window.s.onclose = function(){
- ...
- }
这样,你就创建了一个websocket连接,浏览器是否支持需要你写个判断,我用的Chrome,直接支持websocket.其他具体说明请问度娘或谷大哥. 后台也需要接受,由于我用的是Django,后来了解到Django channels也可以实现实时通讯,同时也发现了一个dwebsocket第三方库也是可以实现实时通讯,其实都是用了websocket技术而已...看你个人喜好,我使用了dwebsocket,因为Django channels安装的不少,还需要加到INSTALLED_APPS中,我只是需要用到websocket技术而已,dwebsocket相比较之下非常轻便,直接pip install dwebsocket就可以用,符合人生苦短,我用python的编程思想.其实我是懒
三.使用dwebsocket
一.使用方法
使用上很方便,如果为一个单独的视图函数处理一个websocklet连接可以使用accept_websocket装饰器,它会将标准的HTTP请求路由到视图中。使用require_websocke装饰器只允许使用WebSocket连接,会拒绝正常的HTTP请求。
在设置中添加设置MIDDLEWARE_CLASSES=dwebsocket.middleware.WebSocketMiddleware这样会拒绝单独的视图实用websocket,必须加上accept_websocket 装饰器。设置WEBSOCKET_ACCEPT_ALL=True可以允许每一个单独的视图实用websockets.....当然,在settings中这样做完全没必要,因为我就是一个视图函数来处理请求的,无需复杂化.直接在视图函数处引入提供的装饰器accept_websocket即可.
二.一些属性和方法
1.request.is_websocket()
如果是个websocket请求返回True,如果是个普通的http请求返回False,可以用这个方法区分它们。
2.request.websocket
在一个websocket请求建立之后,这个请求将会有一个websocket属性,用来给客户端提供一个简单的api通讯,如果request.is_websocket()是False,这个属性将是None。
3.WebSocket.wait()
返回一个客户端发送的信息,在客户端关闭连接之前他不会返回任何值,这种情况下,方法将返回None
4.WebSocket.read()
如果没有从客户端接收到新的消息,read方法会返回一个新的消息,如果没有,就不返回。这是一个替代wait的非阻塞方法
5.WebSocket.count_messages()
返回消息队列数量
6.WebSocket.has_messages()
如果有新消息返回True,否则返回False
7.WebSocket.send(message)
向客户端发送消息
8.WebSocket.__iter__()
websocket迭代器
必须要学习了解才知道我们需要用到什么.当然了,客户端的也需要了解一下:
这是客户端的一些说明,在客户端,websocket的两个属性:readyState和bufferedAmount,区别和说明如下:
根据readyState属性可以判断webSocket的连接状态,该属性的值可以是下面几种:
0 :对应常量CONNECTING (numeric value 0),
正在建立连接连接,还没有完成。The connection has not yet been established.
1 :对应常量OPEN (numeric value 1),
连接成功建立,可以进行通信。The WebSocket connection is established and communication is possible.
2 :对应常量CLOSING (numeric value 2)
连接正在进行关闭握手,即将关闭。The connection is going through the closing handshake.
3 : 对应常量CLOSED (numeric value 3)
连接已经关闭或者根本没有建立。The connection has been closed or could not be opened.
根据bufferedAmount可以知道有多少字节的数据等待发送,若websocket已经调用了close方法则该属性将一直增长。
好了,其实也就这点内容,接下来就开始逻辑实现了,我在服务器端部分代码如下:
- @method_decorator(accept_websocket)
- def get(self, request):
- if request.is_websocket():
-
- for message in request.websocket:
- if ...:
- ...
- else:
- request.websocket.send(...)
-
- else:
- .....
- return HttpResponse('点个赞')
这里我用到了method_decorator,这个可以忽略,因为我这个是视通函数,不是的话直接用@accept_websocket就行
注意这里request.websocket.send发送的内容格式需要为bytes,而不是str..同时需要判断请求,用request.is_websocket()来进行判断.这里用for message in request.websocket比较好,前端发来的信息都会在里面,分析源码后感觉比wait()方法要好用,源码如下:
- while True:
- message = self.wait()
- yield message
- if message is None:
- break
会处理多条message.具体后台的业务逻辑就可以根据需求添加了四.websocket心跳包机制
接下来还缺一个需求,就是如何判断前后端是否是连接状态,否则一方端口另一方会无法察觉,而继续发送,以至于报错.分析了一下这种长连接是哪方进行心跳包发送比较好,理论上来说都应该间隔性主动发送,但是,考虑到服务器端的带宽资源性问题,以及重要性问题,还是由客户端主动定期发送比较合适.同时发现,当客户端断开连接,刷新退出浏览器时,for message in request.websocket中的message会返回None,根据message返回的值来处理逻辑问题.那样,就剩下客户端需要加入心跳包了
废话少说,主要代码如下:
- function keepalive(ws) {
- var time = new Date();
- if ((time.getTime() - last_health > 35000)) {
- //ws.close();
- //clearInterval(window.heartbeat_timer)
- } else {
- if (ws.bufferedAmount == 0 && ws.readyState == 1) {
- ws.send('ping')
- }
- }
- }
time.getTime() - last_health > 35000表示现在时间与上次接受到服务器端数据时间相差大于35s,就ws.close()断开连接,或者重连或者实现其他业务逻辑都可...否则,判断ws.bufferedAmount == 0 && ws.readyState == 1,表示没有拔网线,即无阻塞,且是联通状态,发送心跳包,ping,服务器收到后会回复个pong,随个人喜好.在onopen中就需要加入间隔性发送的代码:
- window.s.onopen = function () {
- window.heartbeat_timer = setInterval(function () {keepalive(window.s)}, 30000);
这里将心跳包间隔设置为30s,别忘了在onmessage中每次收到服务器消息都要刷新收到消息的时间.这样,就实现了客户端和服务器之间的心跳包重连.
五.后记
对于如何用python进行文件的实时读取并发送前端有时间会记录下来.