Pulpcode

捕获,搅碎,拼接,吞咽

0%

服务器推送技术

预备知识

web的工作原理,可以概括为:

  1. 客户端(浏览器)通过TCP/IP协议,建立到服务器的TCP链接。
  2. 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档,
  3. 服务器向客户端发送HTTP协议应答包。
  4. 客户端与服务器断开,客户端解释HTML文档,在客户端屏幕上渲染返回结果。

那么,可以发现,HTTP协议有两个问题:

  1. HTTP是无状态协议,所以,上次的请求和这次的请求之间没有关系。
  2. HTTP协议是单向的,也就是说,服务器不能向客户端发送请求。
  • 对于第一个问题,可以在服务器使用session,或者在客户端使用cookie,能够解决“无状态协”议这个问题.
  • 对于第二个问题,就是此篇博客想要讨论。

本文将比较三种服务器推送方式。

  1. ajax轮询
  2. longpoll
  3. websocket

在实例代码中,我前台使用javascript和一些jquery,后台web服务器使用了tornado。之所以使用tornado,是因为

  1. 人生苦短,我用python :-)
  2. tornado对long poll,和web socket都有很好的支持,而且,tornado是异步非阻塞式服务器,处理起需要服务器推送,交互性强的web应用,非常适合。

我的代码要实现这样一个例子:

在服务器端,保存一个字符串,连接到服务器的用户,将看到这个字符串,同时也可以修改它,当修改此字符串的时候,连接到此服务器的其他用户,能够在不主动刷新的情况下,看到修改。这就用到了“服务器推”的技术

1.ajax轮询

所谓ajax轮询实现服务器推技术,就是客户端,每隔一段时间,“偷偷”的发送ajax请求,之后,将返回的数据局部刷新,这样感觉就像是服务器在推数据.

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
import tornado.web
import tornado.httpserver
import tornado.ioloop
import tornado.options

import json

#用来保存服务器端的字符串
class Announce():
subject = "nima"
#使用json传送数据
def getSubjson(self):
return json.dumps({'content':self.subject})

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html")

# 修改字符串,为post请求
# ajax 隔一段时间,会发送get请求
class ChatHandler(tornado.web.RequestHandler):
def post(self):
self.application.announce.subject = self.get_argument('content')
self.write(self.application.announce.getSubjson())
def get(self):
self.write(self.application.announce.getSubjson())
class Application(tornado.web.Application):
"""
"""

def __init__(self):
"""
"""
self.announce = Announce()
handlers = [
(r'/',MainHandler),
(r'/chat',ChatHandler),
]
settings = {
'template_path': 'templates',
'static_path': 'static',
'debug': True
}
tornado.web.Application.__init__(self, handlers, **settings)

if __name__ == '__main__':
tornado.options.parse_command_line()
app = Application()
server = tornado.httpserver.HTTPServer(app)
server.listen(8000)
tornado.ioloop.IOLoop.instance().start()

下面是javascript代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script type="text/javascript">
$(function(){
updateMsg();

$("#mypp").click(function(){
$.post("chat",{
content: $("#message").val()
},function(data, textStatus){
var content = data.content;
var txt = "<p>"+content+"</p>";
$("#chatContent").html(txt);
},"json");
});
});

function updateMsg(){
$.get("chat",{},function(data, textStatus){
var content = data.content
var txt = "<p>"+content+"</p>";
$("#chatContent").html(txt);
},"json");
setTimeout(updateMsg, 4000);
}
</script>

updateMsg 隔一段时间,会发送数据请求,注意setTimeout(updateMsg, 4000);定时器
可以看出,ajax轮询有两个明显的缺点,

  1. 如果轮询的频率低服务器端的数据更新后,客户端不能马上更新
  2. 如果轮询的频率高,那么服务器大多是些无用的请求,反而会增加服务器的压力。

Long Poll

那么就来看看第二种方式,long poll, 这算是一种”真正的”的服务器推送技术(comet)
long poll的原理是,客户端与服务器将建立一条长连接,也就是说,客户端会发出一个请求,而服务器,将阻塞请求,直到有数据需要传递,才会返回。
返回之后,客户端将关闭此连接,然后再次发出一个请求,建立一个新的连接,再次等待服务器推送数据.
服务器端实现:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import tornado.web
import tornado.httpserver
import tornado.ioloop
import tornado.options

from uuid import uuid4
import json

#服务器端保存的字符串
class Announce():
subject = "nima"
callbacks = []

def register(self, callback):
self.callbacks.append(callback)

#改变后,会推送给保存的注册的客户端
def changeSubject(self, data):
self.subject = data
self.notifyCallbacks()

def getJson(self):
return json.dumps({'content':self.subject})

def notifyCallbacks(self):
for c in self.callbacks:
self.callbackHelper(c)

self.callbacks = []

def callbackHelper(self, callback):
callback(self.getJson())

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html")

