Цели

  • Ознакомиться с протоколом HTTP.
  • Применить привычные приемы в новом контексте.
  • Перейти от Си к веб-программированию.

Запустите «Виртуальную лабораторию CS50» или CS50 IDE, и в терминале выполните команду:

update50

чтобы обновить окружение до актуальной версии.

Как и в практическом задании пятой недели, задачник шестой недели идет вместе с кодом, который нужно загрузить перед началом работы. В консоли перейдите в папку Dropbox, если вы работаете в «Виртуальной лаборатории CS50» (workspace для CS50 IDE) или любую другую, которую вы используете для работы и выполните следующую команду:

wget http://cdn.cs50.net/2014/fall/psets/6/pset6/pset6.zip

чтобы загрузить ZIP-архив с материалами к заданию. Если вы теперь выполните команду ls, по идее, вы увидите в вашей рабочей директории файл pset6.zip.

Разархивируйте его командой:

unzip pset6.zip

Если снова выполнить ls, вы заметите появление папки pset6. Теперь мы можем удалить архив:

rm -f pset6.zip

Перейдите в папку pset6:

cd pset6

выполните:

tree

tree — это такой рекурсивный иерархический вариант ls. Вы также увидите, что содержит папка, только в несколько другом виде и с
подпапками:

Подготовка к практическому заданию - 1

Незадача, снова этот Си нарисовался! Но помимо «сишного» файла есть еще несколько интересных вещей. Так что продолжаем. Откройте в gedit (или CS50 IDE) файл cat.html. Он достаточно прост, не так ли? Похоже, что в нём есть тег img, значение src которого — это cat.jpg.

Далее, открываем hello.html в gedit (или в CS50 IDE). Обратите внимание, он содержит форму для передачи через GET текстового поля name к hello.php. Присмотритесь к коду в файле hello.php. Преимущественно здесь HTML-код, но в теле есть и часть PHP-кода:

<? = Htmlspecialchars ($ _ GET [ "name"])?>

Нотация &lt? = просит «передать следующее значение сюда».

А вот htmlspecialchars — всего лишь специфически названная функция. Её задача — заверить, что специальные символы (наподобие ^lt) воспроизведены правильным образом как сущности HTML. Если интересно знать больше, посмотрите здесь: php.net/manual/en/function.htmlspecialchars.php.

Далее, $ _GET — это суперглобальная переменная, в которую через GET поступают любые HTTP-параметры в hello.php. Точнее, это ассоциативный массив (хеш-таблица) с ключами и значениями. По форме в hello.html, одним из таких ключей должно быть name! Но самое главное в деталях.

Теперь перейдем к самой веселой части: откройте server.c в gedit (или CS50 IDE).
Да, вы угадали! Вы должны реализовать ваш собственный веб-сервер, который в курсе, как обрабатывать статический (файлы .html, .jpg, например) так и динамический (файлы .php) контент.

server.c

server.c

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

