1.长轮询
轮询:在前端通过写js实现。缺点:有延迟、服务器压力大。
就是客户端通过一定的时间间隔以频繁请求的方式向服务器发送请求,来保持客户端和服务器端的数据同步。问题很明显,当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,带来很多无谓请求,浪费带宽,效率低下。
长轮询
首先需要为每个用户维护一个队列,用户浏览器会通过js递归向后端自己的队列获取数据,自己队列没有数据,会将请求夯住(去队列中获取数据),夯一段时间之后再返回。
注意:一旦有数据立即获取,获取到数据之后会再发送请求。
用户发来请求之后,最多会夯住N秒(30s),因为有消息的时候回立即返回,没有消息时才最多夯N秒。
2.websocket
2.1 原理
- WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:
- WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;
- WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。
2.2具体实现:
-
客户端向服务端发送随机字符串,在http的请求头中
Sec-WebSocket-Key
-
服务端接受到到随机字符串
- 随机字符串 + 魔法字符串
- sha1加密
- base64加密
- 放在响应头中给用户返回
Sec-WebSocket-Accept
-
客户端浏览器会进行校验,校验不通过:服务端不支持websocket协议。
-
客户端和服务端进行相互收发消息,数据加密和解密。
-
解密细节:
-
获取第二个字节的后7位,payload len
-
判断payload len的值
-
= 127
2字节 + 8字节 + 4字节 + 数据
-
= 126
2字节 + 2字节 + 4字节 + 数据
-
<= 125
2字节 + 4字节 + 数据
-
-
使用masking key 对数据进行解密
-
2.3 手动创建支持websocket的服务端(理解)
-
服务端
import socket import hashlib import base64 def get_headers(data): """ 将请求头格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') header, body = data.split('\r\n\r\n', 1) header_list = header.split('\r\n') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(' ')) == 3: header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ') else: k, v = header_list[i].split(':', 1) header_dict[k] = v.strip() return header_dict def get_data(info): """ 对数据进行就解密 """ payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding='utf-8') return body sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8002)) sock.listen(5) # 等待用户连接 conn, address = sock.accept() # 握手环节 header_dict = get_headers(conn.recv(1024)) # 公认的魔法字符串(固定) magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' random_string = header_dict['Sec-WebSocket-Key'] value = random_string + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) # 给前端发送的响应数据 response = "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://127.0.0.1:8002\r\n\r\n" response = response %ac.decode('utf-8') conn.send(response.encode('utf-8')) # 接受数据 while True: data = conn.recv(1024) msg = get_data(data) print(msg)
-
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <input type="button" value="开始" onclick="startConnect();"> <script> var ws = null; function startConnect() { // 1. 内部会先发送随机字符串 // 2. 内部会校验加密字符串 ws = new WebSocket('ws://127.0.0.1:8002') } </script> </body> </html>
2.4 企业应用
我们学过django和flask框架,内部基于wsgi做的socket,默认他俩都不支持websocket协议,只支持http协议。
-
flask中应用
pip3 install gevent-websocket
-
django中应用
pip3 install channels
请保留2个案例(django):
在基于Django实现的时候因为需要导入channels
模块,会跟Django中的wsgiref
发生冲突所以需要在配置文件中添加配置:
1.首先是在settings文件中的需要注册这个应用:
INSTALLED_APPS = ['channels']
2.在settings文件中增加一个文件的路径:
ASGI_APPLICATION = "wechat.routing.application"
这个文件是用来写前端使用websocket发送数据时指定的接收路径,相当于url;
-
1v1示例
-
接收数据的url
from channels.routing import ProtocolTypeRouter, URLRouter from django.conf.urls import url from app01 import consumers application = ProtocolTypeRouter({ 'websocket': URLRouter([ url(r'^x1/$', consumers.ChatConsumer), ]) })
-
内部处理的数据的代码:
from channels.exceptions import StopConsumer from channels.generic.websocket import WebsocketConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): """ websocket连接到来时,自动执行 """ print('有人来了') self.accept() def websocket_receive(self, message): """ websocket浏览器给发消息时,自动触发此方法 """ print('接收到消息', message) self.send(text_data='收到了') # self.close() def websocket_disconnect(self, message): print('客户端主动断开连接了') raise StopConsumer()
-
前端操作的代码:
<script> # 指定一个变量 var ws; # 当加载页面时自动执行 $(function () { # 执行函数 initWebSocket(); }); function initWebSocket() { # 与后端的websocket建立连接,创建一个对象 ws = new WebSocket("ws://127.0.0.1:8000/x1/"); # 成功得到时候自动触发这个方法 ws.onopen = function(){ $('#tips').text('连接成功'); }; # 接收到消息的时候自动触发这个方法; ws.onmessage = function (arg) { var tag = document.createElement('div'); tag.innerHTML = arg.data; $('#content').append(tag); }; # 服务端断开连接客户端也断开 ws.onclose = function () { ws.close(); } } # 向服务端发送数据,使用创建的ws对象.send方就可以发送数据 function sendMessage() { ws.send($('#txt').val()); } </script>
-
-
n v n 示例
- 在routing文件中的代码:前端websocket发送数据的路径
from channels.routing import ProtocolTypeRouter, URLRouter from django.conf.urls import url from app01 import consumers application = ProtocolTypeRouter({ 'websocket': URLRouter([ url(r'^x1/$', consumers.ChatConsumer), ]) }) # 这个文件写的是和前端websocket创建连接的路径,
-
在consumers文件中的代码:websocket的模块应用
这里主要是接收数据对数据进行一个处理
from channels.exceptions import StopConsumer from channels.generic.websocket import WebsocketConsumer from asgiref.sync import async_to_sync class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): """ websocket连接到来时,自动执行 """ print('有人来了') # 里边的'22922192'字符串是随意指定的,相当于一个标识; async_to_sync(self.channel_layer.group_add)('22922192', self.channel_name) self.accept() def websocket_receive(self, message): """ websocket浏览器给发消息时,自动触发此方法 """ print('接收到消息', message) # 接收到消息 {'type': 'websocket.receive', 'text': 'fdsfsdfafd'} async_to_sync(self.channel_layer.group_send)('22922192', { # 这个是指定函数执行 'type': 'xxx.ooo', # 将获取到的消息 'message': message['text'] }) def xxx_ooo(self, event): # 触发这个方法的时候就将消息发出 message = event['message'] self.send(message) def websocket_disconnect(self, message): """ 断开连接 """ print('客户端主动断开连接了') async_to_sync(self.channel_layer.group_discard)('22922192', self.channel_name) raise StopConsumer()
另一种websocket的写法
class NewChatConsumer(WebsocketConsumer): def connect(self): print('有人来了') async_to_sync(self.channel_layer.group_add)('22922192', self.channel_name) self.accept() def receive(self, text_data=None, bytes_data=None): print('接收到消息', text_data) async_to_sync(self.channel_layer.group_send)('22922192', { 'type': 'xxx.ooo', 'message': text_data }) def xxx_ooo(self, event): message = event['message'] self.send(message) def disconnect(self, code): print('客户端主动断开连接了') async_to_sync(self.channel_layer.group_discard)('22922192', self.channel_name)
其实都是一样的,只是这个源码内部是多了几步的调用,而这种写法是将调用的步骤省去;
- 前端websocket的写法:
<script> # 指定一个变量 var ws; # 当加载页面时自动执行 $(function () { # 执行函数 initWebSocket(); }); function initWebSocket() { # 与后端的websocket建立连接,创建一个对象 ws = new WebSocket("ws://127.0.0.1:8000/x1/"); # 成功得到时候自动触发这个方法 ws.onopen = function(){ $('#tips').text('连接成功'); }; # 接收到消息的时候自动触发这个方法; ws.onmessage = function (arg) { var tag = document.createElement('div'); tag.innerHTML = arg.data; $('#content').append(tag); }; # 服务端断开连接客户端也断开 ws.onclose = function () { ws.close(); } } # 向服务端发送数据,使用创建的ws对象.send方就可以发送数据 function sendMessage() { ws.send($('#txt').val()); } </script>
来源:oschina
链接:https://my.oschina.net/u/4332571/blog/4093089