前言
说起来长轮询也不是什么新鲜概念,不过个人首次用python实现。环境Mac, Python3.5.2, Tornado4.4.2。
http协议
在介绍长轮询前先了解下http协议。
http:超文本传输协议,是网络七层模型中的应用层协议,它是基于TCP/IP协议的。在
1,最早的http协议0.9版本,有且仅有一个GET请求。
2,往后就是http1.0,其引入了GET, POST两种请求,此外规定在每次请求中必须发送请求header–头信息。请求响应也多了状态码,缓存等。重要的是http1.0在建立TCP连接发送请求,响应后就会断开TCP连接。换句话说每次http1.0请求,都会同步新建和关闭TCP连接。
3,http1.1中又了put, patch, delete, header, option等请求。重要的改变是http1.1是默认不关闭TCP连接的,也就是说,三次握手建立了一个TCP连接后可以复用,用来重复发送http请求。
在之前的tornado异步机制浅析中即用socket去发送http请求也提到了1.0和1.1带来的区别。
可是不管怎么发展,一定是client端主动向server请求资源。是否想过,当qq,微信上线是如何收到服务端推送来的未读消息?
长轮询
网购时常有页面聊天室,client端又是怎样获取server推送来的消息呢?
client端可以不停的发送请求去server拿取新消息显示在聊天窗口中。如果1min发送一个请求拿一次那么消息延迟最多1min。如果30s一次,那就可能消息延迟30s。为保证这种即时通信的即时性只能加大请求频率,这样服务器处理的请求数量将极大增多,显然不可取。
长轮询登场:client端只发送一个请求给server,该请求不会立刻返回结果,而是和server端保持了一个连接,当server端收到其他需要推送给此client的消息时,即将内容作为这个http请求结果返回给client。如果始终没有消息待到这个http请求超时后返回,客户端client端会再次发送一个类似的http请求。此种请求轮询拿取信息的方式,称之为长轮询。
和普通http响应相比,server如何在适当的时候(有需要推送给特定客户端的消息进来时)返回这个被挂起的长请求?redis的发布订阅。
Redis
Redis使用率非常高的数据库,数据存在内存之中。通常Redis用来作为缓存,常见有五种数据结构,字符,哈希,数组,集合和有序集合。
http://doc.redisfans.com/
此处用到的是redis的pub/sub功能,即发布/订阅功能。
本地Redis中开两个终端,订阅发布如下:
python-tornado实现
服务端代码如下:
import time
import tornadoredis
from tornado import web, httpserver, ioloop, gen
from tornado.options import options, define
from tornado.escape import json_encode
define(name='port', default=8888, type=int)
class LongPollingHandler(web.RequestHandler):
def initialize(self):
self.client = tornadoredis.Client()
self.client.connect() # 连接到Redis
@web.asynchronous
def get(self):
self.get_data()
# 订阅Redis的消息
@gen.engine
def subscribe(self):
yield gen.Task(self.client.subscribe, 'test_channel')
self.client.listen(self.on_message)
def get_data(self):
if self.request.connection.stream.closed():
return
self.subscribe()
num = 120 # 设置超时时间为120s
ioloop.IOLoop.instance().add_timeout(
time.time() + num,
lambda: self.on_timeout(num)
)
def on_timeout(self, num):
self.send_data(json_encode({'name': '', 'msg': ''}))
if self.client.connection.connected():
self.client.disconnect()
def send_data(self, data): # 发送响应
if self.request.connection.stream.closed():
return
self.set_header('Content-Type', 'application/json; charset=UTF-8')
self.write(data)
self.finish()
def on_message(self, msg): # 收到了Redis的消息
if msg.kind == 'message':
print(msg)
self.send_data(str(msg.body))
elif msg.kind == 'unsubscribe':
self.client.disconnect()
def on_finish(self):
if self.client.subscribed:
self.client.unsubscribe('test_channel')
if __name__ == '__main__':
options.parse_command_line()
app = web.Application(handlers=[(r"/index", LongPollingHandler)])
http_server = httpserver.HTTPServer(app)
http_server.listen(options.port)
ioloop.IOLoop.instance().start()
启动服务器后,浏览器请求资源:
可以看出,此时这个http请求处于挂起状态,当手动去Redis数据库中发布对应订阅频道的消息后结果如下:
这样python+redis+tornado的长轮询基本实现。
在实现过程中参考了:
http://www.it610.com/article/597504.htm中的server端代码