Django Channels 学习笔记

时间:2021-11-15 23:56:55

一.为什么要使用Channels

  在Django中,默认使用的是HTTP通信,不过这种通信方式有个很大的缺陷,就是不能很好的支持实时通信。如果硬是要使用HTTP做实时通信的话只能在客户端进行轮询了,不过这样做的开销太大了。
  因此,在1.9版本之后,Django实现了对Channels的支持,他所使用的是WebSocket通信,解决了实时通信的问题,而且在使用WebSocket进行通信的同时依旧能够支持HTTP通信。

二.创建一个Django Channels样例的完整流程

1. 首先下载相应插件

pip install channels
pip install asgi_redis

2. 创建一个新工程

django-admin.py startproject channels_example


工程目录如下:
|-- channels_example
|    |--channels_example
|        |-- __init__.py
|        |-- settings.py
|        |-- urls.py
|        |-- wsgi.py
|    |-- manage.py

这里我们要在内层的channels_example目录下创建几个channels所需的必要文件。
包括:
routing.py
consumer.py
asgi.py

这几个文件先放着,之后我会一一介绍。
新的目录如下:
|-- channels_example
|    |--channels_example
|        |-- __init__.py
|        |-- settings.py
|        |-- urls.py
|        |-- wsgi.py
|        |-- routing.py
|        |-- consumer.py
|        |-- asgi.py
|    |-- manage.py

3. 设置settings.py中的参数

这里首先将channels加入到INSTALLED_APPS中,如下:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'channels', ]

然后,添加新的参数CHANNEL_LAYERS,如下:

CHANNEL_LAYERS = { "default": { "BACKEND": "asgiref.inmemory.ChannelLayer", "ROUTING": "channels_example.routing.channel_routing", }, }

这里,需要注意的是 ROUTING 参数,他是用来指定WebSocket表单的位置,当有WebSocket请求访问时,就会根据这个路径找到相应表单,调用相应的函数进行处理。
channels_example.routing 就是我们刚才建好的routing,py文件,里面的channel_routing我们下面会进行填充。

4. 填写新的表单——routing.py文件

上一步我们已经了解到,routing.py文件其实就是个表单,功能和Django原来的urls.py文件一样,不过这里使用的不是URL,而是请求的类型。

from channels.routing import route from channels_example import consumers  #导入处理函数
 channel_routing = [ #route("http.request", consumers.http_consumer), 这个表项比较特殊,他响应的是http.request,也就是说有HTTP请求时就会响应,同时urls.py里面的表单会失效
 route("websocket.connect", consumers.ws_connect),        #当WebSocket请求连接上时调用consumers.ws_connect函数
    route("websocket.receive", consumers.ws_message),        #当WebSocket请求发来消息时。。。
    route("websocket.disconnect", consumers.ws_disconnect),    #当WebSocket请求断开连接时。。。
]

5. 填写新的视图文件——consumers.py

上一步我们已经了解到,routing.py相当于新的urls.py,而consumers.py就相当于新的view.py。

代码:

from django.http import HttpResponse from channels.handler import AsgiHandler #message.reply_channel 一个客户端通道的对象 #message.reply_channel.send(chunk) 用来唯一返回这个客户端

#一个管道大概会持续30s

#当连接上时,发回去一个connect字符串
def ws_connect(message): message.reply_channel.send({"connect"}) #将发来的信息原样返回
def ws_message(message): message.reply_channel.send({ "text": message.content['text'], }) #断开连接时发送一个disconnect字符串,当然,他已经收不到了
def ws_disconnect(message): message.reply_channel.send({"disconnect"})

6. asgi.py文件

类似于Django自己生成的wsgi.py文件,内容如下:

import os import channels.asgi os.environ.setdefault("DJANGO_SETTINGS_MODULE", "channels_example.settings")    #这里填的是你的配置文件settings.py的位置
channel_layer = channels.asgi.get_channel_layer()

7. 发布运行

python manage.py runserver 0.0.0.0:8000

这是一个测试用的JS代码,能够发送和接收WebSocket请求:

<!DOCTYPE HTML>
<html>
   <head>
   <meta charset="utf-8">
   <title></title>
    
      <script type="text/javascript">
         function WebSocketTest() { if ("WebSocket" in window) { alert("您的浏览器支持 WebSocket!"); socket = new WebSocket("ws://" + "localhost:8000" + "/channels_example/"); socket.onmessage = function(e) { alert(e.data); } socket.onopen = function() { socket.send("hello world"); } // Call onopen directly if socket is already open
                if (socket.readyState == WebSocket.OPEN) socket.onopen(); } else { // 浏览器不支持 WebSocket
 alert("您的浏览器不支持 WebSocket!"); } } </script>
        
   </head>
   <body>
   
      <div id="sse">
         <a href="javascript:WebSocketTest()">运行 WebSocket</a>
      </div>
      
   </body>
</html>

 

三.进阶 —— Group

 

四.进阶 —— message详解

 

五.遇到的问题

1. 后来的请求可能会打断之前请求的执行

比如,我写了下面三个函数响应connect,send和disconnect

@channel_session def ws_message(message): #送给全组人
    print message.channel_session['room'] Group("chat-%s" % message.channel_session['room']).send({ "text": message.content['text'], }) @channel_session def ws_connect(message): room = message.content['path'].strip("/") message.channel_session['room'] = room Group("chat-%s" % room).add(message.reply_channel) @channel_session def ws_disconnect(message): print message.channel_session['room'] Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)

 

客户端我这样写:

socket.onopen = function() { socket.send("hello world"); }

 

这样就会产生一个问题,当客户端连接到服务端时,服务端执行ws_connect()函数,然后客户端又send()了数据,服务端就会去调用ws_message()函数,而终止了ws_connect()函数的执行。

此时,我的ws_connect()函数还未对message.channel_session['room']进行赋值,而ws_message()函数就调用了他,所以会报错。

然后我尝试了断开连接,运行正常,这就说明ws_connect()函数最终还是执行完了,不过是在ws_message()函数执行之后。

综上,当服务端接收到新的请求时,可能会打断上个请求的执行,这点需要特殊注意一下。