日常的网络通信大多使用http/https, 在这种协议下通信的建立取决于客户端的需要, 那么存不存在一种由服务端主导通信的协议呢
前言
做个比喻, 如果说A是服务端, B是客户端, 现在要在A家里吃火锅, 虽然A说你人来就行, 但是B心想总得带点东西过去, 于是去了市场.
先到了蔬菜店, B想买点菠菜, 但又怕A家里已经有了, 于是给A打电话
B: "我带点菠菜过去吧?"
A: "好"
然后挂断. 过一会儿到了水产区
B: "我带点虾过去吧?"
A: "不用"
...如此反复多了之后A突然发现自己确实少准备了一些东西, 于是A给主动给B打了电话
A: "我忘准备蘸料了, 你买点, 然后先别挂掉"
...
A: "再买瓶酒"
...
这就是websocket了
websocket库
django当让也提供对websocket的支持, 虽然这似乎不是他更擅长的东西. 我们可以通过channels实现websocket连接
使用场景
诸如上述例子的场景都是合适的场景
举例来说的话比如聊天室, 每个人发送的消息都要实时显示在别人的屏幕上.
比如说数据监控, 波动状态也要实时的呈现在屏幕上, 而不是依赖于使用者自己刷新.
django channels 安装/配置
需要安装channels
, asgi_redis
, asgiref
, channels_redis
. 后三个未必都需要装, 记不太清了, 总之安装过程都在channels的使用文档上.
INSTALL_APPS
中需要加上"channels"
, 需要注意的是因为这是一个list, 是有先后顺序的, 最好把它加在第一个.
这里我们的channel通过redis实现, 要在settings.py
中配置
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [{"address": (REDIS_HOST, REDIS_PORT), "db": 8, "password": REDIS_PASSWD}]
},
},
}
这里还有点小坑, 官方文档里的hosts不是这种格式, 是"uri"
这种模式, 但是如果你在设置redis密码时机智的设置了特殊符号('#$%'
这种), 你就会发现redis的uri
直接就用不了了, 期间尝试各种方法, 转义什么的也试了都不行, 然后去github上开了个issue, 结果作者说我们是通过aioredis
连接的, 你去找他们的文档吧....
然后就找到了这种方式.
常规的WSGI不支持websocket, 所以还需要配置ASGI
ASGI_APPLICATION = 'project.routing.application'
同wsgi的配置一样, 这是指向project
文件夹下routing.py
文件的application
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
使用
这里建议大家跟这官方教程的Tutorial走一遍. 有个比较悲剧的地方就是网上可以搜到许多channels使用指南, 大多都是搭个简易聊天室什么的, 然而你用起来可能发现存在各种报错, 因为channels升了2.0之后更改了一些方法, 而那些教程里基本全都是1.x的版本.
简单说下, 首先startapp
叫chat
, 假如这里我们没有进行前后端分离, 里面有templates
, 两个html
:index
和room
分别对应首页和某一个聊天室
新建consumers.py
来写websocket
方法
import json
from backend.dao.db_conn import redis_pool_db9
from backend.service.dashboard import preparing_list
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from django.conf import settings
# test: 测试用, 聊天室demo
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
username = text_data_json['username']
if message == "1":
message = "mark it."
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': username
}
)
# Receive message from room group
def chat_message(self, event):
message = event['message']
username = event['username']
if message == "1":
message = "mark it."
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message,
'username': username
}))
如上, connect
和disconnect
含义分别如函数名. 因为是聊天室, 所以同一个聊天室内的人应该消息共享, 用room_group_name
来区分所在的频道.
receive
和chat_message
是对消息的处理. 当一个用户发送消息时, 前端把消息通过websocket发送过来, receive
收到消息提取关键内容, 通过chat_message
发送给组内的所有连接. 这时保持连接的所有组内人员都会收到这条消息推送, 前端收到推送再显示在屏幕上.
定义websocket的地址
类似于django
的url
(consumers.py
就类似于views.py
), 同级新建routing.py
from django.conf.urls import url
from . import consumers
websocket_urlpatterns = [
url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
url(r'^ws/dashboard/daily_left/$', consumers.DailyLeftConsumer),
url(r'^ws/dashboard/assigning_queue/$', consumers.AssigningQueueConsumer),
]
统一用ws/来区分websocket的连接
剩下常规的页面配置和django一样
views.py
:
from django.shortcuts import render
from django.utils.safestring import mark_safe
import json
def index(request):
return render(request, 'chat/index.html', {})
def room(request, room_name):
return render(request, 'chat/room.html', {
'room_name_json': mark_safe(json.dumps(room_name))
})
urls.py
:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^(?P<room_name>[^/]+)/$', views.room, name='room'),
]
前端配置
注意: 如果网站是http, 连接使用ws, 如果是https要修改成wss
var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
var chatSocket = new WebSocket(
{#ws_scheme + '://' + window.location.host +#}
ws_scheme + '://' + window.location.hostname +
'/ws/chat/' + roomName + '/');
剩下的自己找资料吧, 笔者对前端了解的不多
启动
本地的话runserver
就好了, 但是在线上还是得更改启动方式应对高并发.
传统的uwsgi
不支持websocket
.
gunicorn
好像可以同时支持websocket
, 但是性能不太ok
这里我们用daphne
daphne -b 0.0.0.0 -p 8001 mshan.asgi:application --access-log /var/log/daphne.log
k8s配置
这里需要额外开个服务, 专门负责处理websocket.
ingress中要配置路由跳转