Context
Http是在tcp的基础上做了高级封装,变成了一个被动的,短链接请求.请求完了就把连接断开了,下一次要请求,就需要再一次进行tcp握手,然后通信……在前面第一篇中,我用了定时轮训的方式问服务器,当前这个二维码是否登录,从功能的角度来说,应该是没有问题的,而且写起来也是特别的方便.
不过仔细的想一想,其实还是比较浪费资源的.从信息论的角度来说,中间的N次质询,相当于没有任何的信息量(请不要深究这一句),所以,我们不用http来更新扫码状态!
我们用
Web Socket
!
Web Socket
关于websocket的定义,解释什么的,百度一下,你全都知道了,所以我就不在这个地方多说.
只说明一点,websocket屌的飞起,和socket的操作一毛一样,关键是服务器终于可以主动的发消息给客户端啦!
他算是一个长连接,握手之后,就开始了肮脏的通信,通信完了,一方可以发起关闭操作,然后……,最后连接就拆掉了.
Django
websocket怎么说也就只能算是对web服务器的一个扩充,如何在已有的web服务器上快速集成一个websocket的服务,才是比较重要的~
由于之前用的是django做服务器,所以我们就找找给django加上websocket的方法~
在他的官方文档中提到了很多可用的开源app,其中有一个叫做channels的库,他用了asgi,直接可以快速集成在django中,牛的一批.
Channels
这个库的文档,真的是无力吐槽,我真是集成的很辛苦
我们来简化一下他的文档吧!
Router
和django一样,websocket的连接也要有个路由的,我们先建立一个py文件
from channels.routing import route
# 这个文件等会儿解释
from users.webClient import connection_handler, disconnection_handler,subscribe_token
channel_routing = [
route("websocket.connect", connection_handler, path=r"/ws/$"),
route("websocket.disconnect", disconnection_handler, path=r"/ws/$"),
route("websocket.receive", subscribe_token, path=r"/ws/$"),
]
这里的websocket.connect
,websocket.disconnect
,websocket.receive
相当于是websocket的三种事件,已连接、已断开、收到消息
中间那个参数和django里面的那个描述一样,就是该路由对应的处理方法.
Handler
现在来解释一下上一节说的 users.webClient这个文件
# -*- coding: utf-8 -*-
import json
from channels.sessions import channel_session
from users.qrcode_helper import *
@channel_session
def connection_handler(message):
message.reply_channel.send({"text": json.dumps({"status":False,"msg":"connected"})})
@channel_session
def subscribe_token(message):
try:
obj = json.loads(message.content["text"])
tokens = obj["token"]
qrcode_record = fetch_qrcode_record_with_token(token=tokens)
if qrcode_record is not None:
if is_token_pass_the_authentic(tokens):
message.reply_channel.send(
{"text": json.dumps({"status": True, "msg": "succeed"})})
else:
message.reply_channel.send(
{"text": json.dumps({"status": False, "msg": "succeed"})})
qrcode_record.webSocket_session = str(message.content["reply_channel"])
qrcode_record.save()
# link token to this channel
else:
message.reply_channel.send({"text": json.dumps({"status": "-1", "msg": "token invalid"})})
message.reply_channel.send({"close": True})
except Exception as e:
message.reply_channel.send({"text": e.message})
message.reply_channel.send({"close": True})
@channel_session
def disconnection_handler(message):
channel = str(message.content["reply_channel"])
qrcode_records = fetch_qrcode_record_with_web_socket_channel(channel)
if qrcode_records is not None:
for qrcode_record in qrcode_records:
qrcode_record.webSocket_session = ""
qrcode_record.save()
很长,也不用仔细看,因为里面主要做了扫码登录里面的事情,如果只是要集成一下websocket的话,不用关心这些方法里面的操作,只要注意这里每个方法的传入参数 message
Message有那些用处呢?
1.通过message.channel或者message.reply_channel可以获取到连接的通道(请暂时不要使用高级操作)
2.message.content其实是一个字典,这个字典遵循了ASGI的消息模型,里面有text
,reply_channel
等等字段
在我的处理代码里面,我从message.content里面取出了reply_channel
,他的value是一个长得很奇怪的字符串(我把它类比成文件描述符把),毕竟要有很多很多很多连接的话,它们的文件描述符不能重复对吧,所以就又长又难看,比如像daphne.response.FSITXWDDzG!IOtDNILqzh
有了这个reply_channel,在后面主动发送消息下去的时候,就可以指定连接发送了~
Attention
注意,这里的websocket.connect的handler里面一定一定,千万千万要给反馈消息回去,不然浏览器(只测试了Chrome的)会以为连接超时的!然后给你一个503 bad gateway,之后就主动断开连接了,留你一脸蒙逼的在那儿傻看着.
Send
现在我们可以看看之前第一章中的app调用的扫码接口了
@csrf_exempt
@require_POST
@pass_auth
@require_parameter(["code"])
def allow_the_qrcode_login(request):
code = request.POST["code"]
user = get_user_from_response_session(request)
qr_record = fetch_qrcode_record_with_code(code)
if user is not None and qr_record is not None:
if qr_record.user_id is not None:
return JsonResponse({"msg": "expired", "status": -1}, status=400)
qr_record.user_id = user.user_uuid
qr_record.status = True
qr_record.save()
if qr_record.webSocket_session is not None:
# send new status to the web socket
channel = Channel(name=qr_record.webSocket_session)
content = {"text": json.dumps({"status": True, "msg": "succeed"})}
channel.send(content)
pass
return JsonResponse({"msg": "succeed", "status": 0}, status=200)
return JsonResponse({"msg": "code not existed", "status": -400}, status=400)
这里加了一些些小的改动,我再把他取出来
if qr_record.webSocket_session is not None:
channel = Channel(name=qr_record.webSocket_session)
content = {"text": json.dumps({"status": True, "msg": "succeed"})}
channel.send(content)
websocket.receive的时候,我把websocket的文件描述符记录在了qrcode的记录中,这样扫描某一个qrcode的时候,就可以知道这个二维码是否有websocket正在订阅他,有订阅的话,就可以拿来对指定的通道进行发送了
End
网页端真的没什么好说的,直接贴(里面没有很严密的控制权限,稍微改一改就行,但是我懒)
function listen_for_status() {
socket = new WebSocket("ws://" + window.location.host + '/ws/');
socket.onmessage = function (e) {
var obj = JSON.parse(e.data);
if (obj.status == true) {
socket.close();
window.location="dashboard.html";
}
};
socket.onopen = function () {
var obj = {};
obj.token = token;
socket.send(JSON.stringify(obj));
};
}
还有不要忘记更新nginx的配置
扫码登录的这个小玩意儿,虽然看起来很是简单,但是也耗了我很多的时间,这个过程中,也是学到了很多新的东西呢!
这次也是达成了一个新的成就:WebSocket(Server&Client)