转载:https://www.vimiix.com/post/2018/07/26/channels2-tutorial/

认识 Channels 之前,需要先了解一下 asgi ,全名:Asynchronous Server Gateway Interface。它是区别于 wsgi 的一种异步服务网关接口,不仅仅只是通过 asyncio 以异步方式运行,还支持多种协议。完整的文档戳这里

关联的几个项目:

  • https://github.com/django/asgiref ASGI内存中的通道层,函数的同步异步之间相互转化需要
  • https://github.com/django/daphne 支持HTTP,HTTP2和WebSocket协议服务器,启动 Channels 的项目需要
  • https://github.com/django/channels_redis Channels专属的通道层,使用Redis作为其后备存储,并支持单服务器和分片配置以及群组支持。(这个项目是 Channels 的一个附属项目,配置的时候作为可选项使用,该软件包的早期版本被称为 asgi_redis,如果你在使用 Channels 1.x项目,它仍可在PyPI下通过这个名称使用。但 channels_redis 仅适用于 Channels 2 项目。)

备注:之前体验使用过一个基于 asgi 的web 框架 uvicon,也是同类项目。

流程图

实践

还是和官方同步,以一个聊天室的例子说起,Channels 提供从 PYPI 直接pip下载安装,

  1. pip install -U channels==2.0.2 Django==2.0.4 channels_redis==2.1.1

这里,我们不仅安装了 Channels 2.0,还安装了Django 2.0 作为我们web项目框架,安装了channels_redis作为 Channels的后端存储通道层。

下载好包以后,把 channels 作为 Django 的一个应用(Django新建项目就不赘述了,假设我们已经建立好另一个项目叫 my_project),添加到配置文件的 INSTALLED_APPS中:

  • my_project/settings.py

    1. INSTALLED_APPS = (
    2. 'django.contrib.auth',
    3. 'django.contrib.contenttypes',
    4. 'django.contrib.sessions',
    5. 'django.contrib.sites',
    6. ...
    7. 'channels',
    8. 'chat'
    9. )

    chat是我们准备建立的聊天室应用,接着就建立我们的 asgi 应用,并指定其要使用的路由。在 settings 同级目录下新建一个 routing.py 的文件:

  • my_project/routing.py
    1. from channels.auth import AuthMiddlewareStack
    2. from channels.routing import ProtocolTypeRouter, URLRouter
    3.  
    4. import chat.routing
    5.  
    6. application = ProtocolTypeRouter({
    7. # (http->django views is added by default)
    8. # 普通的HTTP请求不需要我们手动在这里添加,框架会自动加载过来
    9. 'websocket': AuthMiddlewareStack(
    10. URLRouter(
    11. chat.routing.websocket_urlpatterns
    12. )
    13. ),
    14. })

    chat.routing 以及 chat.routing.websocket_urlpatterns 是我们后面会自己建立的模块。

    紧接着,我们需要在 Django 的配置文件中继续配置 Channels 的 asgi 应用和通道层的信息:

  • my_project/settings.py
    1. ASGI_APPLICATION = "my_project.routing.application" # 上面新建的 asgi 应用
    2. CHANNEL_LAYERS = {
    3. 'default': {
    4. # 这里用到了 channels_redis
    5. 'BACKEND': 'channels_redis.core.RedisChannelLayer',
    6. 'CONFIG': {
    7. 'hosts': [('127.0.0.1', 6379)], # 配置你自己的 redis 服务信息
    8. },
    9. }
    10. }

    到这里,Channels 的基本配置就差不多了。

    我们说以官方给的聊天室的例子来说明,所以最开始的时候,在 INSTALLED_APPS 中,我们添加了一个 chat的应用。下面就来创建这个应用。

    channels的应用和Django的还是不太一样,所以就不使用 python manage.py startapp命令创建应用了。我们手动创建几个文件。

  • chat/routing.py
    1. from django.urls import path
    2.  
    3. from . import consumers
    4.  
    5. websocket_urlpatterns = [ # 路由,指定 websocket 链接对应的 consumer
    6. path('ws/chat/<str:room_name>/', consumers.ChatConsumer),
    7. ]

    在 routing 文件中,我们定义了一个 websocket_urlpatterns 的路由列表,并设定了 ChatCunsumer 这个消费类(理解成Django的view)。这个 routing 文件就是上面 my_project/routing.py 中引用到的路由。

    下面来看看consumer的实现:

    1. import json
    2.  
    3. from asgiref.sync import async_to_sync
    4. from channels.generic.websocket import WebsocketConsumer
    5.  
    6. #customer相当于django中的view
    7. class ChatConsumer(WebsocketConsumer):
    8. def connect(self):
    9. # 当 websocket 一链接上以后触发该甘薯
    10. self.room_name = self.scope['url_route']['kwargs']['room_name']
    11. self.room_group_name = 'chat_%s' % self.room_name
    12.  
    13. # 把当前链接添加到聊天室
    14. # 注意 `group_add` 只支持异步调用,所以这里需要使用`async_to_sync`转换为同步调用
    15. async_to_sync(self.channel_layer.group_add)(
    16. self.room_group_name,
    17. self.channel_name
    18. )
    19.  
    20. # 接受该链接
    21. self.accept()
    22.  
    23. def disconnect(self, close_code):
    24. # 断开链接是触发该函数
    25. # 将该链接移出聊天室
    26. async_to_sync(self.channel_layer.group_discard)(
    27. self.room_group_name,
    28. self.channel_name
    29. )
    30.  
    31. def receive(self, text_data):
    32. # 前端发送来消息时,通过这个接口传递
    33. text_data_json = json.loads(text_data)
    34. message = text_data_json['message']
    35.  
    36. # 发送消息到当前聊天室
    37. async_to_sync(self.channel_layer.group_send)(
    38. self.room_group_name,
    39. {
    40. # 这里的type要在当前类中实现一个相应的函数,
    41. # 下划线或者'.'的都会被Channels转换为下划线处理,
    42. # 所以这里写 'chat.message'也没问题
    43. 'type': 'chat_message',
    44. 'message': message
    45. }
    46. )
    47.  
    48. # 从聊天室拿到消息,后直接将消息返回回去
    49. def chat_message(self, event):
    50. message = event['message']
    51.  
    52. # Send message to WebSocket
    53. self.send(text_data=json.dumps({
    54. 'message': message
    55. }))

