Вячеслав Тихановский

НАИМЕНОВАНИЕ

PSGI - Спецификация Веб-серверного Шлюзового Интерфейса для Perl

ОБЗОР

Этот документ определяет стандартный интерфейс между веб-серверами и веб-приложениями или фреймворками на Perl. Этот интерфейс призван содействовать переносимости веб-приложений и сокращению дублирования усилий разработчиков веб-фреймворков.

Пожалуйста, имейте в виду, что PSGI - не просто "Ещё Один Веб-фреймворк". PSGI - это спецификация, предназначенная для отделения среды веб-сервера от кода веб-фреймворка. Также, PSGI не является программным интерфейсом (API) для веб-приложений. Разработчики веб-приложений (конечные пользователи) не будут запускать свои веб-приложения, используя PSGI напрямую - вместо этого предполагается использование фреймворков, поддерживающих PSGI.

ТЕРМИНОЛОГИЯ

Веб-серверы

Веб-серверы принимают HTTP запросы от веб-клиентов, передают эти запросы веб-приложениям (если сконфигурированы делать это) и возвращают HTTP ответы инициировавшим запрос клиентам.

PSGI сервер

PSGI сервер - это программа на Perl, предоставляющая среду для запуска в ней PSGI приложения.

PSGI определяет интерфейс для веб-приложений и основную задачу веб-приложений, которая заключается в том, чтобы обслуживать запросы через Интернет. PSGI сервер, скорее всего, будет либо: частью веб-сервера (как Apache mod_perl), соединён с веб-сервером (при помощи FastCGI, SCGI), вызываться веб-сервером (как в старом добром CGI), или же сам по себе будет автономным веб-сервером, написанным целиком или частично на Perl.

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

PSGI Сервер также часто называется PSGI Application Container, так как он похож на Java Servlet container, который представляет собой Java процесс, предоставляющий среду для сервлетов Java.

Приложения

Веб-приложения принимают HTTP запросы и возвращают HTTP ответы.

PSGI приложения - веб-приложения, соответствующие интерфейсу PSGI. Они должны принимать форму code reference с определёнными входными и выходными данными.

Для простоты PSGI приложения также будут называться Приложениями до конца этого документа.

Middleware (связующее ПО)

Middleware (Связующее ПО) - приложение PSGI (code reference), а также Сервер. Middleware выглядит как приложение, когда вызывается со стороны сервера, и, в свою очередь, может вызывать другие приложения. Можно думать о нём как о плагине для расширения приложения PSGI.

Разработчики фреймворков

Разработчики фреймворков - это авторы фреймворков для веб-приложений. Они пишут адаптеры (или движки), которые принимают входные данные PSGI, запускают веб-приложение и возвращают ответ PSGI серверу.

Разработчики веб-приложений

Разработчики веб-приложений - это разработчики, которые пишут код на основе веб-фреймворков. Эти разработчики никогда не должны иметь дело с PSGI напрямую.

СПЕЦИФИКАЦИЯ

Приложение

Приложение PSGI - это Perl code reference. Оно принимает ровно один аргумент, окружение, и возвращает ссылку на массив, содержащий ровно три значения.

  my $app = sub {
      my $env = shift;
      return [
          '200',
          [ 'Content-Type' => 'text/plain' ],
          [ "Hello World" ], # or IO::Handle-like object
      ];
  };

Окружение

Окружение ДОЛЖНО быть ссылкой на хеш, включающей в себя CGI-подобные заголовки, как описано ниже. Приложение может свободно изменять окружение. Окружение ДОЛЖНО содержать эти переменные (заимствовано из PEP 333, Rack и JSGI), за исключением тех случаев, когда они, как правило, пусты.

Если переменная окружения является логической переменной, её значение ОБЯЗАНО соответствовать нотации Perl для булевых чисел. Это означает, что пустая строка или явно указанный 0 являются правильными false значениями. Если булевый ключ отсутствует, приложение МОЖЕТ интерпретировать его как false значение.

Значения всех CGI переменных (в имени которых нет точки) ДОЛЖНЫ быть скалярными строками.

