Салимóненко Дмитрий Александрович

Вычислительные Сети, Системы и Телекоммуникации

ЗАДАНИЕ 7: Простой браузерный визуальный html-«редактор» на ТСР-сокетах (C++, Linux)


Введение

В этом задании мы с Вами попробуем создать простейший браузерный визуальный (WYSIWYG) html-«редактор» на ТСР-сокетах, работающий в операционной системе linux. Конечно, термин «редактор» надо бы взять в кавычки: все, что он может делать – это обеспечивать визуальное редактирование локальной html-страницы, открытой в браузере и сохранять сделанные изменения после нажатия кнопки.

Думается, что те, кто детально разберется в этом небольшом проекте и самостоятельно сможет его как-то дополнить, скорректировать – вполне могут считать, что, в первом приближении, разобрались с сетевыми технологиями «клиент-сервер» на TCP/HTTP уровне. Вообще, на мой взгляд, подобные технологии – это один из самых сложных вопросов в прикладном программировании: ведь приходится одновременно отлаживать ДВЕ программы. Причем работа сервера зависит от того, что возвращает клиент, а клиент, наоборот, зависит от того, что вернет ему сервер.

Конечно, здесь не рассматриваются такие клиент-серверные технологии, как AJAX, Comet, WebSockets, …

Естественно, разработка сетевых приложений на языках типа PHP, Python, java и т.п. – гораздо проще. Так что, научившись здесь, уж на Яве (или РНР - см. ниже) вполне напишете то, что потребуется. Ну, при условии освоения соответствующих языков, конечно.

Обычными средствами браузера (по крайней мере, Firefox) подобного не достичь: даже если перевести страницу в режим редактирования, при сохранении (Файл-> Сохранить как -> …) будет сохраняться исходный код, а не то, что было результатом редактирования на странице браузера. Хотя, конечно, наверняка, существуют различные дополнения плагины, которые позволяют это сделать.

Редактирование и сохранение результатов обеспечивается путем взаимодействия с программой «сервер», написанной на языке С/С++.

Об ограничениях и недостатках редактора кратко поговорим ниже; на самом деле – их очень много. Цель разработки «редактора», в основном, учебная и состоит в том, чтобы более подробно и детально разобраться во взаимодействии клиент-серверных приложений. Хотя, конечно, его вполне можно применять и на практике при работе с html-файлами. С некоторыми оговорками. Например, этот «редактор» неспособен корректно работать с контентом страницы, сформированным динамически (при помощи javascript). Т.е. редактировать-то такой контент можно, но после сохранения он останется на странице, продублировавшись. Тем более, редактор не дает возможности редактировать, не говоря уже о сохранении, то, что создано средствами CSS.

Хотя, справедливости ради, стоит сказать, что на это неспособен ни один из известных мне визуальных редакторов html (если говорить именно о работе в WYSIWYG режиме, а не о работе непосредственно с кодом html). Это – именно тот факт, который мешает созданию полноценных визуальных редакторов. Самое интересное, я это понял только после того, как создал этот «редактор». К сожалению, уже задним числом. В свое время, я где-то читал одну переводную статью о невозможности создания полноценного визуального редактора, но там как-то все было в общих словах, расплывчато.

Хотя, может, в будущем такие редакторы появятся. Но, это уже будет целая интеллектуальная система, способная проводить анализ способа, последовательности возникновения контента и корректировать их в визуальном режиме. На данный момент, мне неизвестны даже намеки на это. К известным мне визуальным конструкторам сайтов это ни в коей мере не относится.

Итак, редактируем только то, что является результатом обработки браузером чистого html. Т.е. если надо по-быстрому что-то исправить в ТЕКСТЕ страницы (например, грамматические ошибки, которые будут отлично выделены браузером – примерно так, как в Word) и нет желания копаться в коде html – вот тогда этот редактор будет удобен.

Состав проекта

В проект, для удобства, входят четыре файла: f_nonadress.html, server_saver.html, server_saver.c, server_saver. Хотя, при необходимости, вполне можно «прошить» вспомогательные html-файлы в ОДНОЙ программе-сервере server_saver. Но, тогда, при необходимости внесения даже мелких изменений в них придется заново перекомпилировать программу. Поэтому было решено сделать не один, а несколько служебных файлов. Они должны находиться в каком-нибудь одном каталоге. Месторасположение его – произвольно; ну, с учетом системных и иных требований, конечно.


Этапы взаимодействия браузера с программой-сервером

Перечислим все этапы взаимодействия браузера с программой-сервером с точки зрения пользователя, которого не интересуют программные аспекты:

  1. Открыть в браузере ЛОКАЛЬНУЮ страницу html, которую следует отредактировать. Пусть это будет файл f.html.
  2. Запустить программу-сервер server_saver (без этого ничего работать не будет).
  3. В закладках браузера следует нажать закладку «Редактировать эту страницу».
  4. На странице появится зеленая кнопка «Начать редактирование», на которую следует нажать левой кнопкой мыши.
  5. После этого в браузере откроется соседняя вкладка, на которой возникнет сообщение «Нажмите ОК».
  6. После нажатия «ОК» появится вновь редактируемая страница, а на ней, примерно в середине – форма с кнопкой «Сохранить». Теперь на странице можно редактировать (добавлять, исправлять, удалять текст, в том числе, пустые строчки). После нажатия кнопки «Сохранить», сделанные изменения будут записаны в файл f1.html, в чем можно убедиться, открыв его в браузере.

Процесс работы "редактора"

