Протокол HTTP

Как работает WWW (всемирная паутина, веб) в двух словах:

  • браузер пользователя (клиент) отсылает на сервер запрос с адресом сайта (URL);
  • сервер получает этот запрос и отдаёт клиенту требуемый тому контент.

Иными словами, весь современный веб построен на модели клиент-серверного взаимодействия. И чтобы весь этот процесс оказался возможным, необходим универсальный язык-протокол, который будет понимать и сервер, и браузер. Такой протокол есть, а называется он просто — HTTP.

Как работает HTTP и зачем нам это знать

Программировать на PHP можно и без знания протокола HTTP, но есть ряд ситуаций, когда для решения определенных задач знание о том, как функционирует сервер, становится просто необходимым. Ведь PHP — это в первую очередь серверный язык программирования.

Протокол HTTP очень прост и состоит, по сути, из двух частей:

  • заголовков запроса/ответа;
  • тела запроса/ответа.

Всегда вначале идёт список заголовков, затем пустая строка, а затем (если есть) тело запроса/ответа.

И клиент и сервер могут посылать друг другу заголовки и тело, но только в случае с клиентом доступные заголовки будут одни, а с сервером — другие. Рассмотрим пошагово как будет выглядеть работа по протоколу HTTP в случае, когда пользователь хочет загрузить главную страницу социальной сети «Вконтакте».

1. Браузер пользователя устанавливает соединение с сервером vk.com и отправляет следующий запрос:

GET / HTTP/1.1
Host: vk.com

2. Сервер принимает запрос и отправляет ответ:

HTTP/1.1 200 OK
Server: Apache

<html>
<head>
  <title>ВКонтакте</title>
</head>
<!-- остальной контент страницы ниже -->

3. Браузер принимает ответ и показывает готовую страничку

Больше всего нам интересен самый первый шаг, где браузер инициирует запрос к серверу vk.com
Рассмотрим подробнее что тут происходит. Самая первая строка этого запроса определяет несколько важных параметров, а именно:

  • метод, которым будет запрошен контент;
  • адрес страницы;
  • версию протокола.

GET — это метод (глагол), который мы применяем для доступа к указанной странице.
GET является самым часто используемым методом, потому что он говорит серверу о том, что клиент всего лишь хочет прочитать указанный документ. Но помимо GET есть и другие методы, а один из них рассмотрим уже в следующем разделе.

После метода идет указание на адрес страницы — URI (универсальный идентификатор ресурса). В нашем случае мы запрашиваем главную страницу сайта, поэтому используется просто слеш — /.
Последним в этой строке идет версия протокола и почти всегда это будет HTTP/1.1

После строки с указанием основных параметров всегда следует перечисление заголовков, которые передают серверу дополнительную, полезную информацию: название и версия браузера, язык, кодировка, параметры кэширования и так далее.
Среди всех этих заголовков, которые передаются при каждом запросе, есть один обязательный и самый важный — это заголовок Host. Именно он определяет адрес домена, который запрашивает браузер клиента.

Сервер, в свою очередь, получив запрос, ищет у себя сайт с доменом из заголовка Host, а также указанную страницу.
Если запрошенный сайт и страница найдены, клиенту отправляется ответ:
HTTP/1.1 200 OK
Такой ответ означает, что всё хорошо, документ найден и будет отправлен клиенту. Если говорить более обобщённо, стартовая строка ответа имеет следующую структуру:
HTTP/Версия Код состояния Пояснение

Больше всего здесь интересен именно код состояния, он же код ответа сервера.
В этом примере код ответа — 200, что означает: сервер работает, документ найден и будет передан клиенту. Но не всегда всё идет гладко.
Например, запрошенный документ может отсутствовать или сервер будет перегружен, в таком случае клиент не получит контент, а код ответа будет отличным от 200.

  • 404 — если сервер доступен, но запрошённый документ не найден;
  • 503 — если сервер не может обрабатывать запросы по техническим причинам.

Спецификация HTTP 1.1 определяет 40 различных кодов HTTP.

После стартовой строки следуют заголовки, а затем тело ответа.

Работа с заголовками в PHP

В PHP есть все возможности для взаимодействия с протоколом HTTP:

  • получение тела запроса;
  • получение заголовков запроса;
  • добавление/изменение заголовков ответа;
  • управление телом ответа.

Разберём всё по порядку.

Получение тела запроса

Тело запроса — это информация, которую передал браузер при запросе страницы.
Но тело запроса присутствует только если браузер запросил страницу методом POST.
Дело в том, что POST - это метод, специально предназначенный для отправки данных на сайт. Чаще всего метод POST браузер задействует, когда отправляет форму. Значит содержимое самой формы и будет телом запроса.