Ниже определены несколько констант, они определяют ограничения размера для HTTP-запросов. Мы же определили их на основе тех значений, используемых в популярном веб-сервере Apache (если вдруг интересно: http://httpd.apache.org/docs/2.2/mod/core.html).

Ниже определена константа OCTETS. Она указывает, сколько октетов будет считываться в буфер за один раз. Октет — сетевой термин для обозначения привычного байта (8 бит).

Следующими в группу подключены заголовочные файлы, и определён октет — восьмибитный тип, аналогичный char. Также присутствует несколько прототипов.

В конце, непосредственно перед функцией main, определенные глобальные переменные.
Обычно их лучше избегать, однако это не всегда возможно. Вскоре вы убедитесь, что здесь присутствует «обработчик сигналов», а он требует, чтобы определенные состояния сервера были глобальными, поэтому полное высвобождение памяти состоится при отключении сервера (Ctrl-C)

main

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

Ниже — вызов функции getopt, определенной в unistd.h. Она облегчает синтаксический разбор ввода из командной строки. Подробности — в документации по getopt (man 3 getopt).

Обратите внимание, каким образом мы используем getopt (и некоторые логические выражения), чтобы гарантировать, что сервер используется должным образом.

Следом идет вызов start (прототип которого вы могли заметить ранее). Подробнее об этом позже.

Далее видим:

signal (SIGINT, handler)

Это — указание «прослушивать» ввод на предмет SIGINT (Ctrl-C — команды отключения) и если это событие произойдёт, будет вызван handler (это ещё одна функция, определённая в другом месте).

Далее main входит в бесконечный цикл while. Внутри цикла вызывается reset — определенная нами функция, которая служит для высвобождения определенной памяти и перевод ряда параметров в нужное состояние между обработкой HTTP-запросов.

После этого есть вызов connected в операторе if. Независимо от того, вернёт connected true или false, она сделает это путем «блокирования», ожидая, пока браузер подключится к серверу перед возвращением значения.

Затем происходит вызов parse. Эта функция синтаксически анализирует HTML-запрос браузера и возвращает один длинную строку (char *) запроса и все заголовки, причем каждая строка отделена посредством CRLF (\ r \ n), вместо LF (\ n). Согласно спецификациям, строки в HTTP-сообщениях (таких, как RCF-запросы на комментарии) должны быть разделены с помощью CRLF (как и текстовые файлы в Windows). Подробнее можно посмотреть на https://tools.ietf.org/html/rfc7230. Обратите внимание, что функция parse записывает адрес обработанного запроса в глобальной переменной request, объявленной перед main.

Затем следует код, обрабатывающий этот запрос дальше, только непосредственно со строкой запроса. И тут есть небольшое задание для вас(TODO)! Подробности вы узнаете немного позднее.

Под TODO содержится написанный нами код для добавления поддержки .php файлов. На первый взгляд он очень сложен, но на самом деле, всё, что код делает на запрос, например, hello.php, это выполнение кода наподобие:

QUERY_STRING = "name = Alice" REDIRECT_STATUS = 200 SCRIPT_FILENAME = / home / jharvard / Dropbox / pset6 / public / hello.php php-cgi

Эффект от этого заключается в передаче содержания hello.php интерпретатору PHP (php-cgi) вместе с HTTP-параметром среды QUERY_STRING.

С помощью написанной нами функции load, мы записываем ответ интерпретатора в память. Адрес при этом сохранится в глобальной переменной body, также объявленной перед main. Теперь сервер посылает браузеру динамично сгенерированный ответ:

HTTP / 1.1 200 OK
Connection: close
Content-Length: 127
X-Powered-By: PHP / 5.5.9-1ubuntu4.4
Content-type: text / html

<! DOCTYPE html>

&lthtml>
    &ltHead>
         &ltTitle> hello </ title>
     </ head>
     &ltbody>
        hello, Alice  </ body>
 </ html>

Возможно, использование popen, memmem, dprintf и write будет для вас в новинку. Прочитайте страницу документации (man). И знайте, что popen открывает «трубу» (pipe) для процесса (у нас для php-cgi), которую предоставляет указатель на файл (через него мы можем считывать стандартный вывод процесса так, как будто это был бы реальный файл).

Обратите внимание: функции dprintf, write, open и read используют дескриптор файла (значения int, вместо указателя FILE*) для обращения к файлам. Это важное, но не единственное отличие их от fprintf, fwrite, fopen и fread. Просто почитайте документацию для уточнения, что и где использовать.

Под этим кодом для работы с PHP-файлами есть ещё одна задачка для вас. Об этом тоже чуть позже.

Так вот, на этом main заканчивается! Обратите внимание, порой здесь используется continue для того, чтобы вернуться к началу бесконечного цикла, где вызывается reset. Иногда перед continue вызывается error с статус-кодом HTTP. Эти строки, вместе с теми, которые вы еще напишете, позволяют серверу обрабатывать и отвечать на ошибки прежде, чем обрабатывать новые запросы.

connected

Пробегитесь глазами по телу функции connected (под main). Не огорчайтесь, если непонятно, что она делает, но попробуйте догадаться, почитав страницы документации для memset и accept!

error

Уделите больше времени изучению error. Именно эта функция отправляет браузерам сообщения об ошибках (404, например). Она несколько длиннее, но содержит больше знакомых конструкций. Перед дальнейшей работой хорошенько разберитесь в ее работе.

load

Это функция, записывает файл в память, используя глобальную переменную file, определенную перед main. Просмотрите её код неспешно, постарайтесь понять, каким образом она использует один буфер buffer для считывания постоянного количества октетов за раз и соединяет их (в другом буфере (чей адрес содержится в body, другой глобальной переменной, находящейся перед main), размер которого по надобности может меняться (с помощью realloc).

Если кратко, она записывает содержимое файла в память.

handler

Как хорошо, что функция такая короткая! По сути, она только вызывает stop. А вызывается с помощью Ctrl-C

lookup

А тут еще одна задача для вас!

parse

Еще одна большая, очень похожа на load, функция. Parse считывает HTTP-запрос от соединения, отклоняет один CRLF и тело запроса (если таковой имеется), и оставляет только строку запроса и заголовки, адреса которых хранятся в глобальной request, также объявленной перед main. Заметьте, эта функция тоже в определенных случаях вызывает error.

reset

Это та функция вызывается внутри бесконечного цикла в main. Она освобождает память, но и обновляет глобальные переменные до известных значений.

start

Это функция запускает весь процесс. Не беспокойтесь, если не понимаете (даже с документацией) все, что в ней написано. Там вы найдёте преимущественно код для сети. Просто знайте, что эта функция настраивает сервер, чтобы тот устанавливал соединение на нужный TCP-порт!

stop

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