А теперь рассмотрим процесс работы редактора более подробно, с точки зрения программирования:

  1. Открываем в браузере ЛОКАЛЬНУЮ страницу html, которую следует отредактировать. Например, файл f.html. Т.е. страница изначально должна быть открыта по протоколу FILE. Это можно сделать, например, путем нажатия правой кнопки мыши на файле (в менеджере файлов), затем «Открыть с помощью», «Браузер». Редактор, в его нынешней версии, не предназначен для редактирования страниц сайтов, открытых из интернета (хотя, старые версии некоторых браузеров, например, Опера - позволяли это делать).
  2. Запускаем программу-сервер. Она представляет собой обычную программу-сервер, с технологией создания которой Вы уже не раз встречались во многих заданиях по Вычислительным сетям, системам и телекоммуникациям. Эта программа создает сокет, привязывает его к некоторому порту (как уж у нас повелось, это – 3425). При нажатии кнопки «Сохранить» браузер отправляет определенное сообщение программе, а она, анализируя его, выделяет из сообщения html-код отредактированной страницы и сохраняет его в файле. После того, как изменения будут сохранены, можно продолжить редактирование.
  3. В закладках браузера следует нажать вкладку «Редактировать эту страницу». Эта закладка представляет собой небольшой букмарклет, который копирует на открытую в браузере html-страницу некоторый javascript-код, предназначенный для (только для этого!) изменения протокола с FILE на HTTP, а также чтобы задать хост, с которого будет открываться страница – локальный хост или 127.0.0.1; также задается порт. Запуск этого кода производится путем нажатия на кнопку «Начать редактирование». При его срабатывании страница открывается в новой вкладке, но уже по протоколу HTTP и с указанием локального хоста. Дело в том, что первый протокол не позволяет никоим образом взаимодействовать с программой-сервером, тогда как HTTP как раз и предназначен для подобного.
  4. Новая вкладка, открытая по протоколу HTTP , содержит, на первом этапе, начальную страницу из файла f_nonadress.html, а также содержимое файла server_saver.html. Это – результат работы сервера, который отправляет соответствующее содержимое браузеру по адресу http://127.0.0.1:3425/.../f.html. Отметим, что на этом этапе во вкладке, открытой по протоколу HTTP, еще НЕ ПРИСУТСТВУЕТ код html той страницы, которая была открыта с целью редактирования. При открытии этой новой вкладки вначале формируется ее URL вида "http://127.0.0.1:3425/" + doc_loc, где doc_loc – это URL изначально открытой локальной страницы в браузере без учета самых первых символов file:/// . Т.е. file:/// меняется на http://127.0.0.1:3425/ . Так вот, вначале по протоколу HTTP открывается во вкладке начальная страница (но, ее URL будет http://127.0.0.1:3425/.../f.html). Затем, после полной ее загрузки, срабатывает событие onload тега body. При этом браузер запускает обработчик этого события subm_form(). Он, в свою очередь, вызывает обработчик полей формы сохранения изменения f(). Кроме того, он вызывает появление контрольного (ну, на всякий случай, чтобы было яснее, как все работает) сообщения «Нажмите ОК» и, после этого, отправку полей формы серверу. Сама форма, с предварительно заполненными полями, содержится в файле server_saver.html. Перед отправкой, как только что говорилось, ее поля изменяются при помощи обработчика f(). Код из файла server_saver.html задает html-форму сохранения изменений, сделанных в процессе редактирования и упомянутый обработчик f(). Кроме того, он задает для тега body атрибут contenteditable, равный значению true. Это и позволяет сделать страницу, открытую в браузере, редактируемой. Поля формы содержат URL текущей страницы (что важно, ДЕкодированный; это позволяет не осуществлять декодирование на сервере), а также почти весь html-код редактируемой страницы.
  5. После редактирования страницы и нажатия на кнопку «Сохранить» браузер, при помощи кода javascript, формирует переменную. в которую входит все содержимое тегов <html>…</html> включая сами эта теги. Также формируется переменная, содержащая URL редактируемой страницы в ДЕкодированном виде (при этом русские буквы так и передадутся русскими, а не в виде %D0%A0%D0…. Хотя, это и НЕкорректно (на практике так лучше не делать), но я так сделал, чтобы не возиться с декодированием на сервере. Ибо средствами JS – это достигается при помощи команды decodeURI(), а на сервере пришлось бы писать свою соответствующую процедуру, стандартных функций в С/С++ под Linux я что-то не нашел. После чего, браузер отправляет серверу URL страницы, заголовки, а также указанные переменные при помощи запроса POST. А наш сервер выводит в консоль все то, что получит от браузера. Сервер  сохраняет содержимое тегов <html>…</html>, присланное браузером в качестве одной из переменных, в файле f.html. А из другой переменной он извлекает полное имя файла f.html, открывает его, считывает и передает содержимое браузеру. Т.е. после обработки запроса браузера, сервер посылает ему свои заголовки, пустую строку и, что самое важное, html-код страницы f.html вместе с html-кодом формы сохранения и требующийся для ее работы JS. Ну, а браузер просто отображает на экране все то, что прислал ему сервер, в соответствии с правилами протокола HTTP .

При последующих сохранениях пункты 4-5 повторяются.

Наверное, с первого раза выглядит сложно. Дело в том, что клиент-серверные технологии, да еще на «низком» уровне (на сокетах, безо всяких высокоуровневых оболочек типа РНР, Apache и т.п.) представляют собой один из самых сложных разделов в прикладном программировании (я не принимаю во внимание здесь ту область, которая связана со сложными математическими алгоритмами; в последнем случае актуально не столько чисто умение программировать, сколько - реализовать правильный и эффективный алгоритм). Поэтому совсем уж по-простому здесь объяснить не получится. Однако, если кто освоит хотя бы это, он будет свободен от условностей и суеверий в создании и программировании сетевых технологий. И почувствует работу клиентов и серверов как бы немного изнутри. И поймет, что, скажем, передача браузером серверу запроса типа http://127.0.0.1:3425/katalog/f.html из адресной строки, на самом-то деле, представляет собой, всего-навсего, ПЕРЕДАЧУ СООТВЕТСТВУЮЩЕЙ СТРОКИ БАЙТ СЕРВЕРУ через сокет, открытый по порту 3425 на локальном хосте. И все. А уж что сделает с этой строкой сервер – это зависит от него и только от него. Он может, например, вернуть в сокет содержимое локального файла /katalog/f.html (и тогда этот файл будет вновь отображен браузером – страница будет обновлена), вывести какую-то информацию на консоли, записать что-то в какой-нибудь файл (вовсе необязательно f.html), вернуть браузеру через сокет содержимое ДРУГОГО ФАЙЛА (и тогда именно оно будет отображено браузером, вместо файла f.html) или сделать вообще что-то другое.

Процесс работы "редактора" (подробно)

Чтобы было нагляднее, давайте посмотрим, что делается, на нижеприведенных рисунках. Запускаем программу сервер. Открываем в браузере страницу f.html. В закладках браузера нажимаем «Редактировать эту страницу». После чего видим следующее:

Запущена программа-сервер, открыт файл html по протоколу file:

Файл f.html состоит из минимальной html-разметки и фразы «Hello world!». Выделенная часть (строки 13…29) – это результат действия закладки (букмарклета). Т.е. букмарклетом было дописано это содержимое на страницу f.html, открытую в браузере. В исходном коде, естественно, этого содержимого нет.

Нажимаем кнопку «Перейти к редактированию этой страницы:

Открыта новая вкладка по протоколу http

В браузере открылась вкладка http://127.0.0.1:3425/.../f.html, появилось сообщение: «Нажмите ОК». Перед этим браузер, соответственно, передал серверу GET-запрос на открытие это страницы, что видно в консоли слева: сервер вывел вначале все заголовки, присланные браузером, потом вывел то, что он послал браузеру. А это «то» есть не что иное, как содержимое файла f_nonadress.html. Т.е. сервер в ответ на запрос браузера передал ему (на данном этапе) содержимое указанного файла.

Нажимаем в браузере «Нажмите ОК»

Результат редактирования страницы html (пример)

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

----------3009315
….
----------3009315

В этих областях и содержатся байты – переменные, которые браузер отсылает серверу. Так получилось в соответствии с отправкой сообщения браузером в формате протокола http. Две переменные (с именами name=”url” и name=”html”), соответственно, две области. Первая переменная содержит, через пустую строку, URL открытой в браузере страницы, а вторая - содержимое файла server_saver.html без скрипта, имеющего класс class="asdfghj". Почему? Потому, что браузер удалил этот скрипт из DOM открытой страницы перед отправкой сообщения (это выполнил обработчик f(), содержащийся в файле server_saver.html).

После этого сервер вывел в консоль еще некоторую вспомогательную информацию (см. server_saver.c). Перед этим он открыл файл f.html. Благо, из запроса браузера, путем анализа области

----------3009315
name=”url”

----------3009315

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

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

Повторимся, на практике лучше извлекать URL из заголовков браузера и декодировать его, а не из переменной POST-запроса.

Сервер в ответ на запрос браузера вернул (ему) содержимое страницы f.html + содержимое файла server_saver.html. Последнее необходимо для работы с этой страницей в браузере, в частности, для последующей отправки изменений, сделанных в результате ее редактирования.

Как видим, в браузере открылась страница файла f.html, на ней размещена зеленая форма с кнопкой для сохранения.

Напишем в строчке, ниже фразы «Hello World!» любое слово, например, 12345. Ну, или что-то иное. Слова можно писать на новых строчках, делая переносы строк.

Примечание. В браузере нажатие клавиши «Enter» создает новый абзац (или блок – зависит от браузера). А нажатие «Shift + Enter» создает перенос строки (тег <br />).
Строго говоря, если открыта страница именно html, а не xhtml, браузер, скорее всего, создаст тег <br>, а не <br />.

После этого, для сохранения, нажимаем кнопку «Сохранить». обновляя файл f1.html, видим в разметке, в самом деле, внесенные изменения. Те же изменения видны и в консоли (слева) на рисунке:

Процесс редактирования и последующего сохранения результатов может продолжаться до тех пор, пока работает программа сервер. Каждый раз после отправки сообщения браузеру и после завершения необходимых действий сервер ожидает прихода сообщений на открытый им сокет по порту 3425.

Кстати, возможно одновременное редактирование нескольких локальных файлов, открытых в соответствующих вкладках браузера, сразу.

Ограничения и недостатки «редактора»

Их, как уже говорилось, достаточно много:

  1. Невозможность корректного редактирования контента, образованного при помощи javascript, не говоря уже о CSS.
  2. «Редактор» является уязвимой программой, так как практически нигде входные данные не проверяются. Отсюда возникает возможность переполнения памяти, иногда (например, при некорректных заголовках, полученных от браузера) возможен аварийный останов программы.
  3. Не производится проверка расширения открываемого файла.
  4. Не производится проверка того факта, что открываемый файл действительно содержит ВАЛИДНУЮ (это важно!) разметку html. В случае же, если разметка html-файла окажется невалидной, он, скорее всего, немного изменится при редактировании. Дело в том, что при сохранении выполненных при редактировании изменений, браузер, перед отправкой сообщения-запроса серверу, должен прочитать html-код страницы. Это делается средствами javascript. Для этого он обращается к созданной ранее браузером ее DOM (document object model). Если разметка является валидной, то все хорошо, DOM создается без проблем. А вот если нет – браузер, в процессе формирования DOM, делает ее валидной ПРИНУДИТЕЛЬНО. Иначе невозможно будет построить DOM и, соответственно, обратиться к нему при помощи JS. Кстати, в случае невалидной разметки, разные браузеры могут делать DOM по-разному. Но, в любом случае, при построении DOM для невалидной разметки содержимое файла, отправляемое серверу, даже при отсутствии редактирования, не будет совпадать с исходным. Например, если в исходном файле, который открыт для редактирования, есть открытый тег <p>, но нет закрывающего, то браузер обязательно САМ закроет его, добавив </p> туда, куда сочтет наиболее правильным. Естественно, эти изменения будут потом сохранены сервером.
  5. Не сохраняется DOCTYPE, так как из JS невозможно получить к нему доступ в форме, достаточной для его считывания. DOCTYPE можно сохранить, разве что, средствами сервера. Это вполне допустимо, ведь при редактировании страницы в браузере изменить DOCTYPE невозможно, он остается тем же самым.
  6. Не всегда сохраняются символы, присутствующие после закрывающего тега </html>. Например, там может быть комментарий.

Теперь перейдем к непосредственному разбору программных кодов.



Программные коды

Сервер (файл server_saver.c):

  1. // Виртуальный Эхо-сервер: редактор страницы html, открытой в браузере.
  2. #include <sys/socket.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5. #include <unistd.h>
  6. //#include <fcntl.h>
  7. #include <sys/stat.h>
  8. #include <sys/types.h>
  9. #include <netinet/in.h>
  10. #include <cstring>
  11. //#include <regex>
  12. #include <iostream>
  13. // Описываем прототип функции чтения из html-файла
  14. char *open_read_file(char *);
  15.  
  16. using namespace std;
  17. string str = "";
  18.  
  19. int main(void)
  20. {
  21. char message1[10240], mes[4];
  22. unsigned int sz;
  23. int sock, listener;
  24. struct sockaddr_in addr;
  25. char buf[20480];
  26. int bytes_read = 0;
  27. listener = socket(AF_INET, SOCK_STREAM, 0);
  28.  
  29. if(listener < 0)
  30. {
  31. perror("socket");
  32. exit(1);
  33. }
  34.  
  35. //  Устанавливаем сокету параметр SO_REUSEADDR или SO_REUSEPORT, чтобы после прекращения работы программы порт не использовался системой и был готов для последующего использования. Полезно, когда программу нужно запустить вновь СРАЗУ после того, как она прекратила работу в предыдущий раз.
  36. int reuse = 1;
  37. if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) < 0)
    1. perror("setsockopt(SO_REUSEADDR) failed");
  38. #ifdef SO_REUSEPORT
  39. if (setsockopt(listener, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuse, sizeof(reuse)) < 0)
    1. perror("setsockopt(SO_REUSEPORT) failed");
  40. #endif
  41.  
  42. addr.sin_family = AF_INET;
  43. addr.sin_port = htons(3425);
  44. addr.sin_addr.s_addr = htonl(INADDR_ANY);
  45. if(bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0)
  46. {
  47. perror("bind");
  48. exit(2);
  49. }
  50. listen(listener, 1);
  51. int i =0;   // Счетчик итераций сервера
  52. while(1)
  53. {i++;
  54. sock = 0;
  55. sz=sizeof(addr);
  56. sock = accept(listener, (struct sockaddr *)&addr, &sz );
  57. //  printf ("\n");
  58. printf("%d 1 - %d\n", bytes_read, sock);
  59. if(sock < 0) {perror("accept"); exit(3);}
  60.  
  61. //printf("%d 2 - %d\n", bytes_read, sizeof(buf));
  62. memset (buf, 0, sizeof(buf));
  63. bytes_read = recv(sock, buf, sizeof(buf), 0);
  64.  
  65. // Печатаем в консоль разъясняющую информацию (номер итерации и протоколы, принятые от браузера)
  66. printf("Server iteration: %d \n\n", i);
  67. printf("Browser request: \n");
  68.  
  69. //  printf("%d 3 - %d\n", bytes_read, sizeof(buf));
  70. printf("%s \n", buf);
  71. string buf_string = string(buf);
  72.  
  73. // Определяем значение    boundary в заголовках, присланных браузером
  74. // Вначале находим сами заголовки. Они, как известно, идут в начале сообщения и отделены от тела сообщения пустой строкой (двухкратными \r\n)
  75. int size_beg = sizeof("POST") - 1;  
  76. int p1 = buf_string.find("POST");
  77. int p2 = buf_string.find("\r\n\r\n", p1+1); // Указав p1+1, игнорируем первую пустую строку \r\n (если она есть), которую может передать клиент, нарушающий протокол HTTP
  78. printf("%d 4 - %d %d\n", p1, p2, ((p1 >= 0) && (p2 > 0)));
  79.  
  80. string url = "";    
  81.  
  82. if((p1 >= 0) && (p2 > 0)) {  // Для чего сделана эта проверка?
  83. string headers = buf_string.string::substr(p1 + size_beg, p2 - p1 - size_beg);  // Заголовки без первых 4 символов
  84.  
  85. cout<<"___________   headers:  _____________\n"<<headers<<'\n';
  86.  
  87. // Теперь в заголовках находим boundary и до конца соответствующего заголовка (строки)
  88. size_beg = sizeof("Content-Type: multipart/form-data; boundary=") - 1 ; // не учитываем \0
  89. p1 = headers.find("Content-Type: multipart/form-data; boundary=") ;
  90. p2 = headers.find("\r\n", p1);
  91. string boundary = "--" + headers.string::substr(p1 + size_beg, p2  - p1 - size_beg);        
    1. cout <<"p1= "<<p1<<"  p2= "<<p2<< "  boundary: \n" << boundary << '\n';
  92.  
  93. p1 = buf_string.find("\r\n\r\n")+sizeof("\r\n\r\n");
  94. p2 =  buf_string.size();
  95. string html_and_mes = buf_string.string::substr(p1, p2);        
  96.  
  97. p1 = html_and_mes.find("name=\"html\"")+sizeof("name=\"html\"")+sizeof("\r\n\r\n")-2;
  98. p2 = html_and_mes.find(boundary, p1);
  99. string html = html_and_mes.string::substr(p1, p2-p1-1 );
  100.  
  101. // Теперь находим переданный URL
  102. p1 = html_and_mes.find("name=\"url\"")+sizeof("name=\"url\"")+sizeof("\r\n\r\n")-2;
  103. p2 = html_and_mes.find(boundary, p1);
  104. url = html_and_mes.string::substr(p1, p2-p1-1 );
  105.  
  106. p1 = url.find("3425");
  107.  
  108. if((int)url.find("#") >= 0){ // На случай, если вдруг в адресной строке браузера после имени файла будет введен якорь (например, #anckor)
  109. p2 = (int)url.find("#");
  110. url = url.string::substr(p1, p2-p1 );
  111. }
  112.  
  113. p1 = url.find("3425");          
  114. if((int)url.find("?") >= 0){// На случай, если будет сделан GET-запрос через адресную строку
  115. p2 = (int)url.find("?");
  116. }else{
  117. p2 = url.size() - 1; // Для чего сделано -1 ?
  118. }
  119.  
  120. url = url.string::substr(p1 + sizeof("3425")-1, p2-p1 - sizeof("3425")+1);  
  121. char* url1 = (char*)url.c_str(); // Преобразуем string в char (в С++)

  1. string url_cons = "/home/Рабочий стол/f.html"; // Это лишь для примера, как представляются строки в случае нелатинских (например, кириллического) алфавитов
  2. cout<<"n  r  : "<<(int)url.find('\n')<<"  "<<(int)url.find('\r')<<"  "<<(int)url.find('\0')<<'\n' ;
  3. cout<<"n  r  : "<<(int)url.find("\n")<<"  "<<(int)url.find("\r")<<"  "<<(int)url.find("\0")<<'\n' ;
  4. cout<<strlen(url1)<<"   "<<url.size()<<"   "<< url_cons.size()<<"\n\n"  ;
  5.  
  6. int j; // Обратите внимание, КАК распечатываются на экране отдельные символы URL. Если в URL присутствуют. например, русские буквы, то они будут выводиться в виде нечитаемых символов (побайтово). Ибо - кодировка Юникод. В юникоде русская буква (кириллическая) представляется в виде ДВУХ байтов.
  7. for(j=0; j < url.size(); j++){
  8. cout<< url_cons[j] <<"   "<< url[j] <<"   "<<"   "<<url1[j]<<"   " << (url[j] == url1[j])<<"  "<<(url_cons[j] == url[j]) <<'\n';
  9. }
  10.  
  11. // Проверим, на всякий случай, существует ли файл с таким URL (проблема может возникнуть из-за ошибок с кодировками):
  12. FILE* fp;
  13. struct stat sb;
  14. if(stat(url1, &sb) != 0) {
  15. printf("Файла с именем, которое передал браузер, не существует, или имя файла передано неправильно, или имя файла неправильно обработано программой сервером. Передано имя - см. между вертикальными чертами: |%s|\n", url1);
  16. return 0;
  17. }else{
  18. printf("%s \n\n", "Все хорошо, исходный html-файл найден и существует.\n"); // Стало быть, файл можно открывать и работать с ним
  19. }
  20.  
  21.  
  1. printf("%s \n\n", " Итак: ");
  1. // Записываем html во временный файл  
  2. if((fp=fopen("f1.html", "wb"))==NULL) {
  3. printf("Ошибка при открытии файла.\n");
  4. exit(1);
  5. }
  6. char* buf1 = (char*)html.c_str();
  7. fwrite(buf1, html.size()-1, 1, fp); // Следует вначале очистить файл, а потом уже записывать в него, во избежаниме возможных остатков старого содержимого.
  8. //      fputs(buf1, fp);
  9. fclose(fp);
  10. } // Конец проверки ((p1 >= 0) && (p2 > 0))

  1. char message1[] = "HTTP/1.1 200 OK\nDate: Wed, 15 aug 2018 11:20:59 GMT\nServer: Apache\nLast-Modified: Wed, 11 Feb 2009 11:20:59 GMT\nContent-Language: ru\nContent-Type: text/html;\ncharset=utf-8\nConnection: close";  
  2.  
  3. printf("File sent by Server: \n");
  4.  
  5. // Добавляем к сообщению сервера пустую строку
  6. strncat(message1, "\n\n", 4);
  7.  
  8. string name_s;
  9. if(url != "") {
  10. name_s = url;  // Имя исходного html-файла, полученное от браузера НЕперекодированным
  11.  
  12. }else{
  13. name_s = "f_nonadress.html";
  14. }
  15. char* name = (char*)name_s.c_str();

  1. memset (buf, 0, sizeof(buf));
  2. char* buf =  open_read_file(name);
  3.  
  4.  
  5. printf("message1= %s \n\n", buf);
  6.  
  7.  
  8. // Формируем сообщение:  Заголовки сервера + пустая строка + код html(редактируемого файла) + код html(форма+скрипт)
  9. strncat(message1, buf, strlen(buf)*sizeof(char));
  10.  
  11. name_s = "server_saver.html";
  12. name = (char*)name_s.c_str();
  13. memset (buf, 0, sizeof(buf));
  14. buf = open_read_file(name);

  1. strncat(message1, buf, strlen(buf)*sizeof(char));  
  2.  
  3. // Отсылаем сообщение браузеру
  4. send(sock, message1, strlen(message1)*sizeof(char), 0);
  5. if(bytes_read < 0) break;
  6. close(sock);
  7.  
  8. memset (message1, 0, strlen(message1)*sizeof(char));
  9. }
  10. return 0;
  11. }
  12.  
  13.  
  14. // Функция, читающая html-файл
  15. char *open_read_file(char *file_name) {
  16. #define BS 64
  17. size_t bytes_read;
  18. char buf[BS+1], *str; // Что подразумевается, когда делаем +1 ?
  19. ssize_t count;
  20. int i = 0;
  21. FILE *fd = fopen(file_name, "rt"); // Что такое "rt" ? Зачем?
  22.  
  23. memset (buf, 0, sizeof(buf)); // Поэкспериментируйте, попробовав перенести эту строчку непосредственно перед циклом while. Программа может начать работать некорректно. Почему? Неужели порядок, в котором задаем переменные, играет столь важную роль?...
  24.  
  25. // Выделяем BS+1 байтов памяти, для начала
  26. if((str = (char*)malloc(BS+1))==NULL){
  27. perror("Allocation error1.");
  28. exit (0);
  29. }
  30. //  str = (char*)"0"; // Почему эта строчка - ошибочная?
  31. memset (str, 0, sizeof(str)); // А эта чем лучше?
  32.  
  33. // Читаем html-файл до тех пор, пока не дойдем до его конца
  34. while(!feof(fd))  {
  35. i++;
  36. //Считываем не более, чем BS байтов из файла за 1 итерацию цикла
  37. if(fgets(buf, BS, fd))
  38. strncat(str, buf, BS);
  39. // Добавляем еще BS байтов памяти после того, как записали считанные байты в массив str (зачем добавляем байты, кстати? Почему именно столько?)
  40. if((str = (char*)realloc(str, (1+i)*BS+1))==NULL){
  41. perror("Allocation error2.");
  42. exit (0);
  43. }
  44.  
  45. }
  46. if (str == "")  {printf("Not to read from file");}
  47. fclose(fd); return str;
  48. }