以上就是以同步的简单的实现了聊天室的收发功能,我们来看看这个互动过程是怎样进行的。

请求链接

当前端发起一个 websocket 的请求过来的时候,会自动触发 connect()函数事件。

前端发起请求的实现方式:

  • chat/templates/chat/room.html

    1. <script>
    2. var roomName = {{ room_name_json }};
    3.  
    4. var chatSocket = new WebSocket(
    5. 'ws://' + window.location.host + '/ws/chat/' + roomName + '/');
    6. ...
    7. </script>

    触发 connect事件以后,进入函数,self.scope可以类比的理解为django中的self.request,从url中取出room_name字段备用,这里的变量名是在路由中设置的。

    chat/urls.py

    1. from django.urls import path
    2.  
    3. from . import views
    4.  
    5. urlpatterns = [
    6. path('', views.index, name='index'),
    7. path('<str:room_name>/', views.room, name='room'),
    8. ]

    self.channel_name 是每一个websocket请求过来时候, Channels 自动帮我们生成的一个个性化名称,实现代码就是用随机函数组装:

  • channels/layers.py : L243
    1. async def new_channel(self, prefix="specific."):
    2. """
    3. Returns a new channel name that can be used by something in our
    4. process as a specific channel.
    5. """
    6. return "%s.inmemory!%s" % (
    7. prefix,
    8. "".join(random.choice(string.ascii_letters) for i in range(12)),
    9. )

    然后我们将新的 channel_name 和 room_name 绑定,也相当于,将当前链接加入到指定聊天室。端口链接

端口链接

前端实现方式:

  • chat/templates/chat/room.html

    1. <script>
    2. ....
    3. chatSocket.onclose = function(e) {
    4. console.error('Chat socket closed unexpectedly');
    5. };
    6. ....
    7. </script>

    当 server 端的 WebSocket 调用 self.close() 关闭时,前端的 chatSocket.onclose 会自动触发。如果前端页面关闭,导致端口链接时,后端的disconnect()函数会自动触发。