Подробнее см. ниже.

  • REQUEST_METHOD: Метод HTTP запроса, такой как "GET" или "POST". Это НЕ ДОЛЖНА быть пустая строка, так что метод всегда должен присутствовать.

  • SCRIPT_NAME: Начальная часть пути URL запроса, соответствующего приложению. Говорит приложению о своем виртуальном "местоположении". Может быть пустой строкой, если приложение соответствует корневому URI сервера.

    Если этот ключ не пуст, он ДОЛЖЕН начинаться с прямого слэша (/).

  • PATH_INFO: Остаточная часть пути URL запроса, определяющая виртуальное "расположение" цели запроса внутри приложения. Может быть пустой строкой, если запрашиваемый URL указывает на корень приложения и не имеет завершающего слэша. Это значение должно быть URI-декодировано серверами для того, чтобы быть совместимым с RFC 3875.

    Если эта переменная не пуста, она ДОЛЖНА начинаться с прямого слэша (/).

  • REQUEST_URI: Не декодированная, необработанная строка URL. Это необработанный путь URI и часть запроса, которые фигурируют в HTTP GET /... HTTP/1.x строке и не содержат схемы URI и имён хоста.

    В отличие от PATH_INFO, это значение НЕ ДОЛЖНО декодироваться сервером. Это ответственность приложения - правильно декодировать пути для того, чтобы отображать URL-ы на обработчики приложения, если оно предпочитает использовать эту переменную окружения вместо PATH_INFO.

  • QUERY_STRING: Порция запрашиваемого URL, которая следует за ?, если таковая имеется. Эта переменная МОЖЕТ быть пустой, но ДОЛЖНА всегда присутствовать, даже если она пуста.

  • SERVER_NAME, SERVER_PORT: При комбинации со SCRIPT_NAME и PATH_INFO, эти переменные окружения могут быть использованы для завершения URL. Обратите внимание, что, тем не менее, для реконструкции запрашиваемого URL предпочтительнее использовать HTTP_HOST (если он присутствует), чем SERVER_NAME. SERVER_NAME и SERVER_PORT НЕ ДОЛЖНЫ быть пустыми строками, и всегда должны присутствовать.

  • SERVER_PROTOCOL: Версия протокола, используемого клиентом для отправки запроса. Как правило, имеет вид "HTTP/1.0" или "HTTP/1.1" и может использоваться приложением для определения того, как трактовать заголовки запроса HTTP.

  • CONTENT_LENGTH: Длина содержимого в байтах, в виде целого числа. Присутствие или отсутствие этой переменной окружения должно соответствовать присутствию или отсутствию HTTP заголовка Content-Length в запросе.

  • CONTENT_TYPE: MIME-тип запроса, в том виде, в каком он указан клиентом. Присутствие или отсутствие этой переменной окружения должно соответствовать присутствию или отсутствию HTTP заголовка Content-Type в запросе.

  • Переменные HTTP_* : Эти переменные окружения соответствуют предоставленным клиентом заголовкам HTTP запроса. Присутствие или отсутствие этих ключей должно соответствовать присутствию или отсутствию соответствующих заголовков HTTP в запросе.

    Переменная окружения получается путём конвертации имени поля заголовка HTTP в верхний регистр, замещения всех дефисов - на подчёркивания _ и подстановки приставки HTTP, как в RFC 3875.

    Если есть несколько строк заголовка, отправленных с одним ключом (именем переменной), серверу следует рассматривать их так, как будто они были посланы в одной строке и объединять их при помощи , , как описано в RFC 2616.

В добавление к ключам, описанным выше, окружение PSGI ДОЛЖНО также включать следующие специфичные для PSGI ключи:

  • psgi.version: Ссылка на массив [1,1], представляющий используемую версию PSGI. Первое число - старший номер версии, второе - младший номер версии.

  • psgi.url_scheme: Строка http или https, зависящая от URL запроса.

  • psgi.input: входной поток. Подробности см. ниже.

  • psgi.errors: поток ошибок. Подробности см. ниже.

  • psgi.multithread: Это булево значение, которое ДОЛЖНО быть true, если приложение может быть одновременно вызвано другой нитью того же процесса, иначе false.

  • psgi.multiprocess: Это булево значение, которое ДОЛЖНО быть true, если эквивалентный объект приложения может быть одновременно вызван другим процессом, иначе false.

  • psgi.run_once: Булево значение, содержащее true, если сервер ожидает (но не гарантирует!), что приложение будет вызвано только в этот единственный раз за время жизни содержащего его процесса. Обычно это бывает верно только для сервера, основанного на CGI (или на чём-то похожем).

  • psgi.nonblocking: Булево значение, содержащее true, если сервер вызывает приложение в неблокирующем событийном цикле.

  • psgi.streaming: Булево значение, равное true, если сервер ожидает отложенного ответа в стиле callback и потокового пишущего объекта.

