|
| 1 | +原文:[Django Channels and Celery Example](http://vincenttide.com/blog/1/django-channels-and-celery-example/) |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +在本教程中,我将探讨如何建立一个Django Channels项目来与Celery协同工作,以及在任务开始和结束的时候,即时通知。Django Channels使用WebSockets来启用服务器和浏览器客户端之间的双向通信。假设读者已经熟悉如何建立一个普通的Django项目,我们将只覆盖Channels和Celery相关的部分。 |
| 6 | + |
| 7 | +你可以在[这里找到Github repo](https://github.com/VincentTide/django-channels-celery-example)以及位于<http://tasker.vincenttide.com>的一个相同部署。注意,该部署包含了在此教程不包括的一些额外的内容,例如取消功能。示例部署的前端还运行React库,而我们在这个演示将只使用JavaScript。 |
| 8 | + |
| 9 | +首先,让我们安装一些需要的依赖。我们将需要一个Channels层后端,Channels用来传递和存储数据。我们还将需要一个Celery broker传输后端。事实证明,我们可以为这些任务同时使用Redis,因此Redis就是我们所用的。 |
| 10 | + |
| 11 | +```python |
| 12 | + |
| 13 | + # Add Chris Lea’s redis ppa - he maintains the ppa for many open source projects |
| 14 | + $ sudo add-apt-repository ppa:chris-lea/redis-server |
| 15 | + $ sudo apt-get update |
| 16 | + $ sudo apt-get install redis-server |
| 17 | + |
| 18 | + # Now check that redis-server is up and running |
| 19 | + $ redis-cli ping |
| 20 | + # PONG |
| 21 | + |
| 22 | +``` |
| 23 | + |
| 24 | +在一个virtualenv内设置一个新的Django项目,然后安装以下库: |
| 25 | + |
| 26 | +```python |
| 27 | + |
| 28 | + $ pip install django |
| 29 | + $ pip install channels # the channels library |
| 30 | + $ pip install asgi_redis # the redis channel layer we are using |
| 31 | + $ pip install celery # Celery task queue |
| 32 | + |
| 33 | +``` |
| 34 | + |
| 35 | +让我们先看看settings.py文件。 |
| 36 | + |
| 37 | +```python |
| 38 | + |
| 39 | + # Add our new app to installed apps |
| 40 | + INSTALLED_APPS = [ |
| 41 | + #… |
| 42 | + ‘jobs’, |
| 43 | + ] |
| 44 | + |
| 45 | + # Channels settings |
| 46 | + CHANNEL_LAYERS = { |
| 47 | + "default": { |
| 48 | + "BACKEND": "asgi_redis.RedisChannelLayer", # use redis backend |
| 49 | + "CONFIG": { |
| 50 | + "hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')], # set redis address |
| 51 | + }, |
| 52 | + "ROUTING": "django_channels_celery_tutorial.routing.channel_routing", # load routing from our routing.py file |
| 53 | + }, |
| 54 | + } |
| 55 | + |
| 56 | + # Celery settings |
| 57 | + BROKER_URL = 'redis://localhost:6379/0' # our redis address |
| 58 | + # use json format for everything |
| 59 | + CELERY_ACCEPT_CONTENT = ['json'] |
| 60 | + CELERY_TASK_SERIALIZER = 'json' |
| 61 | + CELERY_RESULT_SERIALIZER = 'json' |
| 62 | + |
| 63 | +``` |
| 64 | + |
| 65 | +首先,添加我们的新app到INSTALLED_APPS列表中。Channels设置只是告诉Channels我们所使用的后端,在这里,是Redis。ROUTING选项告诉Channels到哪里找我们的WebSockets路径,这里是routing.py文件。Celery设置告诉Celery到哪里找我们的broker,以及对所有东西,我们想要使用json格式。 |
| 66 | + |
| 67 | +现在,看看routing.py文件: |
| 68 | + |
| 69 | +```python |
| 70 | + |
| 71 | + from channels import route |
| 72 | + from jobs import consumers |
| 73 | + |
| 74 | + channel_routing = [ |
| 75 | + # Wire up websocket channels to our consumers: |
| 76 | + route("websocket.connect", consumers.ws_connect), |
| 77 | + route("websocket.receive", consumers.ws_receive), |
| 78 | + ] |
| 79 | + |
| 80 | +``` |
| 81 | + |
| 82 | +这里,我们只是为连接和接收消息设置了处理函数。我们还可以添加一个函数来处理断开连接消息,但对我们来说,并不需要。我们告诉Channels在我们的jobs/consumers.py文件中查找函数。 |
| 83 | + |
| 84 | +这里是consumers.py文件的主要部分: |
| 85 | + |
| 86 | +```python |
| 87 | + |
| 88 | + @channel_session |
| 89 | + def ws_connect(message): |
| 90 | + message.reply_channel.send({ |
| 91 | + "text": json.dumps({ |
| 92 | + "action": "reply_channel", |
| 93 | + "reply_channel": message.reply_channel.name, |
| 94 | + }) |
| 95 | + }) |
| 96 | + |
| 97 | + |
| 98 | + @channel_session |
| 99 | + def ws_receive(message): |
| 100 | + try: |
| 101 | + data = json.loads(message['text']) |
| 102 | + except ValueError: |
| 103 | + log.debug("ws message isn't json text=%s", message['text']) |
| 104 | + return |
| 105 | + |
| 106 | + if data: |
| 107 | + reply_channel = message.reply_channel.name |
| 108 | + |
| 109 | + if data['action'] == "start_sec3": |
| 110 | + start_sec3(data, reply_channel) |
| 111 | + |
| 112 | +``` |
| 113 | + |
| 114 | +在我们的ws_connect函数中,我们将只是把客户端的回复通道(reply channel)地址回传。回复通道是一个分配给每一个连接到我们的websockets服务器的浏览器客户端的唯一地址。可以从message.reply_channel.name中检索到该值,并且可以保存或传递该值到另一个函数,例如Celery 的task,这样的话,就可以将消息发回去。事实上,这就是我们将要做的事。message.reply_channel.send是Channels为我们提供的,用来回复到同一个客户端的一个方便的快捷方式。如果你仅有reply_channel名,那么你将必须使用以下方法来发送消息: |
| 115 | + |
| 116 | +```python |
| 117 | + |
| 118 | + Channel(reply_channel_name).send({ |
| 119 | + "text": json.dumps({ |
| 120 | + "action": "started", |
| 121 | + "job_id": job.id, |
| 122 | + "job_name": job.name, |
| 123 | + "job_status": job.status, |
| 124 | + }) |
| 125 | + }) |
| 126 | + |
| 127 | +``` |
| 128 | + |
| 129 | +在我们的ws_receive函数中,我们根据action参数来看看客户端想要我们做什么。如果你想要做不同的事,那么可以有多个action指令。在我们的例子中,我们只有一个指令,它运行一个名为start_sec3的函数。start_sec3只是休眠3秒,然后回复它已经完成的消息给客户端。注意,我们传递了reply_channel地址,因此它知道发送响应到哪。 |
| 130 | + |
| 131 | +最后一个重要的块是客户端侧的javascript处理函数。 |
| 132 | + |
| 133 | +```python |
| 134 | + |
| 135 | + $(function() { |
| 136 | + // When we're using HTTPS, use WSS too. |
| 137 | + var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws"; |
| 138 | + var ws_path = ws_scheme + '://' + window.location.host + '/dashboard/'; |
| 139 | + console.log("Connecting to " + ws_path) |
| 140 | + var socket = new ReconnectingWebSocket(ws_path); |
| 141 | + |
| 142 | + socket.onmessage = function(message) { |
| 143 | + console.log("Got message: " + message.data); |
| 144 | + var data = JSON.parse(message.data); |
| 145 | + |
| 146 | + // if action is started, add new item to table |
| 147 | + if (data.action == "started") { |
| 148 | + var task_status = $("#task_status"); |
| 149 | + var ele = $('<tr></tr>'); |
| 150 | + ele.attr("id", data.job_id); |
| 151 | + var item_id = $("<td></td>").text(data.job_id); |
| 152 | + ele.append(item_id); |
| 153 | + var item_name = $("<td></td>").text(data.job_name); |
| 154 | + ele.append(item_name); |
| 155 | + var item_status = $("<td></td>"); |
| 156 | + item_status.attr("id", "item-status-"+data.job_id); |
| 157 | + var span = $('<span class="label label-primary"></span>').text(data.job_status); |
| 158 | + item_status.append(span); |
| 159 | + ele.append(item_status); |
| 160 | + task_status.append(ele); |
| 161 | + } |
| 162 | + // if action is completed, just update the status |
| 163 | + else if (data.action == "completed"){ |
| 164 | + var item = $('#item-status-' + data.job_id + ' span'); |
| 165 | + item.attr("class", "label label-success"); |
| 166 | + item.text(data.job_status); |
| 167 | + } |
| 168 | + }; |
| 169 | + |
| 170 | + $("#taskform").on("submit", function(event) { |
| 171 | + var message = { |
| 172 | + action: "start_sec3", |
| 173 | + job_name: $('#task_name').val() |
| 174 | + }; |
| 175 | + socket.send(JSON.stringify(message)); |
| 176 | + $("#task_name").val('').focus(); |
| 177 | + return false; |
| 178 | + }); |
| 179 | + }); |
| 180 | + |
| 181 | +``` |
| 182 | + |
| 183 | +这里,我们首先创建websockets对象,然后用socket.onmessage函数来处理为每个websockets消息我们应该做的事。如果action参数的值是“started”,那么我们将添加一个新的条目到表格中。如果action是completed,我们只会修改对应的列状态为已完成。 |
| 184 | + |
| 185 | +而表单则是发送一个websockets消息到服务器,告诉它运行action “start_sec3”。 |
| 186 | + |
| 187 | + |
| 188 | +要看完整的项目文件,请访问[Github repo](https://github.com/VincentTide/django-channels-celery-example)。要运行Github repo代码,先确保你安装了Redis,然后运行以下命令: |
| 189 | + |
| 190 | +```python |
| 191 | + |
| 192 | + pip install -r requirements.txt |
| 193 | + python manage.py makemigrations |
| 194 | + python manage.py migrate |
| 195 | + python manage.py runserver # Start daphne and workers |
| 196 | + celery worker -A example -l info # Start celery workers |
| 197 | + |
| 198 | +``` |
| 199 | + |
| 200 | +这会启动部署服务器,地址为`http://localhost:8000`。再一次说明,你可以在http://tasker.vincenttide.com上找到一个类似的部署。 |
| 201 | + |
| 202 | + |
0 commit comments