Механизм общения браузера и сервера на языке Python

18.09.2018 0 Автор admin

СОДЕРЖАНИЕ

  1. Простой сервер
  2. Конфигурируемый сервер
  3. Передача файлов и параметров
  4. WSGI
  5. Заставляем Python выполнить код на PHP
  6. ИТОГИ

 

Предисловие

Когда-то давно, читая вопросы ребят, работающих с Django пришел к мысли о том, что большинство вопросов вызваны непониманием механизма общения браузера и сервера. Поэтому я решил написать статью, в которой попытаюсь пролить свет и объяснить как все происходит. В данной статье будет частично рассмотрен протокол   http .

Простой сервер

Для того, чтобы понять как все происходит, я создам свой собственный веб-сервер, на котором и буду объяснять процесс общения.

Итак, приступим.

Создаем сервер:

Код сервера

Как вы видите, я использую модуль socket. Поэтому настоятельно рекомендую вам ознакомиться с этим модулем, так как здесь я буду объяснять лишь используемые мною свойства и методы модуля. В строках 10 и 11 мы указываем, что сервер будет работать на localhost и 80м порте (веб). Теперь, если мы запустим наш сервер и в браузере введем адрес localhost, то в консоли (или командной строке в Windows) увидим запрос, посланный браузером серверу. Здесь вы можете прочитать про заголовки запроса и ответа в  http . Я в данной статье буду использовать лишь некоторые заголовки. А здесь список Content-Type/MIME type.

Запускаем наш скрипт (сервер)

ВНИМАНИЕ! Если вдруг при запуске сервера вы получили ошибку, сообщающую о том, что адрес уже занят (Bind failed. Error Code : 98 Message Address already in use), то это означает лишь то, что на  80  порту уже работает какой-либо веб-сервер на данной машине. Необходимо остановить этот веб-сервер.

После запуска скрипта в консоли увидим сообщения о том, что сервер запустился.

У меня это выглядит вот так:

Теперь осталось открывать браузер и перейти на localhost. Как только мы напечатаем в адресной строке и нажмем «Ввод», то браузер инициализирует соединение и передаст некоторую информацию. Что это за информация мы увидим чуть позже. Строка 33 напечатает нам полученную информацию по соединении. А в строках 38 и 43 мы выведем всю информацию, которую передаст нам браузер.

Вот что мы увидим в консоли:

 

Теперь давайте разберемся, что же это за информация и как ее обрабатывать. Как я уже сказал, в строке 33 мы выводим информацию о подключении. На скриншоте мы видим, что к серверу подключились с ip-адреса 127.0.0.1:39494. А вот ниже уже идут строки данных, полученные от браузера. Разбираемся что это за информация. Согласно спецификации  http , первой строкой в запросе идет так называемая стартовая строка и формат ее такой:

 

где:

  • Метод — тип запроса, одно слово заглавными буквами. Список методов для версии 1.1 представлен в спецификации.
  • URI определяет путь к запрашиваемому документу.
  • Версия — пара разделённых точкой цифр. Например: 1.0.

Все 3 параметра разделены между собой одним пробелом.

Далее идет блок заголовков. Спецификация  http  регламентирует обязательно одну пустую строку после всех заголовков и перед началом тела самого сообщения.

Все заголовки разделяются на четыре основных группы:

  1. General Headers («Основные заголовки») — могут включаться в любое сообщение клиента и сервера;
  2. Request Headers («Заголовки запроса») — используются только в запросах клиента;
  3. Response Headers («Заголовки ответа») — только для ответов от сервера;
  4. Entity Headers («Заголовки сущности») — сопровождают каждую сущность сообщения.

В стартовой строке мы видим, что браузер сообщает о методе GET (имена всех методов пишутся ПРОПИСНЫМИ БУКВАМИ). Обратился браузер к ресурсу по адресу / и версия  HTTP  = 1.1. Далее смотрим на общие заголовки. Название заголовка и его значение разделены символами «: «. Первым заголовком идет Host. Так как мы обращались к localhost, то здесь мы и видим наш адрес. Если когда-то ранее браузер уже обращался к ресурсу localhost (возможно запускали тестовый веб-сервер, на котором разрабатывали сайт) и сервер передал браузеру куки, то браузер передаст серверу уже имеющиеся куки, что мы и видим на скрине выше по наличию заголовка Cookie. Благодаря этому имеют место быть атаки на куки у пользователей. Так как в запросе мы больше ничего не передаем, то тело запроса осталось пустым. Чуть позже я покажу что будет, если передать нашему серверу файл или какие-то другие данные из формы в html странице.

После того, как сервер получил и обработал запрос от браузера, сервер должен отправить браузеру ответ. Ответ так же начинается со стартовой строки.

Стартовая строка ответа сервера имеет следующий формат: 