Компилируется программа следующим образом:

g++ -o server_saver server_saver.c -std=c++11

Ну, и так, на всякий случай. Желательно бы нынче при компиляции указывать стандарт, в соответствии с которым она производится. За это как раз и отвечает параметр -std. Дело в том, что в разных стандартах некоторые команды языка C/C++ могут вести себя по-разному. Это замечание особенно актуально сейчас, так как этот язык вполне развивается, дополняется.

Начнем разбираться

Так что же, как говорил Сократ. Хотя, особо сложного в этой программе ничего нет, но, тем не менее. Строчки по 33 включительно описывать, я думаю, необходимости нет. Ибо, по-видимому, перед тем, как приступить к изучению этой программы, Вы, конечно же, разобрались с предыдущими заданиями, в том числе с заданием 2, заданием 3, заданием 5. Если нет – обязательно сделайте вначале их. А также с предыдущими, более легкими заданиями, обязательными для выполнения (см. наши методические указания). Там достаточно подробно разбиралась технология работы с сокетами на языках С/С++ в Linux. А данная программа представляет дополнительный, касаемо указанных заданий, интерес, начиная со строчки №35.

Параметры SO_REUSEADDR или SO_REUSEPORT системного вызова setsockopt()

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

Address already in use

Пока система не освободит порт, программа не запустится. Так вот, чтобы избежать этого неудобства, используются строчки 36…40. При этом, кстати, порт, как только закрывается сокет, использующий его, может быть сразу доступен другим сокетам, даже в других приложениях. Это, кстати, я посоветовал бы применять и в других сетевых программах.

