"Асинхронные" запросы в Django стандартными средствами

Разрабатывая небольшой сайт с использованием фреймворка Django, я столкнулся со следующей задачей: необходимо было отправить со страницы POST-запрос содержащий определённые параметры, после получения которого Django должен запустить довольно тяжёлый процесс и не дожидаясь результатов его выполнения, вернуть ответ о том что процесс успешно запущен. 

Для начала вспомним что такое синхронный и асинхронный запрос:
Синхронный запрос - запрос с ожиданием ответа от сервера.
Асинхронный запрос - запрос без ожидания ответа от сервера.
Вроде всё просто, но есть один подводный камень: так как Django блокирующий фреймворк, то формально в нём исключена возможность выполнения асинхронных запросов, то есть на каждый запрос мы должны вернуть ответ, даже если этот запрос отправлен с помощью JQuery и AJAX. Но мы можем пойти на хитрость :) Нам ни что не мешает игнорировать ответы при условии что мы его дождёмся. Тогда задача сводится к тому как не "повесить" страницу нашим запросом, если мы хотим запустить тяжёлый фоновый процесс. Да, по факту это не асинхрон, но зато данный подход позволит решить ряд мелких задач и при этом нам не нужно дополнительно разворачивать сервер для обработки асинхронных запросов с нашей страницы.

В данном случае у нас должно получиться синхронно-асинхронное приложение, так как с одной стороны мы ждём ответ об успешно выполненном действии, но с другой стороны наше действие заключается только в том чтобы что-то запустить и со спокойной душой пойти заниматься своими делами, не дожидаясь результатов работы.

В примере я использую Python 3.3.4 и Django 1.7, операционная система Linux Kubuntu, среда разработки - PyCharm.

Перейдём к практике: создайте Django - проект и добавьте в него новое приложение. Я назвал это приложение "async", вы можете выбрать любое другое название для него. Создайте сразу в проекте папку templates для хранения html-шаблонов. Вот как примерно должна выглядеть структура проекта:


Этот проект должен прекрасно запускаться командой: python3 manage.py runserver

Теперь в папке templates создайте файл index.html:

 <!DOCTYPE html>  
 <html>  
 <head lang="en">  
   <meta charset="UTF-8">  
   <title>Пример "асинхронного" запроса</title>  
   <script src="http://code.jquery.com/jquery-2.0.3.min.js"></script>  
   <script>  
     function doIt() {  
       $.ajax({  
         type: "POST",  
         url: "/",  
         data: {  
           csrfmiddlewaretoken: document.getElementsByName('csrfmiddlewaretoken')[0].value,  
           count: $("#input_count").val()  
         },  
         success: function(data) {  
           alert("Процесс запущен!");  
         },  
         error: function(xhr, textStatus, errorThrown) {  
           alert("Error: "+errorThrown+xhr.status+xhr.responseText);  
         }  
       });  
     }  
   </script>  
 </head>  
 <body>  
   {% csrf_token %}  
   <input id="input_count" type="number" value="1" min="1" max="10000"></p>  
   <input id="button_do_it" type="button" value="Отправить" onclick="doIt()">  
 </body>  
 </html>  

Так как у нас тестовый проект, то я решил обойтись без шаблонов, организации статики и прочих предварительных ласк. Всё что нам нужно поместилось на одной странице, а именно: у нас есть поле (input_count), в которое мы можем ввести число от 1 до 10000 и кнопка (button_do_it), по нажатию которой вызывается функция doIt(). В этой функции, используя библиотеку JQuery мы отправляем POST-запрос с двумя параметрами:

  1. csrfmiddlewaretoken - значение данного параметра является хэшем для идентификатора сессии плюс секретный ключ. Этот параметр нужен для django.contrib.csrf, который защищает от атак типа «подделка HTTP запросов» (Cross-Site Request Forgery).
  2. count - число из поля input_count
Теперь создадим наш "тяжёлый" фоновый процесс. Добавьте в пакет async файл processor.py:

 __author__ = 'Alexey Kutepov'  
 from time import sleep  
 class Processor():  
   def process(self, count=1):  
     for i in range(count):  
       sleep(1) #спим одну секунду  
       print(i)  

Код примитивный: при каждой итерации спим одну секунду и потом выводим текущее значение счётчика в консоль.

В файле views.py приложения async создайте обработчик для наших запросов:

 import threading  
 from django.shortcuts import render_to_response  
 from django.template import RequestContext  
 from django.views.decorators.csrf import csrf_protect  
 from async.processor import Processor  
 @csrf_protect  
 def request_handler(request):  
   if request.is_ajax() and request.method == 'POST':  
     if "count" in request.POST and request.POST["count"]:  
       count = int(request.POST["count"])  
     else:  
       count = 1  
     processor = Processor()  
     thread = threading.Thread(target=processor.process, args=(count,))  
     thread.start()  
     return render_to_response(  
       "index.html",  
       {},  
       context_instance=RequestContext(request),  
     )  
   else:  
     return render_to_response(  
       "index.html",  
       {},  
       context_instance=RequestContext(request),  
     )  

Этот код разберём подробнее. Для начала мы проверяем что сообщение отправлено методом POST через AJAX. Если это так, то в сообщении ищем параметр "count" и если находим, то сохраняем в переменную count. Затем инициализируем класс Processor и присваиваем ссылку на него переменной processor, который мы создали ранее. Всё готово для запуска нашего метода process() из класса Processor, но если мы просто напишем далее processor.process(count), то наш процесс благополучно "повесит" нашу страницу и мы будем вынуждены ждать его завершения. Чтобы этого не произошло, мы вызываем метод process() в отдельном потоке.

Остался последний штрих для того, чтобы мы могли протестировать наш проект. Добавьте в urls.py наш обработчик:

 from django.conf.urls import patterns, include, url  
 from async.views import request_handler  
 from django.contrib import admin  
 admin.autodiscover()  
 urlpatterns = patterns('',  
   url(r'^admin/', include(admin.site.urls)),  
   #Наша страница  
   url(r'^$', request_handler),  
 )  

Всё готово. Вот такую структуру должен иметь наш проект:



Запускаем проект:


Открываем нашу страницу, в поле вводим любое число и жмём кнопку "Отправить". Тут же получаем ответ:


Возвращаемся в нашу консоль и видим как во всю работает запущенный фоновый процесс. Каждую секунду в консоль печатается значение счётчика из цикла:


Исходники проекта можно найти тут: https://github.com/AlexeyKutepov/django_example




Комментариев нет:

Отправить комментарий

Яндекс.Метрика