接收信息

前端实现方式:

  • chat/templates/chat/room.html

    1. <script>
    2. ....
    3. // 点击按键,发送信息是很常见的逻辑
    4. document.querySelector('#chat-message-submit').onclick = function(e) {
    5. var messageInputDom = document.querySelector('#chat-message-input');
    6. var message = messageInputDom.value;
    7. chatSocket.send(JSON.stringify({
    8. 'message': message
    9. }));
    10.  
    11. messageInputDom.value = '';
    12. };
    13. </script>

    chatSocket.send 会将信息发送到服务端,服务端的receive() 事件将会自动触发,将收到的 message 送到对应的 group 中,

    type 字段是一个可以被调用的对象名,Channels 会根据这个名称调度相应的方法来处理逻辑。

    当在相应的函数中处理结束以后,调用send()方法将消息传递回前端。

    前端接收信息的方式:

    1. <script>
    2. ....
    3. chatSocket.onmessage = function(e) {
    4. var data = JSON.parse(e.data);
    5. var message = data['message'];
    6. document.querySelector('#chat-log').value += (message + '\n');
    7. };
    8. ....
    9. </script>

    chatSocket.onmessage 会接收到后端发来的信息,前端再将该信息渲染到页面上。

异步写法

上面是同步方式的写法,官方还给出了异步的写法,在我们的项目中,也采用了异步的写法,Channels 对于异步的支持是非常好的。

  1. import json
  2. from channels.generic.websocket import AsyncWebsocketConsumer
  3.  
  4. class ChatConsumer(AsyncWebsocketConsumer):
  5. async def connect(self):
  6. self.room_name = self.scope['url_route']['kwargs']['room_name']
  7. self.room_group_name = 'chat_%s' % self.room_name
  8.  
  9. await self.channel_layer.group_add(
  10. self.room_group_name,
  11. self.channel_name
  12. )
  13.  
  14. await self.accept()
  15.  
  16. async def disconnect(self, close_code):
  17. await self.channel_layer.group_discard(
  18. self.room_group_name,
  19. self.channel_name
  20. )
  21.  
  22. async def receive(self, text_data):
  23. text_data_json = json.loads(text_data)
  24. message = text_data_json['message']
  25.  
  26. await self.channel_layer.group_send(
  27. self.room_group_name,
  28. {
  29. 'type': 'chat_message',
  30. 'message': message
  31. }
  32. )
  33.  
  34. async def chat_message(self, event):
  35. message = event['message']
  36.  
  37. await self.send(text_data=json.dumps({
  38. 'message': message
  39. }))

从任意地方推送消息

仅仅只了解单次的收发,可能在项目中不能满足真正的需求。假如我们在项目中,其他的地方也需要向前端推送消息,怎么办?

这里就体现出了使用 Channel Layers 的好处。Channel Layers 因为是使用 redis 作为消息通道层,这使得一个应用中两个实例之间可以实现通信。如果你不想通过数据库来传递所有消息或事件,那么 Channel Layers 是开发分布式实时应用程序的非常好的实践。

有了这个,我们就可以项目的任何地方(脱离了 consumer 以外),给通道内广播信息。我们从上面可以知道,要想调用 group_send 需要有 self.channel_layer的实例。但在脱离了 consumer 的情况下, Channels 提供了 get_channel_layer 函数接口来获取它。

  1. from channels.layers import get_channel_layer
  2. channel_layer = get_channel_layer()

然后,一旦你获取到了它,你就可以调用它上面的方法。但请记住,channel layers仅支持异步方法,因此你可以从自己的异步上下文中调用它:

  1. for chat_name in chats:
  2. await channel_layer.group_send(
  3. chat_name,
  4. {"type": "chat.system_message", "text": announcement_text},
  5. )