Сервер или приложение может также сохранять свои данные в переменных окружения. Эти переменные ДОЛЖНЫ содержать по меньшей мере одну точку, и их СЛЕДУЕТ называть, начиная с уникальной приставки.

Приставка psgi. зарезервирована для использования с основной спецификацией PSGI, а приставка psgix. зарезервирована для официально одобренных расширений. Эти приставки НЕ ДОЛЖНЫ использоваться другими серверами или приложением. См. список официально одобренных расширений в psgi-extensions.

Окружение НЕ ДОЛЖНО содержать ключи с именем HTTP_CONTENT_TYPE или HTTP_CONTENT_LENGTH.

Одна из переменных SCRIPT_NAME или PATH_INFO ДОЛЖНА быть установлена. Когда REQUEST_URI - это /, переменной PATH_INFO следует быть равной /, а переменной SCRIPT_NAME следует быть пустой. Переменная SCRIPT_NAME НЕ ДОЛЖНА быть равной /, но МОЖЕТ быть пустой.

Входной Поток

Входной поток в psgi.input - это IO::Handle-подобный объект, который отдаёт поток необработанных данных HTTP POST или PUT запросов. Если это дескриптор файла, он ДОЛЖЕН быть открыт в бинарном режиме. Входной поток ДОЛЖЕН отвечать на запрос read и МОЖЕТ реализовывать seek.

Встроенные в Perl файловые дескрипторы или основанные на IO::Handle объекты должны работать "как есть" внутри PSGI сервера. Разработчикам приложений НЕ СЛЕДУЕТ контролировать тип или класс потока. Вместо этого им СЛЕДУЕТ просто вызывать метод read объекта.

Разработчикам приложений НЕ СЛЕДУЕТ использовать встроенную функцию Perl read или итератор (<$fh>) для чтения из входного потока. Вместо этого разработчикам приложений следует вызывать read как метод ($fh->read) для обеспечения утиной типизации.

Разработчикам фреймворков, если они знают, что входной поток будет использован со встроенной функцией read() в любом апстрим-коде, который они не смогут трогать, СЛЕДУЕТ использовать PerlIO или tied handle для обработки этой проблемы.

Ожидается, что объект входного потока обеспечивает метод read:

read
  $input->read($buf, $len [, $offset ]);

Возвращает число действительно прочитанных символов, 0 в конце файла, или undef при ошибке.

Он может также реализовывать опциональный метод seek. Если переменная окружения psgix.input.buffered равна true, он ДОЛЖЕН реализовывать метод seek.

seek
  $input->seek($pos, $whence);

Возвращает 1 при успехе, иначе 0.

См. документацию IO::Handle для получения дальнейших подробностей о том, как эти методы в точности должны работать.

Поток Ошибок

Поток ошибок в psgi.errors - это IO::Handle-подобный объект для печати ошибок. Поток ошибок должен реализовывать метод print.

Как и с входным потоком, встроенные файловые дескрипторы Perl или основанные на IO::Handle объекты должны работать "как есть" в PSGI сервере. Разработчикам приложений НЕ СЛЕДУЕТ проверять тип или класс потока. Вместо этого, им СЛЕДУЕТ просто вызывать метод объекта print.

print
  $errors->print($error);

Возвращает true при успехе.

Ответ

Приложения ДОЛЖНЫ возвращать ответ или как ссылку на трёхэлементный массив, или code reference на отложенный/потоковый ответ.

Массив, на который возвращается ссылка, состоит из следующих элементов:

Статус

Код статуса HTTP. Это ДОЛЖНО быть целое число, большее или равное 100, и ему СЛЕДУЕТ быть кодом статуса HTTP, как описано в RFC 2616.

Заголовки

Заголовки ДОЛЖНЫ быть ссылкой на массив (не ссылкой на хеш) или парами ключ/значение. Это значит, они ДОЛЖНЫ содержать чётное число элементов.

Заголовок НЕ ДОЛЖЕН содержать ключ с названием Status, а также какие-либо ключи с : или переводом строки в названии. Он НЕ ДОЛЖЕН содержать какие-либо ключи, оканчивающиеся на - или _.

Все ключи ДОЛЖНЫ состоять только из букв, цифр, _ или -. Все ключи ДОЛЖНЫ начинаться с буквы. Значение заголовка ДОЛЖНО быть скалярной строкой, и должно быть определено. Значение строки НЕ ДОЛЖНО содержать символы ниже восьмеричного 037, т.е. chr(31).