где:

  • Версия — пара разделённых точкой цифр, как в запросе;
  • Код состояния (англ. Status Code) — три цифры. По коду состояния определяется дальнейшее содержимое сообщения и поведение клиента;
  • Пояснение (англ. Reason Phrase) — текстовое короткое пояснение к коду ответа для пользователя. Никак не влияет на сообщение и является необязательным.

Я воспользуюсь таким соглашением. Например, стартовая строка ответа сервера на предыдущий запрос может выглядеть так:

После стартовой строки должны идти заголовки ответа. Но сейчас никаких заголовков я передавать не буду. Я лишь ограничусь только стартовой строкой, в которой указываю версию, код ответа (200) и пояснение (ОК). После этого я делаю одну пустую строку (она обязательна), иначе не будет работать. Ну а дальше уже я передаю простенькую html страницу (переменная answermsg). Если сделали все правильно, то вы увидите в браузере слово ok, выделенное жирным шрифтом и курсивом.

Но сейчас сервер получился не очень гибким. А точнее даже вообще не гибким. Если я захочу изменить страницу, то мне придется лезть в код и менять его. Поэтому сейчас я переделаю свой сервер так, чтобы я мог использовать конфигурационные файлы, в которых смогу указать пути, указать файлы и, может быть, что-нибудь еще.

Конфигурируемый сервер

Сейчас я покажу простой пример того, как можно сделать наш сервер конфигурируемым. Для этого я воспользуюсь модулем ConfigParser (configparser для 3.х). Для этого я создам простенький файл конфигурации:

Первой строкой здесь будет являться хост, запрос для которого мы должны обработать. А во второй строке я указываю директорию, в которой лежит файл index.html.

Теперь код сервера выглядит так:

Код сервера
[свернуть]

Здесь я на всякий случай проверяю наличие заголовка Host в запросе. Если такой есть, то читаю к какому хосту направлен запрос. Далее я смотрю по какому uri идет обращение. Если uri обращен к главной странице сайта, то как правило uri будет равен «/», либо будет содержать ключевое слово /index[.html|.php]. Если обращение идет к главной странице сайта, то стоит проверить существование файла index[.html|.php] в корневой директории сайта. Если такой сайт есть, тогда надо отдать страницу. Если обращение идет к какой-то конкретной странице, тогда, если файл страницы существует, отдаем эту страницу. Если файлов страниц не существует, мы должны выдать ошибку с кодом 404 и пояснением «Not Found». Такой статус у меня сделан по-умолчанию. У меня получилась вот такая структура проекта для статьи:

В папку sites я положил два файла:

Теперь, если запустить сервер и сделать запрос к

то увидим в браузере страницу с содержанием из файла index.html.

Если сделать запрос к

то увидим страницу с содержанием из файла test.

Передача файлов и параметров

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

Чтобы добавить параметры к GET запросу, нужно в конце URL-адреса поставить знак «?» и после него начинать задавать их по следующему правилу:

Разделителем между параметрами служит знак «&».

Если запустим текущий вариант сервера и обратиться по адресу

то получим ошибку 404, так как файла с названием «test?te=15» не существует. Необходимо отделить название файла и параметры.

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

Пара слов о шаблоне для кода 404. Как видно из примера, шаблон страницы ответа тоже нужно создавать.

Теперь у меня получился вот такой код:

Код сервера
[свернуть]

В папке sites у меня лежит файл test, в который я вставил вот такой код:

Поэтому в коде сервера я указал

Таким образом я лишь просто вывожу переданные параметры.

Теперь, если запустить сервер и обратиться по адресу

с передачей различных параметров в запросе, эти параметры с их значениями будут выведены на странице в браузере.

Теперь осталось разобраться с тем, как происходит передача параметров в POST-запросе. Для этого в sites/index.html я создам форму, которая будет передавать POST-запрос к странице /test:

И вот здесь-то после нажатия на кнопку «Отправить» сервер у меня завершил процесс с ошибкой. И ошибка эта находится в строке 52. Как оказалось, все дело в том, что параметры POST запроса передаются не в стартовой строке запроса, как в GET, а в теле сообщения запроса. Поэтому нужно переписать код так, чтобы обрабатывать текст сообщений запроса:

Код сервера
[свернуть]

Обратите внимание, что названия передаваемых параметров есть ни что иное, как значение атрибута name тега <input>. Если не указать значение атрибута name, то в запрос данное поле не будет передано. Поэтому считайте, что этот атрибут обязательный, хотя на самом деле в html атрибуты не являются обязательными.

Теперь все в порядке. Процесс не завершается с ошибкой после нажатия на кнопку «Отправить» на клиенте. Вместо этого браузер переходит на страницу test и показывает параметры, которые мы передали в запросе POST.

Но теперь я хочу передать на сервер файл. Первое, что приходит на ум — это использовать поле типа file в форме:

Теперь на странице test мы увидим название файла. НО… Это ведь не сам файл, а лишь его наименование. Как сделать так, чтобы передать содержимое указанного файла??? Ответ на этот вопрос здесь:

Теперь вроде все в порядке. Но после запуска первое с чем я сталкиваюсь — это с размером передаваемого файла. В коде циклически читается 1024 байт, но файл, который я передаю, занимает порядка 10 кБ. В коде не происходит склейки новой порции данных со старыми при чтении данных. Исправляю:

Код сервера
[свернуть]

Теперь можно увидеть, что после добавления атрибута enctype=»multipart/form-data» содержимое запроса изменилось и можно увидеть передаваемые байты файла с его названием.

Теперь в заголовке Content-Type помимо типа еще есть параметр boundary, который переводится как «граница». Эта граница между параметрами в теле сообщений. С помощью значения этого параметра я буду разделять передаваемые параметры и их значения. Я не стал усложнять код, поэтому у меня получился вот такой результат:

Код сервера
[свернуть]

После запуска сервера и передачи текстового файла в папке со скриптом сервера у меня появился файл test.txt с содержимым передаваемого файла. Думаю теперь многим станет ясно, что очень важно правильно прочитать и обработать входящие данные от браузера.

Я не буду здесь рассматривать способ скачивания файлов с сервера и его методы докачки. Об этом можно прочитать тут.

WSGI

Но сервер еще до сих пор не является гибким и если нужно что-то добавить на сайт, то скорее всего придется переделывать код самого сервера. Чтобы избежать этого существует стандарт WSGI.

WSGI — стандарт обмена данными между веб-сервером (backend) и веб-приложением (frontend). Под это определение попадают многие вещи, тот же самый CGI. Так что поясню.

  • Во-первых, WSGI — Python-специфичный стандарт, его описывают PEP 333 и PEP 3333.
  • Во-вторых, он уже принят (статус Final).

По стандарту, WSGI-приложение должно удовлетворять следующим требованиям:

  • должно быть вызываемым (callable) объектом (обычно это функция или метод)
  • принимать два параметра:
    • словарь переменных окружения (environ)[2]
    • обработчик запроса (start_response)[3]
  • вызывать обработчик запроса с кодом  HTTP-ответа  и  HTTP-заголовками 
  • возвращать итерируемый объект с телом ответа

Значит сейчас я создам простенькое приложение, которое будет удовлетворять всем выше изложенным требованиям. У меня оно выглядит так:

В данном случае я создал функцию-приложение с названием simplest_wsgi_app и обработчик запроса start_response. Как видно из кода я ничего не обрабатываю в обработчике запроса, так как это лишь тестовый пример, чтобы показать как работает WSGI. Теперь это приложение нужно импортировать в сервер и при запросе от клиента выполнить данное приложение.

У меня получился вот такой код сервера с WSGI:

Код сервера
[свернуть]

В строке 75 я вызываю созданное приложение и полученное значение сохраняю в тело ответа сервера. Полученный ответ ниже отдается браузеру.

 

Заставляем Python выполнить код на PHP

А для тех, кто все же хочет на своем сервере запускать скрипты php отвечу, что да, такое возможно. Решение взято отсюда.

У меня получился вот такой код:

Код сервера
[свернуть]

Если сейчас обратиться по адресу

то будет выполнен скрипт test.php. У меня test.php выведет информацию о php:

 

ИТОГИ

Теперь я надеюсь всем стало ясно следующее:

  1. Когда мы в браузере печатаем адрес ресурса, тогда веб-браузер, будучи программой, соединяется с сервером и передает ему некоторые сведения: стартовую строку, заголовки и тело сообщения. Эту всю информацию веб сервер парсит, анализирует и выдает клиенту результат обработки запроса.
  2. Передача параметров для GET и POST запросов отличается.
  3. Передача файлов от клиента на сервер и от сервера на клиент тоже имеет свои нюансы. Можно организовывать «докачку» файла.
  4. Можно создавать на питоне свое собственное приложение и это приложение будет вызываться при каждом обращении к веб-серверу. Эту возможность нам дает стандарт WSGI. Есть еще и CGI.
  5. В свою очередь браузер должен правильно составить тело сообщения, стартовую строку и блок заголовков. Но в данной статье процесс работы браузера я не рассматривал.
  6. При формировании ответа мы можем использовать шаблоны страниц, заменяя в них определенные теги. Именно так сделано в Django. Подстановочные теги в ходе обработки заменяются на необходимую информацию.
  7. Страницы для ответов с любым кодом состояния, будь то 404 или 500, необходимо создавать. Именно благодаря этому в Django мы можем создавать свою страницу, указывая серверу, какой шаблон использовать для ответа.

Обращаю внимание, что все скрипты были запущены на Linux Ubuntu 16.04 64 bit.

Все исходные коды, изложенные в данной статье, можете скачать отсюда.

Проект на github — https://github.com/Termvsrobo/web-server

У меня на этом все. Ставьте лайки, пишите комментарии. Все замечания я постараюсь исправить.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.