或者,如果你的上下文是同步环境,可以使用 async_to_sync 来转换调用:

  1. from asgiref.sync import async_to_sync
  2.  
  3. async_to_sync(channel_layer.group_send)("chat", {"type": "chat.force_disconnect"}

channels 2.x的使用的更多相关文章

  1. Django Channels 学习笔记

    一.为什么要使用Channels 在Django中,默认使用的是HTTP通信,不过这种通信方式有个很大的缺陷,就是不能很好的支持实时通信.如果硬是要使用HTTP做实时通信的话只能在客户端进行轮询了,不 ...

  2. 【Go入门教程7】并发(goroutine,channels,Buffered Channels,Range和Close,Select,超时,runtime goroutine)

    有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持了并行. goroutine goroutine是Go并行设计的核心.goro ...

  3. JAVA NIO中的Channels和Buffers

    前言 Channels和Buffers是JAVA NIO里面比较重要的两个概念,NIO正是基于Channels和Buffers进行数据操作,且数据总是从Channels读取到Buffers,或者从Bu ...

  4. Device Channels in SharePoint 2013

    [FROM:http://blog.mastykarz.nl/device-channels-sharepoint-2013/] One of the new features of SharePoi ...

  5. Dynamic Virtual Channels

    refer http://blogs.msdn.com/b/rds/archive/2007/09/20/dynamic-virtual-channels.aspx An important goal ...

  6. iOS上传应用过程中出现的错误"images contain alpha channels or transparencies"以及解决方案

    如何取消图片透明度  本文永久地址为 http://www.cnblogs.com/ChenYilong/p/3989954.html,转载请注明出处. 当你试图通过<预览>进行" ...

  7. A Tour of Go Buffered Channels

    Channels can be buffered. Provide the buffer length as the second argument to make to initialize a b ...

  8. A Tour of Go Channels

    Channels are a typed conduit through which you can send and receive values with the channel operator ...

  9. lr11 BUG?Failed to send data by channels - post message failed.

    问题描述   : http协议,场景运行一会之后,报错! erro信息: Failed to send data by channels - post message failed. 解决方法 :ht ...

  10. 实时 Django 终于来了 —— Django Channels 入门指南

    Reference: http://www.oschina.net/translate/in_deep_with_django_channels_the_future_of_real_time_app ...

随机推荐

  1. hibernate连接数据库和使用

    hibernate.cfg.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibe ...

  2. Linux删除奇怪名字文件

    use ls -ilrt get filenum and use find ./ -inum filenum -exec rm '{}' \; del it

  3. 大兄dei,早点看清this吧

    说道this,可以说是前端中很重要的问题之一了,也是面试或者笔试常考的问题.所以还是早点看清this吧,大兄dei. this是什么?为什么要存在? this关键字是js中最最复杂的机制之一.他被自动 ...

  4. SQL 一列数据整合为一条数据

    SQL 一列数据整合为一条数据: SELECT  STUFF(( SELECT distinct  ',' + 列名 FROM 表名 where  [条件] FOR XML PATH('') ), 1 ...

  5. WinForm中 Asp.Net Signalr消息推送测试实例

    p{ text-align:center; } blockquote > p > span{ text-align:center; font-size: 18px; color: #ff0 ...

  6. Installing ROS Indigo on the Raspberry Pi

    Installing ROS Indigo on the Raspberry Pi Description: This instruction covers the installation of R ...

  7. Log4j2 HelloWorld

    Log4j2 使用教程 Log4j2 的好处就不在这里一一列举了,如果你搜了2,说明你对它一定有兴趣,并且想了解它,使用它. 一.下载log4j2 ,基本上你只需要导入下面两个jar包即可: log4 ...

  8. SQL Server 复制表结构以及数据,去除表中重复字段

    --复制另一个数据库中的某张表的结构及数据--select * from Test.dbo.TestTable(查询表中所有数据) --into [表名] 插入当前数据库新表,如果没有该表就创建 se ...

  9. CCF CSP 201712-1 最小差值

    题目链接:http://118.190.20.162/view.page?gpid=T68 问题描述 试题编号: 201712-1 试题名称: 最小差值 时间限制: 1.0s 内存限制: 256.0M ...

  10. CSS——img自适应div大小

    代码如下: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <tit ...