В PHP-сценарии все данные отправленной формы будут доступны в специальном массиве $_POST. Более подробно об этом написано в следующей главе, посвящённой формам.

Получение заголовков запроса

Напомним ещё раз, что заголовки запроса — это метаинформация, отправленная браузером при запросе нашего сценария.
PHP автоматически извлекает такие заголовки и помещает их в специальный массив — $_SERVER.
Стоит отметить, что в этом массиве помимо заголовков есть и другая информация. Значения заголовков запроса находятся под ключами, что начинаются с HTTP_. Подробно всё содержимое этого массива описано в официальной документации.

Пример как получить предыдущую страницу, с которой перешёл пользователь:

print($_SERVER['HTTP_REFERER']);

Добавление/изменение заголовков ответа

В PHP сценарии можно управлять всеми заголовками ответа, которые попадут к пользователю вместе с контентом страницы. Это возможно, потому что PHP работает на стороне веб-сервера и имеет с ним очень тесную интеграцию.
Вот примеры сценариев, когда пригодится управление заголовками ответа:

  • кэширование;
  • переадресация пользователя;
  • установка cookies;
  • отправка файлов;
  • передача дополнительной информации браузеру.

Как видно, заголовки ответа нужны для выполнения множества важных задач.
Чтобы отправить или поменять абсолютно любой заголовок в PHP есть ровно одна функция: header().
Эта функция принимает имя и значение заголовка и добавляет его в список из всех заголовков, которые уйдут в браузер пользователю после окончания работы сценария.
Например, таким способом выполняется перенаправление пользователя на другую страницу:

header("Location: /index.php");

За переадресацию отвечает заголовок с именем Location, а через двоеточие ему задали значение — адрес страницы для перехода.

Важное замечание по использованию заголовков
Есть одно ограничение: заголовки нельзя отправлять, если пользователю уже отправили любой контент. То есть, если показать что-то на экране через, скажем, функцию print(), то после этого заголовки поменять уже не получится.

Управление телом ответа

Как менять тело ответа ты уже знаешь, потому что всё что PHP выводит на экран и есть содержимое ответа. Иными словами, вызовы функций print, echo или показ текста через шорт-теги: всё это является телом ответа, которое попадает в браузер пользователю.

Параметры запроса

Мы привыкли, что на нашем сайте каждый PHP-сценарий отвечает за одну страницу. Посетитель сайта вбивает в адресную строку путь, который состоит из имени домена и имени php-сценария. Например, так: http://weather-diary.ru/day.php.
Но как быть, если одна страница должна показывать разную информацию?

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

Из чего состоит URI

URI — это уникальный идентификатор ресурса. Ресурс в нашем случае — это полный путь до страницы сайта. И вот как может выглядеть ресурс для показа погоды за конкретный день:
http://weather-diary.ru/day.php?date=2017-10-15

Разберем из чего состоит этот URI.
Во-первых, здесь есть имя домена: weather-diary.ru.
Затем идёт имя сценария: day.php
А вот всё что идёт после — это параметры запроса.

Параметры запроса — это как-бы дополнительные аттрибуты адреса страницы. Они отделяются от имени страницы знаком запроса. В примере выше параметр запроса только один: date=2017-10-30.
Имя этого параметра:date, значение: 2017-10-15.
Параметров запроса может быть несколько, тогда они разделяются знаком амперсанда: ?date=2017-10-15&tscale=celsius

В примере выше указывается два аргумента: дата и единица измерения температуры.

Параметры запроса как внешние переменные

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

Получение параметров запроса

Если есть внешние переменные, то как их прочитать?
Все параметры запроса находятся в специальном, ассоциативном массиве $_GET, а значит сценарий, вызванный с таким адресом: day.php?date=2017-10-15&tscale=celsiusбудет иметь в этом массиве два значения с ключами date и scale соответственно.
Пример как сформировать запрос на получение данных за выбранный день:

<?php
$date = $_GET['date'] ?? date('Y-m-d');
$sql = sprintf('SELECT * FROM weather WHERE day = "%s"', mysqli_real_escape_string($con, $date));

В примере выше в первой строчке мы получаем значение параметра date, а если он отсутствует, то используем текущую дату в качестве выбранного дня.
Никогда не полагайтесь на существование параметра в массиве $_GET и делайте проверку либо функцией isset(), либо как в этом примере.

Формирование URI с параметрами запроса

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

<?php
$params = $_GET;
$date = $params['date'] ?? date('Y-m-d');

$tomorrow = date('Y-m-d', strtotime('tomorrow', strtotime($date)));
$params['date'] = $tomorrow;

$url = basename(__FILE__) . '/?' . http_build_query($params);
print($url);

Здесь мы использовали две функции:

  • basename(__FILE__) — получает имя текущего сценария;
  • http_build_query() — преобразует ассоциативный массив в строку запроса.