Скрыть пояснение
Примечание: Так обстоят дела именно в Linux - эта операционная система, по умолчанию, на некоторое время резервирует только что использованные сокеты, даже если программа уже прекратила выполняться. В других операционных системах, возможно, по умолчанию определено иное поведение.

Строчки 42…51 являются уже известными из предыдущих заданий, заново разъяснять их, видимо, нет необходимости.

Строчка 52 открывает бесконечный цикл (также типичное дело в программах – серверах). В этом цикле, как обычно, используется структура addr, создается рабочий сокет, который, собственно, уже и взаимодействует с браузером.

В строчке 63 происходит считывание данных из сокета. Перед этим переменную buf (куда записываются данные, пришедшие из сети), обнуляем. Если кто не понял, зачем – следует четко выяснить этот вопрос.

После того, как от браузера что-то пришло, выводим все это в консоль (строчка 70), затем преобразуем массив char buf[] в строку типа string. Данных типа string нет в С, но он есть в С++. Делаем это исключительно для удобства последующей работы с buf. Ведь нам надо будет выделить из него границу, значения переданных переменных.

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

Функция (С++) find(), используемая в нижележащих строчках, позволяет находить в строках части других строк (подстроки). Это необходимо для парсинга строк. Они возвращает либо начальное положение подстроки, например, подстроки “POST” (если она присутствует в исходной строке), либо -1 в случае ошибки.

Строго говоря, с функцией find(), равно как и с другими функциями, возвращающими значения типа size_t, связана небольшая проблема. Если брать и напрямую сравнивать ее значение с каким-либо целым числом, например, так:

if(buf_string.find("POST") < 10)

то такой код – некорректен. Он сработает не так, как запланировано в случае, если подстрока POST не будет обнаружена в строке buf_string, т.е. в результате ошибки. Ведь тип size_t не может принимать отрицательные значения, это ведь БЕЗЗНАКОВЫЙ целый тип. Вот в чем дело. Поэтому в случае ошибки значение buf_string.find("POST") будет максимально допустимым для этого типа – это огромное положительное число, триллионы. А вовсе не -1.

Корректный код может быть таким:

int p1 = buf_string.find("POST");
if(p1 < 10)


или таким:

if((int)buf_string.find("POST") < 10)

Вроде бы, мелочь, но я, помнится, потратил на это час с лишним – чтобы понять, почему в некоторых случаях программа выдает странные результаты.

Строчки 76, 77 определяют границы заголовков в сообщении, полученном от браузера. Как известно, в стандарте протокола HTTP концом заголовков является пустая строка, т.е. два подряд перевода строки – символы \r\n\r\n.

Строчка 82 содержит условие, существуют ли подстроки “POST” и "\r\n\r\n" в сообщении, принятом от браузера. Если нет, то это был, как минимум, не POST-запрос. И, следовательно, нет необходимости его анализировать, распарсивать полученное сообщение.

Если условие в строчке 82 выполняется, значит, это был POST-запрос и в нем должны содержаться две интересующих нас строки: URL открытой в браузере страницы в НЕзакодированном представлении (т.е. в обычном своем виде, как есть), а также html код страницы, с учетом произведенного ее редактирования.

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

using namespace string;

Чтобы не указывать его каждый раз. Но, я не стал этого делать – чтобы не смешивать разные пространства имен. Ведь мы уже подключили std. Наверное, никакой некорректной работы и пересечения с пространством string не было бы. Кто его знает. Это – на всякий случай. Хотя, возможно, при присутствии функций, принадлежащих к разным пространствам имен компилятор С++ выдаст предупреждение или ошибку. Точно сказать не могу, лучше не рисковать.

