Python/Go

django websocket

2019-03-18  本文已影响0人  manbug

日常的网络通信大多使用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的版本.
简单说下, 首先startappchat, 假如这里我们没有进行前后端分离, 里面有templates, 两个html:indexroom分别对应首页和某一个聊天室
新建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
        }))

如上, connectdisconnect含义分别如函数名. 因为是聊天室, 所以同一个聊天室内的人应该消息共享, 用room_group_name来区分所在的频道.
receivechat_message是对消息的处理. 当一个用户发送消息时, 前端把消息通过websocket发送过来, receive收到消息提取关键内容, 通过chat_message发送给组内的所有连接. 这时保持连接的所有组内人员都会收到这条消息推送, 前端收到推送再显示在屏幕上.
定义websocket的地址
类似于djangourl(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中要配置路由跳转


ingress2.png
上一篇下一篇

猜你喜欢

热点阅读