Если одни и те же имена ключей появляются несколько раз в ссылке на массив, эти строки заголовков ДОЛЖНЫ отправляться к клиенту по отдельности (например, несколько строк Set-Cookie).

Content-Type

ДОЛЖЕН присутствовать Content-Type за исключением случая, когда Status равен 1xx, 204 или 304 - в этом случае НЕ ДОЛЖНО быть указано типа содержимого.

Content-Length

Заголовок Content-Length НЕ ДОЛЖЕН быть указан, когда Status равен 1xx, 204 или 304.

Если Status не 1xx, 204 или 304 и заголовок Content-Length отсутствует, PSGI сервер МОЖЕТ вычислить длину содержимого, глядя на Тело ответа. Это значение может затем быть добавлено к списку заголовков, возвращенному приложением.

Тело ответа

Тело ответа ДОЛЖНО быть возвращено из приложения или как ссылка на массив, или как дескриптор, содержащий тело ответа в виде байтовой строки. Тело ДОЛЖНО быть закодировано в соответствующей кодировке и НЕ ДОЛЖНО содержать "широких" символов (> 255).

  • Если тело ответа - ссылка на массив, ожидается, что оно содержит массив строк, из которых состоит само тело.

      my $body = [ "Hello\n", "World\n" ];

    Обратите внимание, что элементы в массиве НЕОБЯЗАТЕЛЬНО должны оканчиваться переводом строки. Серверу СЛЕДУЕТ отправлять каждый элемент "как есть" клиенту, и НЕ СЛЕДУЕТ заботиться о том, оканчиваются ли строки переводом строки, или нет.

    Ссылка на массив с единственным значением валидна. Так, [ $html ] - валидное тело ответа.

  • Тело ответа может вместо этого быть дескриптором - или встроенным файловым дескриптором Perl, или IO::Handle-подобным объектом.

      open my $body, "</path/to/file";
      open my $body, "<:via(SomePerlIO)", ...;
      my $body = IO::File->new("/path/to/file");
    
      # mock class that implements getline() and close()
      my $body = SomeClass->new();

    Серверам НЕ СЛЕДУЕТ проверять тип или класс тела ответа. Вместо этого им следует просто вызывать getline для перебора строк тела, и вызвать close по завершению.

    Серверы МОГУТ проверять, является ли тело реальным файловым дескриптором, используя fileno и Scalar::Util::reftype. Если тело - реальный дескриптор файла, сервер МОЖЕТ оптимизировать выполнение, используя методы вроде sendfile(2).

    Объект тела также МОЖЕТ иметь метод path. Ожидается, что этот метод возвратит путь к файлу, доступному серверу. Это позволяет серверу использовать данную информацию вместо номера файлового дескриптора для отдачи файла.

    Серверам СЛЕДУЕТ устанавливать специальную переменную $/ для получения размера буфера при чтении содержимого из $body при помощи метода getline. Это делается путём присваивания $/ ссылки на целое число ($/ = \8192).

    Если файловый дескриптор тела ответа - встроенный файловый дескриптор Perl или объект IO::Handle, они обратят внимание на значение. Похожим образом объект, предоставляющий аналогичный API, МОЖЕТ также обратить внимание на эту специальную переменную, но от него не требуется делать это.

Отложенный Ответ и Потоковое Тело ответа

The PSGI interface allows applications and servers to provide a callback-style response instead of the three-element array reference. This allows for a delayed response and a streaming body (server push).

Интерфейс PSGI позволяет приложениям и серверам предоставлять ответ в виде вызываемой функции вместо ссылки на трёхэлементный массив. Это позволяет возвращать отложенные ответы и потоковое тело ответа (server push).

PSGI серверам СЛЕДУЕТ реализовывать этот интерфейс, и переменная psgi.streaming должна быть равна true в таких серверах.

Чтобы разрешить отложенный ответ, приложению СЛЕДУЕТ возвращать вызываемую функцию в качестве ответа. Приложение МОЖЕТ проверять, что переменная psgi.streaming равна true, и возвращаться к непосредственному ответу, если это не так.

This callback will be called with another subroutine reference (referred to as the responder from now on) as its only argument. The responder should in turn be called with the standard three element array reference response. This is best illustrated with an example:

Эта вызываемая функция будет вызвана со ссылкой надругую функцию (далее называемую ответчик) в качестве единственного аргумента. Ответчик должен в свою очередь быть вызван со стандартным ответом в виде ссылки на трёхэлементный массив. Наилучшим образом это иллюстрируется следующим примером:

  my $app = sub {
      my $env = shift;

      # Откладывает ответ до тех пор, пока он не получит ответ от сети
      return sub {
          my $responder = shift;

          fetch_content_from_server(sub {
              my $content = shift;
              $responder->([ 200, $headers, [ $content ] ]);
          });
      };
  };

Приложение МОЖЕТ опустить третий элемент (тело) при вызове ответчика. Если тело пропущено, ответчик ДОЛЖЕН возвратить ещё один объект, в котором реализованы методы write и close. Снова проиллюстрируем это примером.

  my $app = sub {
      my $env = shift;

      # immediately starts the response and stream the content
      return sub {
          my $responder = shift;
          my $writer = $responder->(
              [ 200, [ 'Content-Type', 'application/json' ]]);

          wait_for_events(sub {
              my $new_event = shift;
              if ($new_event) {
                  $writer->write($new_event->as_json . "\n");
              } else {
                  $writer->close;
              }
          });
      };
  };

Этот отложенный ответ и потоковый API полезны, если вы хотите реализовать основанную на неблокирующем вводе/выводе отдачу потока сервером или технологию long-poll Comet push, но могут также быть использованы для реализации небуферизованной записи в блокирующем сервере.

Middleware

Компонент middleware берёт другое PSGI приложение и запускает его. С точки зрения сервера компонент middleware - PSGI приложение. С точки зрения приложения, запускаемого компонентом middleware, middleware - это сервер. Как правило это делается для реализации некоего способа предварительной обработки хеша PSGI окружения или пост-обработки запроса.

Вот простой пример, который добавляет специальный HTTP заголовок X-PSGI-Used к любому приложению PSGI.

  # $app - простое приложение PSGI
  my $app = sub {
      my $env = shift;
      return [ '200',
               [ 'Content-Type' => 'text/plain' ],
               [ "Hello World" ] ];
  };

  # $xheader - часть middleware, которая обёрнута вокруг $app
  my $xheader = sub {
      my $env = shift;
      my $res = $app->($env);
      push @{$res->[1]}, 'X-PSGI-Used' => 1;
      return $res;
  };

Middleware ДОЛЖНО вести себя в точности так же, как приложение PSGI с точки зрения сервера. Middleware МОЖЕТ решить не поддерживать потоковый интерфейс, обсуждавшийся ранее, но ему СЛЕДУЕТ пропускать беспрепятственно те типы ответов, которые оно не понимает.

CHANGELOGS

1.1: 2010.02.xx

  • Added optional PSGI keys as extensions: psgix.logger and psgix.session.

  • psgi.streaming SHOULD be implemented by PSGI servers, rather than MAY.

  • PSGI keys psgi.run_once, psgi.nonblocking and psgi.streaming MUST be set by PSGI servers.

  • Removed poll_cb from writer methods.

ACKNOWLEDGEMENTS

Some parts of this specification are adopted from the following specifications.

I'd like to thank authors of these great documents.

AUTHOR

Tatsuhiko Miyagawa <miyagawa@bulknews.net>

CONTRIBUTORS

The following people have contributed to the PSGI specification and Plack implementation by commiting their code, sending patches, reporting bugs, asking questions, suggesting useful advices, nitpicking, chatting on IRC or commenting on my blog (in no particular order):

  Tokuhiro Matsuno
  Kazuhiro Osawa
  Yuval Kogman
  Kazuho Oku
  Alexis Sukrieh
  Takatoshi Kitano
  Stevan Little
  Daisuke Murase
  mala
  Pedro Melo
  Jesse Luehrs
  John Beppu
  Shawn M Moore
  Mark Stosberg
  Matt S Trout
  Jesse Vincent
  Chia-liang Kao
  Dave Rolsky
  Hans Dieter Pearcey
  Randy J Ray
  Benjamin Trott
  Max Maischein
  Slaven Rezić
  Marcel Grünauer
  Masayoshi Sekimura
  Brock Wilcox
  Piers Cawley
  Daisuke Maki
  Kang-min Liu
  Yasuhiro Matsumoto
  Ash Berlin
  Artur Bergman
  Simon Cozens
  Scott McWhirter
  Jiro Nishiguchi
  Masahiro Chiba
  Patrick Donelan
  Paul Driver
  Florian Ragwitz

COPYRIGHT AND LICENSE

Copyright Tatsuhiko Miyagawa, 2009-2011.

This document is licensed under the Creative Commons license by-sa.




Hosting generously
sponsored by Bytemark