Кстати, точно такая же идеология верна для xhtml, если есть твердое желание обезопасить себя и НЕ использовать одни и те же переменные в разных скриптах, если таковые вдруг будут присутствовать и если они, вдруг, не будут замкнутыми. В конце концов, многие используют javascript-библиотеки, фреймворки разных производителей, могут быть и свои скрипты. Так вот, если использовать для каждого скрипта свое пространство имен xml, будет гарантирована полная независимость переменных (даже с одними и теми же именами) разных скриптов друг от друга. Собственно, это – одна из причин использования xhtml вместо html.

Итак, вначале определяем наличие границы, которая будет разделять области сообщения в POST-запросе браузера. Если такие области есть, то в граница должны содержаться в заголовках. Определить ее значение (знаки «-» и большое целое случайное число можно, если найти в заголовках строчку

Content-Type: multipart/form-data; boundary=

То, что будет идти после, вплоть до конца этого заголовка (символ \r\n) и есть значение границы.

Так как в форме запроса браузера идет поле <input name="url" type="hidden" value="*" />, а уж потом поле <input name="html" type="hidden" value="**" /> (см. файл server_saver.html), то и при отправке значений формы соответствующие строки будут располагаться в том же самом порядке. В этом можно убедиться, посмотрев в консоли то, что выводит сервер после получения запроса браузера. Однако, порядок поиска значений переменных может быть, вообще говоря, произвольным.

Вначале ищем значение, которое передал браузер для поля формы с именем name="html" (строчки 97…99). Аналогично, находим переданный браузером URL (строчки 102…106).

Затем анализируем URL, отбрасывая из него якорь (то, что идет после символа #), а также параметры GET-запроса (то, что идет после первого знака “?”), если они есть. А то, мало ли, вдруг пользователь введет в адресной строке эти символы по ошибке.

Строго говоря, надо бы проводить анализ URL и по другим направлениям, например, на предмет открытия файла с расширением именно html (или htm, xhtml). Ведь если попытаться открыть в «редакторе» файл другого типа, то, скорее всего, ничего хорошего из этого не выйдет. Но, это существенно увеличит громоздкость программы и она потеряет наглядность. Однако, целесообразно это сделать в качестве дополнительного задания (см. ниже).

Далее, после анализа URL и удаления из него всего того, что, быть может, начинается с символов “#”, “?”, проверяем, а существует ли файл, который фигурирует в URL. В самом деле, ведь браузер может передать URL с ошибкой (вспоминаем также про декодирование) или сервер может определить его как-то неправильно. Если файла такого нет, следует сообщить об этом и прервать выполнение сервера (строчки 133…140).

Строчки 145…153 производят запись html-кода, полученного в запросе (сообщении) браузера, в файл с именем f1.html. Это – сохранение выполненных на открытой в браузере странице изменений в файл на жестком диске. При каждом сохранении, изменения будут записываться в этот файл, обновляя его. Однако, вначале файл может быть больше, потом меньше и т.п. В итоге, если не принять мер, помимо нового содержимого (если оно имеет меньший размер) в файле f1.html будет присутствовать старое содержимое. Поэтому следует устранить такую возможность. Это можно достигнуть, например, путем очистки файла f1.html перед записью в него. Или, как вариант, можно каждый раз перед записью создавать новый файл.

Далее – формируем сообщение сервера. Вначале делаем заголовки (строчка 154). Добавляем к получившемуся сообщению пустую строку (строчка 159).

Затем проверяем, присутствовало ли в сообщении браузера имя файла. Если это было первоначальное открытие дополнительной вкладки с URL вида http://127.0.0.1:3425/.../f.html, которая открылась путем нажатию на кнопку «Перейти к редактированию этой страницы» на вкладке, открытой по протоколу FILE (URL вида file:///.../f.html), то при этом URL НЕ передается. Он будет передан после того, как открывшаяся страница загрузится полностью, сработает событие onload в ее теге body (см. выше) и страница сама, без участия пользователя, отправит данные формы серверу (при помощи обработчика subm_form()).

Поэтому, при самом первом открытии вкладки по протоколу HTTP, если браузер еще не передал имя файла (соответственно, переменная url будет равна "", т.е. пустой строке), то устанавливаем его по умолчанию равным f_nonadress.html. И, затем, в строчке 170 при помощи функции open_read_file() происходит считывание содержимого этого файла.

Обратим внимание: сервер потом пошлет содержимое ЭТОГО файла браузеру. Несмотря на то, что в URL в его адресной строке будет фигурировать файл f.html.

Впоследствии, пре следующем нажатии на кнопку «Сохранить» браузер пошлет серверу имя файла (f.html), соответственно, переменная url уже не будет пустой. А раз так, то (см. строчки 82…153) она как раз будет равна его полному имени. Соответственно, функция open_read_file() будет открывать уже не f_nonadress.html, а f.html.

Наконец, добавляем к сообщению сервера содержимое файла server_saver.html, которое представляет собой html-код формы (с кнопкой «Сохранить») и скрипт-обработчик для заполнения ее полей. Также этот скрипт переводит открытую в браузере страницу в режим редактирования.

После этого сервер производит отправку сообщения браузеру (строчка 186).

В строчке 187 проверяется сокет на предмет ошибки. Так, если возникла ошибка, функция recv() (см. строчку 63) вернет значение, равное -1. Если ошибки нет, в переменную bytes_read будет записано количество байт, которые функция recv() причитала из сокета sock. Однако, может так статься, что количество прочитанных байтов будет равным… нулю.

Это – особый случай. Так как сокет создавался для ТСР соединения (в функции socket() указан параметр SOCK_STREAM), то количество байтов может быть равно нулю, если… в самом деле ничего не было передано. Это может быть тогда, когда процесс «тройного рукопожатия» в рамках установления ТСР-соединения произошел, а вот само сообщение так не и не было передано.

Как я установил опытным путем, браузер (Firefox 61 в Linux) активизирует ТСР-соединение, готовя сокет к отправке данных, например, при наведении указателя мыши на ссылку, имеющуюся на странице. Даже если для нее не зарегистрирован никакой обработчик javascript. По всей видимости, если пользователь навел указатель мыши на ссылку, браузер заранее готовится к передаче запроса на сервер (при этом будет передан html-код страницы, открываемой по ссылке). НО: если на ссылку мышь просто навести, но не нажимать ее, очевидно, никакого перехода по ней не произойдет. Соответственно, данные серверу направляться не будут. Видимо, браузер действует так, чтобы заранее, пока пользователь кликнет по ссылке, подготовить сокет к передаче данных. Сделано это, скорее всего, для снижения задержек времени при открытии ссылок, в целях оптимизации.

Так вот, в таких условиях, браузер, подождав, через несколько секунд закрывает соединение, передает серверу (на низком уровне, в рамках технологии «тройного рукопожатия») специальный флаг. А сервер при этом, соответственно, считает, что передача данных закончена (даже если вообще ничего и не было передано). А раз так, это приводит к срабатыванию функции recv(), которая при этом возвращает, естественно, 0.

Примечание. Если бы использовался протокол UDP, а не TCP, то подобная ситуация привела бы к отправке пустой дейтаграммы.

Именно поэтому при наведении мыши на ссылки и также при некоторых других действиях пользователя происходит закрытие передающего сокета sock (внимание: управляющий сокет listener так и остается открытым) и срабатывание функции recv(). При этом сервер дублирует передачу в консоль параметров, вычисленных на предыдущей итерации цикла (см. строчку 52) и, дойдя до строчки 188, закрывает сокет (по которому на этот раз данные не пришли). Затем начинается новая итерация цикла, вновь открывается сокет, ожидающий приема/передачи следующих данных.

server_saver.html

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

  1. <div class="asdfghj">
  2. <!--<a id="a_to_go_edit" href="" target="_blank">Вызов</a> -->
  3. <form id="asdfghj" style="width:450px; border:solid 2px; background-color:green; text-align:center; position:fixed; top:40%; opacity:0.94; padding-bottom:2px" name="form_1" method= "post" enctype ="multipart/form-data" onsubmit="f() " action="">
  4. <fieldset>
  5. <legend style="" contenteditable="false" title="Именно эту область редактировать невозможно" onclick="alert('Редактирование этой надписи запрещено, чтобы случайно ее не испортить.');">Вы можете редактировать эту html-страницу. Для сохранения изменений нажмите кнопку Сохранить</legend>
  6. <input name="url" type="hidden" value="*" /><input name="html" type="hidden" value="**" />
  7. <input type="submit" name="go_to" value="Сохранить" /><br />
  8. </fieldset>
  9. </form>
  10. <script type="text/javascript ">
  11. // <!-- CDATASection[[ -->
  12. document.getElementsByName('url')[0].value = "*"; // Задаем произвольные параметры, чтобы при очередной перезагрузке страницы НЕ сохранялись ранее заданные (см. ниже)
  13. document.getElementsByName ('html')[0].value = "**";
  14. document.getElementsByName ('form_1')[0].action = document.location;
  15. // document.getElementById ('a_to_go_edit').href = document.location;

  1. var doc_loc = document.location.toString();
  2. var x = doc_loc.substr(0,4);
  3. if(x == 'file'){
  4. if(doc_loc.search(/file:\/\/\//) === 0){
  5. x = 3;
  6. }else{
  7. x = 2;
  8. }
  9. doc_loc = doc_loc.substr("file:".length+x);
  10. }else{
  11. document.body.setAttribute ("contenteditable", "true");
  12. document.getElementById('a_to_go_edit'). parentNode.removeChild(document .getElementById('a_to_go_edit'));
  13. }

  1. function f(){ // Меняем параметры value у тегов с именами(name): 'url' и 'html' - для того, чтобы потом, когда нажмем кнопку формы "Сохранить", передать значения этих параметров нашей программе-серверу, открывшей сокет на порту 3425
  2. document.getElementsByName('url')[0].value = decodeURI(document.location); // Текущий URL. Декодируем, иначе русскоязычные буквы в URL передадутся в виде %D0%A0%D0%B0 и т.п. Но, если бы сервер бы НЕ наш самописный и, скажем, располагался бы где-нибудь на хостинге, то декодировать, в общем случае, НЕЛЬЗЯ. В общем случае, следует раскодировать сообщение на сервере.

  1. var docum = "";
  2. while(document.getElementsByClassName­('asdfghj').length){
  3. var div_classname = document.getElementsBy­ClassName('asdfghj')[0];
    1. if(div_classname.nodeName.toLowerCase() == "div"){
  4. docum = div_classname.parentNode.removeChild­(div_classname);  // Временно запоминаем содержимое тегов <html>...</html>
  5. }else{
  6. div_classname.parentNode.­removeChild(div_classname);
  7. }
  8. }

  1. document.body.removeAttribute ("contenteditable");

  1. var doc1 = document.documentElement.­outerHTML.toString();// Берем все то, что находится между тегами <html> ... </html>, за исключением формы отправки сообщения и скрипта для нее
  2. document.body.appendChild(docum);
  3. document.getElementsByName ('html')[0].value = doc1;
  4. document.body.setAttribute ("contenteditable", "false"); // Иначе кнопка отправки сообщения не сработает
  5. }
  6. // <!-- ]] -->
  7. </script>
  8. </div>

Строчки 3…6 как раз и задают ту самую форму сохранения зеленого цвета, код которой сервер дописывает всякий раз в сообщение, отправляемое браузеру. Для удобства, форма сделана находящейся неподвижно на одном месте экрана. Конечно, ее параметры можно легко изменить. Например, можно сделать ее перемещаемой по экрану при помощи мыши. Можете с этим поэкспериментировать.

Строчки 7…38 представляют собой скрипт JS по управлению формой. Как уже говорилось, в задачу скрипта входит:

Задание параметров тегов <input /> по умолчанию (строчки 10, 11);

Получение текущего URL открытой в браузере страницы и преобразование его в строчный вид (строка 13). Несмотря на то, что javascript самостоятельно преобразует переменные (да, это не С), тем не менее, в некоторых случаях он не всегда делает это корректно. Поэтому, для корректности, все-таки применена функция toString();

Берем первые 4 символа из URL – анализируем. по какому протоколу открыта страница, по протоколу FILE или по какому-то другому (строчка 15). Функция search() ищет в строке doc_loc совпадение с регулярным выражением вида file:///. Дело в том, что в некоторых операционных системах в URL может присутствовать только два слеша / вместо трех;

Строчка 18 переводит открытую в браузере страницу в режим редактирования. После этого ее можно исправлять, как обычный текст;

В строчках 21…36 представлен обработчик f() полей формы при отправке ею данных. Он срабатывает после нажатия на кнопку «Сохранить». После того, как он окончит работу, браузер считывает данные из полей формы и отправляет их серверу;

Задача обработчика – вырезать из поля формы с именем name="html" код формы, ссылку «Перейти к дедактированию» и, собственно, сам скрипт и только после этого дать возможность браузеру отправить сообщение. В самом деле, ведь сервер не должен получить вырезанный контент, иначе он сохранит его в файле f1.html. Именно поэтому блоку, содержащему форму и скрипт назначен класс asdfghj (символы, на самом деле, могут быть произвольными). При этом в html-коде открытой странице НЕ ДОЛЖНО БЫТЬ других элементов, имеющих такой класс. Поэтому вместо asdfghj лучше бы назначить какое-то большое строковое значение, типа fvjkh54t65ihjbnb, чтобы гарантировать, что в html-коде оно не встретится. Конечно, здесь надо бы сделать соответствующую проверку, для надежности.

После того, как в поле тега <input /> с именем name="html" записан html-код страницы, за исключением формы отправки сообщения и скрипта для нее, необходимо отключить режим редактирования страницы, установив contenteditable равным false. Иначе кнопка отправки формы не сработает. Ничего страшного, ибо при получении сообщения от сервера страница будет обновлена и на ней вновь сработает, в частности, строчка 18.

Таким образом, вначале скрипт считывает все содержимое тегов <html>…</html> открытой страницы, вырезает из DOM служебный код, сохраняя его в переменной docum. А то, что осталось, сохраняется в переменной doc1. После чего служебный код ВНОВЬ вставляется на страницу, а значение переменной doc1 сохраняется в поле тега <input /> с именем name="html".

Наверное, возможен и иной, более простой алгоритм. Например, было бы проще удалить служебный код из переменной, в которую считано все содержимое тегов <html>…</html>, при помощи регулярного выражения. Но, так как html-код содержит переводы (концы) строк, регулярные выражения не позволяют работать правильно в javascript. Ну, у меня, по крайней мере, не получилось. Это вот в РНР проблемы с этим нет, все решается указанием флага много- или однострочного режима для регулярного выражения. Но, не в JS. Поэтому пришлось работать с DOM.

f_nonadress.html

Это – файл для начальной страницы, содержимое которой передается браузером серверу при первом открытии дополнительной вкладки с URL вида http://127.0.0.1:3425/.../f.html:

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html  xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4. <title>Site</title>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  6. </head>
  7. <script class="asdfghj" type="text/javascript ">
  8. document.getElementById('a_to_go_edit').href = decodeURI(document.location);
  9. var doc_loc = decodeURI(document.location).toString();
  10. var x = doc_loc.substr(0,4);
  11. if(x == 'file'){
  12. if(doc_loc.search(/file:\/\/\//) === 0){
  13. x = 3;
  14. }else{
  15. x = 2;
  16. }
  17. doc_loc = doc_loc.substr("file:".length+x);
  18. document.getElementById('a_to_go_edit').href = "http://127.0.0.1:3425/" + doc_loc;
  19. }else{
  20. document.body.setAttribute("contenteditable", "true");
  21. }
  22.  
  23. function subm_form(){ // Эта функция необходима для отправки полей формы на странице, когда на нее попали САМЫЙ ПЕРВЫЙ  раз (со страницы с протоколом file:// ). Она вызывает обработчик полей формы и отправку ее полей программно, т.е. без участия пользователя (чтобы ему не делать лишнее нажатие на кнопку "Сохранить"). Отправка полей формы нужна для того, чтобы передать серверу URL открытой страницы, а он оттуда извлечет абсолютный путь к редактируемому файлу, после чего откроет его и т.д.
  24. f(); // Принудительно вызываем тот же самый обработчик onsubmit формы, т.к. ЗДЕСЬ отправляем ее данные программно, при этом обработчик, приведенный в input type="submit" cам по себе НЕ сработает.
  25. alert("Нажмите ОК");
  26. document.getElementById('asdfghj').submit();
  27. alert("ОК"); // Почему не срабатывает эта команда?
  28. }

  29. /*    var xm = new XMLHttpRequest();
  30. xm.open("POST", "", true);
  31. xm.onload = function (){
  32. alert( xm.responseText);
  33. }
  34. var mess = "url=" + doc_loc;
  35. xm.send(mess);
  36. */  

  37. </script>
  38. <body onload="subm_form()">
  39. <p>Нажмите кнопку "Сохранить" (точнее, он должна сейчас нажаться сама, т.е. программно). После этого откроется страница.</p>
  40. </body>
  41. </html>

Здесь, вроде бы, все понятно.

Закомментированный, немного недоработанный, код (на случай) позволяет делать AJAX-запрос. Попробуйте его реализовать. Хотя, следует иметь в виду, что это - самае примитивная форма AJAX. Она не включает в себя контроль событий, которые реализуются в процессе возвращения сервером результатов AJAX. В частности, имеется в виду событие onreadyStateChange.

Букмарклет

Для того, чтобы на локальной странице, открытой в браузере по протоколу FILE, разместить скрипт, можно использовать, по идее, разные способы. Некоторые браузеры (но, не все) позволяют это делать, используя так называемые UserScripts (пользовательские скрипты, которые можно подгружать на страницу)). Самое простое, на мой взгляд, универсальное решение - это использование букмарклета. Который представляет собой код javascript, сохраненный в виде закладки браузера. Соответственно, он будет срабатывать каждый раз при нажатии закладки. Срабатывать он будет на той странице, которая открыта в браузере в этот момент.

Есть генератор букмарклетов, можно было бы воспользоваться им. Но, увы, данный генератор работает с ошибкой: он принимает два слеша, идущие подряд // , идущие в команде с использованием регулярного выражения, за комментарий в коде JS… (в самом деле, один из способов комментирования в javascript как раз и основан на простановке двух слешей). Поэтому при совершенно правильном JS-коде он показывает, якобы, "ошибку":

Окно сервиса Генератора букмарклетов

Как видно, символы //) === 0){ генератор воспринимает, как комментарий, серым цветом их обозначает. Видимо, это – в силу того, что регулярное выражение у нас – «особенное», оканчивающееся на слеш, а генератор явно не готов правильно обработать такое регулярное выражение.

И, что плохо, перенос строки-то сделать нельзя, ведь – букмарклет. Пробел между слешами – тоже делать нельзя, ибо тогда соответствующим образом изменится регулярное выражение. Что же, придется делать букмарклет вручную. Ну, в принципе, можно, конечно, вначале сделать пробел между слешами, сформировать букмарклет, вставить его в закладки, а потом уже ТАМ убрать пробел. Правда, найти его там – проблема: ведь он представлен в виде одной строки, а она - достаточно длинная, да еще и без пробелов.

Кстати, это – характерный пример того, что на генераторы/библиотеки/фреймворки/CMS/… - надейся, а сам(а) – не плошай. В переводе на более понятный язык это звучит следующим образом: забываем про генераторы и т.п., делаем сами, как полагается.

Ведь, в самом деле, несложно создать букмарклет и вручную из любого кода JS. Правда, там есть ограничение на объем кода, но для нас, в данном случае, это неактуально, так как кода-то – совсем ничего. Основные правила создания букмарклетов из кода javascript:

  • Код JS должен представлять собой ОДНУ строку;
  • В коде не должно быть пробелов. Если они все-таки необходимы (например, в конструкциях типа var x=123;), то их следует заменить на символы %20.

Напишем html код того, что должно добавляться на страницу, которую будем редактировать:

  1. <a id="a_to_go_edit" class="asdfghj" style="background-color: rgb(15,230,0); padding:10px" href="" target="_blank">Перейти к  редактированию этой страницы</a>
  2. <script class="asdfghj" type="text/javascript ">
  3. var doc_loc = decodeURI(document.location).toString();
  4. var x = doc_loc.substr(0,4);
  5.   if(x == 'file'){
  6.     if(doc_loc.search(/file:\/\/\//) === 0){
  7.     x = 3;
  8.     }else{
  9.     x = 2;
  10.     }

  11. doc_loc = doc_loc.substr("file:".length+x);
  12. document.getElementById('a_to_go_edit').href = "http://127.0.0.1:3425/" + doc_loc;
  13.   }else{
  14. document.body.setAttribute("contenteditable", "true");
  15.   }
  16.   </script>

Со скриптом, надеюсь, проблем не возникает, там все понятно. Однако, нам, помимо скрипта, необходимо добавить еще и кнопку (представляющую собой ссылку). В букмарклет ссылка сама по себе не войдет, так как она не является кодом JS. Стало быть, придется создать эту ссылку средствами JS. Как увидите ниже, для этого придется писать много строчек кода вместо, буквально, одной. Но, выхода, по-видимому, иного нет. Итак, для того, чтобы средствами javascript нарисовать ссылку

  1. <script class="asdfghj" type="text/javascript ">
  2. // Вначале создаем кнопку для открытия дополнительной вкладки
  3. var a = document.createElement('a');
  4. a.setAttribute('id', 'a_to_go_edit');
  5. a.setAttribute('class', 'asdfghj');
  6. a.style.backgroundColor = 'rgb(15,230,0)';
  7. a.style.position='absolute';
  8. a.style.left=0;
  9. a.style.top=0;
  10. a.style.padding = '10px';
  11. a.setAttribute('href', '');
  12. a.target = '_blank';
  13. a.textContent = 'Перейти к  редактированию этой страницы';
  14. document.body.appendChild(a);
  15. </script>

Осталось только объединить оба скрипта в один. Естественно, вначале должен идти код, рисующий ссылку и задающий для нее стиль и т.п. А потом уже – код, преобразующий ее document.location. Ибо вначале на странице должен быть код, задающий сам элемент (ссылку, в данном случае), а потом - скрипт, обрабатывающий его.

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

javascript:(function%20(){var%20a=document.createElement('a');a.setAttribute('id','a_to_go_edit');a.setAttribute('class','asdfghj');a.style.backgroundColor='rgb(15,230,0)';a.style.padding='10px'; a.style.position='absolute';a.style.left=0;a.style.top=0;a.setAttribute('href',''); a.target='_blank';a.textContent='Перейти%20к%20редактированию%20этой%20страницы';document.body.appendChild(a); var%20doc_loc=decodeURI(document.location).toString();var%20x=doc_loc.substr(0,4);if(x=='file') {if(doc_loc.search(/file:\/\/\//)===0){x=3;}else{x=2;}doc_loc=doc_loc.substr("file:".length+x); document.getElementById('a_to_go_edit').href="http://127.0.0.1:3425/"+doc_loc;}else{document.body.setAttribute("contenteditable","true");}})()


Как видим, код JS для букмарклета представляет собой замкнутую безымянную функцию, запускающуюся по протоколу javascript: при клике на закладку с букмарклетом.

Примечание 1. Псевдопротокол javascript: вызывает обработку того кода, который идет справа от него, как кода javascript. Иными словами, он просто запускает его на выполнение.


Примечание 2. Вообще-то, (особенный) элемент html-разметки под названием <script> НЕ ИМЕЕТ, по стандартам, такого атрибута, как class. Но, тем не менее, браузер хорошо работает с этим атрибутом. Конечно, правильнее было бы поместить этот скрипт в блок <div>…</div>, а вот этому блоку уже назначить класс. Но, честно говоря, я обратил внимание на это слишком поздно, переделывать уже не хотелось. Но, следует иметь в виду, что это - недокументированное использование атрибута class, которое может по-разному восприниматься различными браузерами. Впрочем, в этих "различных браузерах" и документированные особенности, иной раз, реализованы неверно.

Теперь следует создать вкладку и поместить в нее букмарклет. Вот  для этого может пригодиться генератор букмарклетов. Удерживая мышью надпись «Сlick Me», переносим ее в закладки и устанавливаем на нужное место:

Вид букмарклета в закладках

Кликаем ПРАВОЙ кнопкой мыши на вставленном букмарклете и получаем возможность изменить его название и свойства:

Задаем параметры букмарклета

Я сделал ему название «Перевести страницу в режим редактирования», в соответствии с текстом ссылки (кнопки), которую букмарклет создаст на странице после его нажатия.

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

Итак, если Вы все сделали правильно, при клике на закладку с букмарклетом на странице в левом верхнем углу должна появиться зеленая ссылка (кнопка) с надписью «Перейти к редактированию этой странице». Последующие действия описаны выше.


Тестовый файл f.html

Название файла взято лишь для примера, Вы можете задать какое-либо другое. Желательно, чтобы этот файл был небольшим, чтобы не было сложности в анализе работы «редактора». Целесообразно поэкспериментировать, к примеру, с таким файлом f.html:

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html  xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4. <title>Site</title>
  5. <!-- Комментарий  1 -->
  6. И текст без разметки (1)
  7. <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  8. </head>
  9. <body>
  10. <!-- Комментарий  2 -->
  11. Текст без разметки (2)
  12. <p>Hello World!</p>
  13. <br/><br/>
  14. <br/>
  15. <!-- Комментарий  3 -->  
  16. Это третий текст без разметки (3)
  17. </body>
  18. <!-- Комментарий  4 -->
  19. Это еще один текст без разметки (4)
  20. </html>
  21. <!-- Комментарий  5 -->
  22. А это - последний, за пределами тегов html(5)

А теперь на РНР

Рассмотрим программный код «сервера» на PHP, выполняющий аналогичные функции. Точнее, сервер-то уже есть готовый – Apache, к примеру. Более того, есть и его локальная оболочка (интерфейс) под названием «виртуальный сервер Denwer». Итак, рассмотрим код РНР, выполняющий практически те же самые функции, что и server_saver.c.

Файл server_saver.php:

  1. <?php
  2. $file_name = $_REQUEST['name'];
  3. $html = $_REQUEST['html'];
  1. $serv_name = $_SERVER['SERVER_NAME']; // Ищем имя сервера (например, 4846d.ru) в параметре name, переданном браузером
  2. if( (strval(strpos($file_name, "/".$serv_name."/"))) == ""){
  3. die('There is an error1 in Your redactor\'s html-files. Editing is impossiple.');
  4. }
  5. $pos = strpos($file_name, "/".$serv_name."/");
  1. if( (strval(strpos($file_name, "/".$serv_name."/www/"))) == ""){
  2. $www_letters = 1;
  3. }else{
  4. $www_letters = 5;
  5. }
  1. $file_name = substr($file_name, $pos + strlen($serv_name) + $www_letters);
  2. // Полное имя файла в виртуальном сервере
  3. $file_name =  $_SERVER['DOCUMENT_ROOT'] . $file_name ;
  1. // Читаем исходный файл f.html и выводим его на экран
  2. $fp = @fopen($file_name, "rb") or die("Error2 open file f.html");
  3. $f_str = '';
  4. while (!feof($fp)){
  5. $f_str = $f_str . fgets($fp);
  6. }
  7. fclose($fp);
  8. $f_str = mb_convert_encoding($f_str, 'cp1251', 'utf-8' ); // Преобразуем кодировку
  9. echo $f_str;
  1. // Читаем файл, содержащий форму для сохранения отредактированной страницы + скрипт для управления ею и тоже выводим на экран
  2. $fp = @fopen("server_saver.html", "rb") or die("Error3 open file server_saver.html");
  3. $st = '';
  4. while (!feof($fp)){
  5. $st = $st . fgets($fp);
  6. }
  7. fclose($fp);
  8. $st = mb_convert_encoding($st, 'cp1251', 'utf-8' );
  9. echo $st;
  1. // Формируем новое имя файла
  2. $new_file_name = basename($file_name);
  3. // Определяем имя нового файла, вида f1.html, с расширением (исключая полный путь)
  4. $base_nam = basename($file_name, ".html");
  5. // Новое имя пока без расширения
  6. $new_file_name = preg_replace('/(\.html|\.htm)/i', '', $new_file_name) . '1.' .
  7. // Расширение (берем от предыдущего файла f.html)
  8. preg_replace("/(.*?)(html|htm)/i", '$2', $new_file_name);
  9. // В итоге получили новое имя:  "старое имя"+"1."+"расширение". Теперь надо его преобразовать в полный путь
  1. $new_file_name_full = dirname($file_name) . '/' . $new_file_name;
  1. // Ну, и, наконец, сохраняем изменения, присланные браузером, в файле f1.html - в том же каталоге, где находится f.html
  2. $fp = @fopen($new_file_name_full, "wb") or die("Error4 open file server_saver.html");

  1. // Пишем в файл с новым именем то (подразумевается значение переменной html), что прислал браузер при POST-запросе
  2. fwrite($fp, $html);
  3. fclose($fp);
  4. ?>

Как видим, код отдает какой-то простотой и компактностью. Судя по строчкам, объем его в несколько раз ниже, чем на С/С++. Например, функция построчного чтения файла на С/С++ заняла 33 строчки, а на РНР – всего 6. А делает этот код – практически то же самое, что и сервер server_saver.c. Ну, с небольшими отличиями.

Это и естественно. Не нужно создавать сокет, задавать его параметры. Не нужно проводить синтаксический анализ (парсинг) сообщений браузера. Все это за нас теперь делает интерпретатор РНР в Denwer. Да и чтение/запись в файл выглядят гораздо компактнее.
Честно говоря, код делался на скорую руку. Поэтому, конечно, он далек от идеальности. Возможно, в целях повышения производительности целесообразнее было, к примеру, вместо регулярных выражений использовать обычные функции поиска по строке, а то и попросту функцию pathinfo(). Но, это уже детали; если желаете, можете их усовершенствовать. В частности, ну, ОБЯЗАТЕЛЬНО надо бы проверять то, что присылает браузер, во избежание переполнения буфера, по крайней мере. Даже если Вы не будете умышленно делать переполнение путем ввода «особенных» параметров в адресной строке браузера, надо понимать, что за Вас это может сделать какая-нибудь не очень хорошая программа, полученная с какого-нибудь сервера в интернете или скрипт (ведь, по сути, данный код на РНР обходит запрет на обращение к локальным файлам компьютера со стороны javascript). После чего соответствующий код может получить определенную свободу на вашем компьютере.

Прочие файлы

Как и в проекте на С/С++, нам потребуются вспомогательные файлы. Во-первых, понадобится файл server_saver.html. Его следует взять один к одному, как есть. Так как там лишь html и javascript, на серверную часть он не влияет.

Файл f_nonadress.html уже не понадобится.

А вот букмарклет придется изменить. Это связано с разными видами путей к файлам в самой файловой системе и в Denwer по протоколу http (сравните ниже). В С/С++ с этим проблем не было. Предлагаю изменить букмарклет самостоятельно – так, чтобы при нажатии на него он формировал, как обычно, кнопку-ссылку, адрес (href) для которой был бы примерно следующего вида:

http://4846d.ru/server_saver/server_saver.php?name=C:/Denwer/home/4846d.ru/www/server_saver/f.html

здесь f.html, как обычно, имя файла, из которого делается запрос к серверу, т.е. файл, который собираемся редактировать в окне браузера (в визуальном режиме);

4846d.ru – хост на виртуальном сервере (вполне можно использовать и localhost, к примеру);

server_saver – каталог, в котором хранится файл.

Для этого потребуются хотя бы минимальные познания в javascript. На первое время можете жестко прописать в букмарклете подобный адрес.

Как видим, адрес несколько изменился по сравнению с тем, что использовался в программе на С/С++. До знака равенства появилось имя программы на РНР (server_saver.php) с параметром name. Дело в том, что, на самом-то деле, достаточно было бы указать лишь номер порта, к которому привязан соответствующий сокет – и все. Однако, в случае РНР подразумевается, с одной стороны, стандартный http-порт. С другой стороны, программ на РНР, которые могут запускаться при появлении сообщения на этом порту, может быть не одна, а много. Наконец, что наиболее важно, РНР не содержит в себе команд, позволяющих обращаться к ТСР-сокетам напрямую.

Примечания

Файлы следует разместить в каком-нибудь каталоге того или иного виртуального хоста в Denwer. Запускаем – как обычно: вначале открываем локально какой-нибудь файл html, затем вызываем букмарклет из закладки, далее – обновляем страницу. И – можем редактировать. Изменения, сделанные при редактировании, будут сохраняться в файле f1.html, расположенном в том же каталоге.


О пользе для вебпрограммистов

Где и чем может быть полезна такая технология для вебпрограммиста? Казалось бы, типичному вебпрограммисту, якобы, необязательно интересоваться ТСР-сокетами и прочими "дебрями", как оно можно прочитать на разных форумах. Ответ такой: типичному, да, необязательно. Ему "вполне хватит" каких-нибудь jQuerry/Motools/Yii/WordPress/... . Дай бог бы, хоть там он разобрался и сделал что-то стоящее. А вот продвинутому весьма полезно было бы знать поболее; в частности, как раз ему и необходима технология сокетов. Хотя бы для того, чтобы делать FastCGI приложения. Для нагруженных сайтов, когда мощности того же PHP уже не хватает. Такие задачи возникают, как правило, не при каждом запросе из сети, а, к примеру, для служебных целей. Для упорядочения и индексирования данных пользователей, контроля за файловой системой сервера (хостинга) и др. Конечно, повторюсь, для небольших сайтов вполне хватит РНР, Python и т.п. Но, когда речь идет о миллионах файлов и соответствующем количестве пользователей - там высокоуровневые языки если и возможно использовать, то с оговорками. Разместив наше программу-сервер где-нибудь на (другом) хостинге, можно будет делегировать ему соответствующие задания.

Задание для самостоятельной работы:
  1. Создайте букмарклет, записав его в закладку браузера.
  2. Сформируйте проект, запустите букмарклет, сервер и протестируйте. Детально разберитесь во всем. Примечание: если в рамках наших с Вами занятий Вы НЕ изучаете html/javascript, то работу скриптов достаточно будет изучить на общем, поверхностном уровне. Хотя, если есть желание, я могу обсудить и html/javascript тоже. То же касается и PHP.
  3. Сравните исходный файл f.html и тот, что получится после сохранения сервером. Чем вызвана разница? Как можно было бы ее устранить?
  4. Устраните такой недостаток «редактора», как отсутствие DOCTYPE в сохраненном файле f1.html.
  5. Сформулируйте развернутые ответы на вопросы по программе-серверу (как минимум, это - строчки 82, 117, 200, 203, 221).
  6. Напишите процедуру, которая, после того, как файл f1.html будет удачно сохранен, перепишет его содержимое в исходный файл f.html.
    Примечание: в начале работы «редактора» в файл f1.html будет записано содержимое файла f_nonadress.html, а не f.html! При этом запись в f.html осуществляться не должна. Т.е. перед записью следует делать анализ ее целесообразности.
  7. Сделайте проверку, существуют ли в исходном файле f.html элементы с классом или идентификатором asdfghj. Если да, то сервер должен выдавать соответствующее сообщение о невозможности сохранения изменений при редактировании. При этом фон кнопки формы сохранения вместо зеленого должен стать красным.
  8. Допишите в программе-сервер (только в основных местах) процедуры проверки данных и обработку возможных ошибок. Особое внимание обратите на места, где возможно переполнение памяти.

С уважением, Салимоненко Д.А.