пятница, 6 июня 2008 г.

Жизненный цикл запроса Zope3

При разработке приложений для Zope 3 разработчик часто взаимодействует с объектом запроса (request). Создавая представления, ему не надо задумываться о деталях того, откуда взялся запрос в представлении и что происходит с создаваемым в нем ответом (response). Все это хорошо, т.к. в большинстве случаев понимания внутреннего устройства не требуется. Но иногда нам может понадобиться создать собственный сервер или изменить поведение механизма публикации. В данном случае очень полезно знать общую архитектуру серверов и публикаторов Zope. Далее мы рассмотрим жизненный цикл запроса, а для примера возьмем запрос браузера (HTTP Request).

В данной статье подразумевается использование Zope 3.4. За основу взят материал главы 40 из книги The Zope 3 Developers Book - An Introduction for Python Programmers. При использовании материалов данного документа как полностью, так и частично, ссылка на оригинал обязательна.
Что же такое запрос?

Технически, в конкретной ситуации мы обычно ссылаемся на объекты реализующие интерфейс IRequest. Эти объекты отвечают за внутренние устройство, специфичное протоколу и представляют семантику протокола для использования презентационными компонентами.

В общем смысле все, что посылает клиент серверу после установления соединения и есть
запрос. На более низком уровне клиентское приложение (браузер) посылает HTTP строку
примерно такого вида:

GET /index.html HTTP/1.1

в общем, это и есть запрос.



Где начинается запрос?
Теперь мы понимаем, что же такое запрос, углубимся в детали и посмотрим, как обрабатываются соединения и рождается запрос.

При запуске, сервер привязывается к сокету на локальном адресе машины и начинает прослушивание входящих соединений. При обнаружении входящего соединения, вызывается метод accept(), который возвращает объект соединения и адрес компьютера, с которым оно установлено. Все выше описанное является частью стандартной библиотеки сокетов Python и документировано в интерфейсе zope.server.interfaces.ISocket (файл: .../zope/server/interfaces/__init__.py).

Сервер, который по большей части является реализацией IDispatcher, имеет простой интерфейс для обработки событий вызовом соответствующих методов handle_<event>(). Полный список всех событий, обрабатываемых таким образом, приведен в интерфейсе IDispatcherEventHandler (.../zope/server/interfaces/__init__.py).

Таким образом, когда поступает входящее соединение вызывается метод handle_accept(), который переопределен в классе zope.server.serverbase.ServerBase. Этот метод пытается получить соединение вызывая метод accept(). При успешном создании соединения, оно используется для создания канала ServerChannel, являющегося следующим уровнем абстракции. Большая часть остальной функциональности диспетчера предоставляется async.dispatcher, который полностью реализует интерфейс IDispatcher.

def handle_accept(self):
        """See zope.server.interfaces.IDispatcherEventHandler"""
        try:
            v = self.accept()
            if v is None:
                return
            conn, addr = v
        except socket.error:
            if self.adj.log_socket_errors:
                self.log_info ('warning: server accept() threw an exception',
                               'warning')
            return
        for (level, optname, value) in self.adj.socket_options:
            conn.setsockopt(level, optname, value)
        self.channel_class(self, conn, addr, self.adj)
 
На этой стадии канал принимает управление на себя, ведь это всего лишь еще один диспетчер. Таким образом канал запускается для того, чтобы собрать поступающие данные (см. received(data) в .../zope/server/serverchannelbase.py) и немедленно передает их анализатору запросов, экземпляру класса, заданного в переменной parser_class. Естественно, этот класс будет разным в других реализациях сервера (например FTP). Как именно функционирует анализатор не так важно, нам надо знать только что он реализует IStreamConsumer, посредством которого он сообщает о завершении разбора запроса (атрибут completed). В канале может существовать только один запрос, таким образом если канал занят, поступающие запросы помещаются в очередь до тех пор, пока выполняемая задача не будет завершена.
def received(self, data):
        """See async.dispatcher

        Receives input asynchronously and send requests to
        handle_request().
        """
        preq = self.proto_request
        while data:
            if preq is None:
                preq = self.parser_class(self.adj)
            n = preq.received(data)
            if preq.completed:
                # The request is ready to use.
                self.proto_request = None
                if not preq.empty:
                    self.handle_request(preq)
                preq = None
            else:
                self.proto_request = preq
            if n >= len(data):
                break
            data = data[n:]

    def handle_request(self, req):
        """Creates and queues a task for processing a request.

        Subclasses may override this method to handle some requests
        immediately in the main async thread.
        """
        task = self.task_class(self, req)
        self.queue_task(task)

    def queue_task(self, task):
        """Queue a channel-related task to be executed in another thread."""
        start = False
        task_lock.acquire()
        try:
            if self.tasks is None:
                self.tasks = []
            self.tasks.append(task)
            if not self.running_tasks:
                self.running_tasks = True
                start = True
        finally:
            task_lock.release()
        if start:
            self.set_sync()
            self.server.addTask(self)
Всякий раз, после освобождения канала (см. handle_request(self, req) ) запрос приводится к интерфейсу ITask и помещается в очередь (см. queue_task(task) ). Если в данный момент есть другие задачи канала в процессе обработки сервером, то задача помещается в очередь, иначе канал с накопленными задачами немедленно передается серверу на выполнение используя метод IServer.addTask(task) (да, канал тоже реализует ITask). В этом методе задача добавляется в диспетчер задач (см. ITaskDispatcher), который планирует ее выполнение в отдельном потоке. Но почему все это делается через задачи и диспетчер задач? Ранее весь код выполнялся в одном потоке. но для улучшения масштабирования и поддержки долго живущих запросов без блокирования всего сервера, механизму обработки запроса необходимо иметь возможность запуска нескольких параллельных потоков. Итак, реализация ITaskDispatcher отвечает за распределение запросов. Теоретически он даже может задействовать другие компьютеры для выполнения задач. Тем не менее по умолчанию используется zope.server.taskthreads.ThreadedTaskDispatcher. Используя его метод setThreadCount(count), механизм запуска Zope может задавать максимальное количество одновременно работающих потоков.

Как только задачи поступают на обработку, диспетчер вызывает метод ITask.service() который должен выполнить запрос. В частности, когда обрабатывается HTTPTask, вызывается метод executeRequest(task) сервера HTTPServer. Публикатор zope.server.http.publisherhttpserver.PublisherHTTPServer, это единственное, что непосредственно относится к Zope 3, он создает объект IHTTPRequest из задачи и публикует запрос через zope.publisher.publish(request). Сервер имеет атрибут request_factory в котором содержится используемый для создания запросов класс.

Так к чему же пришла система? Мы взяли входящее соединение, прочитали все входящие данные и провели их разбор, запланировали выполнение и на основе этого создали запрос, который был опубликован публикатором Zope 3. Все кроме последнего шага не содержит никакого специфичного для Zope 3 кода, а это значит, что все это может быть заменено на другой Web-сервер, например Twisted.

На этом заканчивается процесс создания запроса. В следующей статье мы рассмотрим его публикацию.

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

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