class ChatHandler(tornado.web.RequestHandler):
def post(self):
content = self.get_argument('content')
self.application.announce.changeSubject(content)

# StatusHandler的处理是异步的
class StatusHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
self.application.announce.register(self.async_callback(self.on_message))

def on_message(self, data):
self.write(data)
# 必须要finish 否则服务器会一直阻塞
self.finish()

class Application(tornado.web.Application):
"""
"""

def __init__(self):
"""
"""
self.announce = Announce()
handlers = [
(r'/',MainHandler),
(r'/chat',ChatHandler),
(r'/status',StatusHandler),
]
settings = {
'template_path': 'templates',
'static_path': 'static',
'debug': True
}
tornado.web.Application.__init__(self, handlers, **settings)

if __name__ == '__main__':
tornado.options.parse_command_line()
app = Application()
server = tornado.httpserver.HTTPServer(app)
server.listen(8000)
tornado.ioloop.IOLoop.instance().start()

客户端与服务器端的链接,会一直保存着,当发生改变时,服务器才会把数据推送给客户端.
这其实就是设计模式中的观察者模式。

客户端js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script type="text/javascript">
$(function(){

setTimeout(requestInventory, 100);

$("#mypp").click(function(){
$.post("//localhost:8000/chat",{
content: $("#message").val()
},type="json");
});
});

function requestInventory(){
$.getJSON("//localhost:8000/status",{},function(data, Status, xhr){
var content = data.content;
var txt = "<p>"+content+"</p>"
$("#chatContent").html(txt);
setTimeout(requestInventory, 0);
});
}
</script>

可以看到,在requestInventory()中,每次数据返回后,setTimeout(requestInventory, 0);将建立一条新的链接.
其实还有一种类似与long poll的技术,iframe流方式,这种方式在页面插入一个隐藏的iframe.利用其src属性,在服务器和客户端之间建立一条长连接。与long poll不同的是,iframe流的这条连接会一直存在,而不是像long poll在数据返回后,客户端关闭此连接,然后重新开启一条连接。

但是,comet中采用的长连接,也会大量的消耗服务器的带宽和资源。

Websocket

websocket,是html5引入的一个特性,也是未来的趋势,web socket 是在浏览器和服务器之间进行全双工通信的网络技术,既然是全双工通信,那么服务器自然可以主动传送数据给服务器,而且通信协议的header也很小,相比与之前的long poll, web socket 能够更好的节省服务器资源和宽带并达到实时通信.

服务器端代码

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import tornado.web
import tornado.websocket
import tornado.httpserver
import tornado.ioloop
import tornado.options

import json

class Announce():
subject = "nima"
callbacks = []

def register(self, callback):
self.callbacks.append(callback)

def unregister(self, callback):
self.callbacks.remove(callback)

def getJson(self):
return json.dumps({'content':self.subject})


def changeSubject(self, data):
self.subject = data
self.notifyCallbacks()

def notifyCallbacks(self):
for c in self.callbacks:
self.callbackHelper(c)


def callbackHelper(self, callback):
callback(self.getJson())

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html")

class ChatHandler(tornado.web.RequestHandler):
def post(self):
content = self.get_argument('content')
self.application.announce.changeSubject(content)

# 注意,这个类的父类,用来进行web socket的WebSocketHandler
class StatusHandler(tornado.websocket.WebSocketHandler):
def open(self):
self.application.announce.register(self.callback)

def on_close(self):
self.application.announce.unregister(self.callback)

def on_message(self, message):
pass

def callback(self, data):
self.write_message(data)

class Application(tornado.web.Application):
"""
"""

def __init__(self):
"""
"""
self.announce = Announce()
handlers = [
(r'/',MainHandler),
(r'/chat',ChatHandler),
(r'/status',StatusHandler),
]
settings = {
'template_path': 'templates',
'static_path': 'static',
'debug': True
}
tornado.web.Application.__init__(self, handlers, **settings)

if __name__ == '__main__':
tornado.options.parse_command_line()
app = Application()
server = tornado.httpserver.HTTPServer(app)
server.listen(8000)
tornado.ioloop.IOLoop.instance().start()

客户端,也是js对websocket 的操作.
注意 var host =’ws://localhost:8000/status’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script type="text/javascript">
$(function(){
requestInventory();
$("#mypp").click(function(){
$.post("chat",{
content: $("#message").val()
},type="json");
});
});

function requestInventory(){
var host ='ws://localhost:8000/status'
var websocket = new WebSocket(host);

websocket.onopen = function (evt) {};
websocket.onmessage = function (evt){
var content = $.parseJSON(evt.data)['content'];
var txt = "<p>"+ content +"</p>"
$("#chatContent").html(txt);
};
websocket.onerror = function (evt) {};
}
</script>

html5就是未来的趋势,而且,chrome,firefox,opera和safari都支持,IE从版本10开始也支持了。