Сегодня речь вновь пойдет о замечательном языке серверных приложений — о PHP. Я покажу, как на нем можно написать, ни много ни мало, программу чтения USENET-новостей.

Что такое USENET? Это бесконечное пространство для общения (подробнее см. статью Валерия Аксака «Интернет-андеграунд + ФИДО = Usenet» в МК 39(158)). Чтобы подробно узнать, «как туда попасть», можно открыть справку Outlook Express и зайти в раздел «Работа с группами новостей». Я же дам краткую характеристику. В USENET есть огромнейшее количество групп новостей (как их называют, эхо-конференций, форумов). Стоит заметить, что «новости» — это лишь формальное название, которое не имеет отношения к УНІАН. Все группы строго разделены по тематике. Пользователи, подписываясь на определенный форум, получают возможность читать письма других участников и самостоятельно отправлять письма в группу. Поскольку новости читает большое количество людей, вы можете быстро получить ответ на возникший вопрос и так же быстро испытать моральное удовлетворение, помогая найти ответы другим.

Работает все это чудо по протоколу NNTP (Network News Transfer Protocol — Протокол Передачи Сетевых Новостей). Полное описание данного протокола можно найти по адресу http://www.w3.org/Protocols/rfc977/rfc977.html.

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

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

1. Открыть соединение, или, как говорят, TCP Socket (socket — розетка);

2. При необходимости идентифицироваться на сервере — передать логин и пароль;

3. Получить информацию о конкретной группе или нескольких группах. Что именно? Список групп, доступных на сервере, количество новых статей (или их общее количество) в выбранной группе.

4. При необходимости можно загрузить все сообщения или только их заголовки (поля «От:», «Тема:» и т.д.). Можно запостить письмо.

5. В конце всех действий нужно закрыть соединение (сокет).

Вот, в принципе, и все действия, к которым сводится процесс общения с сервером. Но проще сказать, чем сделать.

Теперь надо решить, чего мы ожидаем от нашего скрипта.

Поскольку скрипт, который я буду здесь описывать — простенький (но функционирующий) пример, он будет всего лишь уметь писать и отправлять письма в конференцию. Приступим к созданию html-страницы, из которой будет вызываться наш скрипт.

<html> <head> <title>USENET — Клиент</title> </head> <body> <form action="nntpclient.php"> Сервер: <input type="text" name="ServerName"><br> Логин: <input type="text" name="UserLogin"><br> Пароль: <input type="password" name="UserPass"><br> Имя группы новостей: <input type="text" name="GroupName"><br> Получить последних <select name="MessLimit"> <option>5</option> <option>10</option> <option>15</option> </select> сообщений<p> Информация для отправляемого сообщения:<br> От: <input type="text" name="FromName"><br> Тема: <input type="text" name="Subject"><p> Текст сообщения:<br> <textarea name="PostedMess" rows=10> </textarea><p> <input type="submit" value="Готово"><input type="reset" value="Очистить"> </form> </body> </html>

Форма вполне стандартна, но тем не менее я опишу ее поля:

ServerName — сюда пользователь будет вводить имя (или IP-адрес) сервера USENET;

UserLogin, UserPass — логин и пароль пользователя на сервере, соответственно.

GroupName — определяет имя группы новостей, из которой будут браться сообщения для отображения и куда будет отправлено сообщение, написанное пользователем;

MessLimit — ниспадающий список из трех пунктов (5/10/15). В нем выбирается количество сообщений, которые должны быть отображены в браузере.

Следующий блок полей определяет заголовки исходящего письма, а также его текст.

В конце две стандартные кнопки Reset и Submit. При нажатии на последнюю данные из формы пересылаются в скрипт, который находится в файле nntpclient.php.

Теперь, когда форма готова, осталось написать сам скрипт. Как уже говорилось, он должен быть в файле с именем nntpclient.php и находиться в той же папке, что и файл с формой. Впрочем, если это не так, то нужно указать соответствующий путь в параметре action тэга form.

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

<title>Результаты</title> <? Начинаем код на PHP. if(!$nntpsock=fsockopen($ServerName, 119)) {echo "Не удалось установить соединение!!!"; exit; }

Функция fsockopen() работает практически аналогично функции fopen(), которая предназначена для открытия файлов. Разница в том, что fsockopen() применяется для установления соединения с удаленным компьютером. В качестве первого параметра указывается либо имя сервера, либо его IP-адрес. Второй параметр — это порт, к которому нужно подключится. Все NNTP-серверы работают на 119-м порту, который мы и указали в качестве параметра. Полный вид функции вот такой: int fsockopen (string hostname,int port [, int errno [, string errstr [, float timeout]]]). Следующие три необязательных параметра задают, соответственно: номер ошибки (в случае появления), ее строковое значение, и таймер. Таймер определяет время, через которое нужно дать отбой, если сервер не откликается.

Если соединение было выполнено успешно, тогда функция возвращает дескриптор соединения, если же вызов провалился, тогда 0 (ноль). Результат заносится в переменную $nntpsock. Как известно, в PHP понятия 0 и false практически эквивалентны, следовательно, если установить соединение с сервером не удалось, условие в операторе if принимает значение true. Вследствие этого выводится сообщение об ошибке, и скрипт останавливается. Если же соединение прошло успешно, скрипт продолжает работу.

$answer=fgets($nntpsock, 1000);

А теперь, о чудо, оказывается, что для обмена данными с сокетом применяются те же самые функции, что и для работы с файлами для построчного чтения и записи: fgets() и fputs(). При работе с сокетом нужно постоянно помнить одну важную деталь. Всегда после открытия сокета и передачи ему какой-либо информации необходимо считывать очередную строку (даже если она нам не нужна), тем самым перематывая «файл» в конец. Это необходимо потому, что вывод буферизируется, и при последующем обращении к сокету мы будем получать не ту строку, что надо, а более раннюю. В переменную $answer мы всегда будем считывать последний ответ сервера.

fputs($nntpsock, "AUTHINFO USER ".$UserLogin."\n"); $answer=fgets($nntpsock,1000); fputs($nntpsock, "AUTHINFO PASS ".$UserPass."\n"); $answer=fgets($nntpsock, 1000);

Здесь мы поочередно передаем серверу две команды: AUTHINFO USER логин_пользователя \n и AUTHINFO PASS пароль_пользователя\n. Они предназначаются для передачи серверу логина и пароля, введенных пользователем. Стоит обратить внимание на то, что в конце любой команды обязательно нужно ставить символ новой строки: \n.

if (trim($answer)!="281 OK") { echo "Логин или пароль введены неправильно"; exit(); }

Если логин и пароль были указаны верно, сервер скажет: 281 OK. Но поскольку эта фраза может быть сильно «приукрашена» ведущими и заключающими символами \r и \n, мы воспользуемся функцией trim(), чтобы отсечь их. Если же логин и пароль были указаны неверно, то выведется соответствующее сообщение, и скрипт остановится.

fputs($nntpsock, "GROUP ".$GroupName."\n"); $answer=fgets($nntpsock, 1000); if (substr($answer,0,3)!="211") { echo "Имя группы введено неправильно, или на сервере нет такой группы"; exit(); }

Теперь необходимо передать серверу информацию о том, с какой группой мы хотим работать — без этого нельзя. Делается это путем передачи команды GROUP имя_группы. После этого сервер вернет строку типа:

211 a b с имя_группы group selected

где:

211 — код, обозначающий, что команда распознана, группа выбрана, и все просто чудесно;

a — количество новых сообщений;

b — номер первой статьи (самой старой);

c — номер последней статьи (самой новой).

Итак, при помощи функции substr() мы проверили, начинается ли ответ сервера с цифр 211. Если нет, выводится соответствующее сообщение, и скрипт прекращает работу.

$GroupInfo=split(" ",$answer); echo "В группе $GroupName всего $GroupInfo[3] сообщений(я), из них $GroupInfo[1] новых<p>\n";

Теперь при помощи функции split() мы разделяем полученную строку, используя в качестве разделителя пробел. В результате получился массив $GroupInfo следующего содержания:

(211, количестно_новых_сообщений, номер_первого_сообщения, номер_последнего_сообщения, …).

То, что идет дальше, нам не интересно. Для уведомления пользователя о состоянии выбранной им группы выводится сообщение с полученными данными:

$NumMess=$GroupInfo[3]-$GroupInfo[2]+1; if ($NumMess<$MessLimit) { $MessLimit=$NumMess; }

В этом блоке выполняется проверка количества статей в выбранной группе. Если общее количество статей (номер_последней —номер_первой) в данной группе меньше, чем пользователь хочет посмотреть ($MessLimit), то тогда скрипт будет выводить только существующее количество статей.

for ($i=$GroupInfo[3]; $i>=$GroupInfo[3]-$MessLimit; $i--) { fputs($nntpsock, "ARTICLE $i\n"); $answer=fgets($nntpsock, 1000); if (substr($answer, 0,3)==«220»){ while (trim($answer)!=".") { $answer=fgets($nntpsock, 1000); echo $answer."<br>\n"; }} }

Теперь выводим последних $MessLimit статей, которые есть в выбранной группе. Вывод производится с минимальным форматированием. Для того чтобы получить статью, нужно передать серверу команду ARTICLE номер_статьи. Если такая статья есть, тогда сервер ответит:

220 n <a> article retrieved — head and body follow

где:

220 — опять же код, указывающий на то, что команда распознана;

n — номер статьи на сервере;

— message-ID: уникальный идентификатор статьи.

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

После передачи сервером вышеуказанной строки он передает сам текст сообщения: сначала все имеющиеся заголовки, а потом текст статьи. При этом заголовки отделяются от текста одной пустой строкой. Признак конца сообщения выглядит так: \r\n.\r\n (обратите внимание на точку между \r\n). Здесь использован один из двух возможных форматов команды ARTICLE — по номеру статьи. Есть также второй вариант —ARTICLE . Такой формат применяется, если нужно получить статью по ее идентификатору.

Кроме ARTICLE есть еще команды HEADER и BODY для получения только заголовков статьи или же только самого текста. Эти две команды, как и ARTICLE, можно использовать как с номером статьи, так и с ее идентификатором.

На этом процесс получения писем заканчивается, и мы приступаем к отправке сообщения, введенного пользователем. Если пользователь ничего не набрал, значит и отправлять нечего — выходим из скрипта:

if ($PostedMess=="") exit();

Декоративная горизонтальная линия, которая отделит отображенные статьи от последующего вывода:

echo "<hr>";

А теперь посылаем серверу команду POST:

fputs($nntpsock, "POST\n"); $answer=fgets($nntpsock, 100); if (substr($answer,0,3)!="340") { echo "Не удалось отправить письмо на сервер"; exit(); }

Если все в порядке, сервер вернет строку

340 send article to be posted. End with <CR-LF>.<CR-LF>

где:

340 — код, означающий, что все в порядке. Далее сервер сообщает, что нам можно отправить статью, которую нужно закончить уже упоминавшейся последовательностью \r\n.\r\n.

Если постить в группу запрещено, тогда ответ будет выглядеть так: 440 posting not allowed — извините, не повезло.

$ConvertedMess="From: $FromName Newsgroups: $GroupName Subject: $Subject $PostedMess\r\n.\r\n";

Формируем готовое к отправке сообщение (со всеми заголовками и окончанием) в переменной $ConvertedMess. Добавляем заголовки, введенные пользователем из формы. Как я уже говорил, первая пустая строка в сообщении означает, что все написанное дальше — текст сообщения. Иногда можно попасться на этом, написав вот так:

$ConvertedMess=" From: $FromName

И вновь напоминаю: не забудьте заканчивать сообщение последовательностью \r\n.\r\n. Если сервер ее не найдет, то сообщение будет забраковано и не принято.

fputs($nntpsock, $ConvertedMess);

Теперь передаем сообщение.

echo "Было получено $MessLimit писем.<p>Ваше сообщение было успешно доставлено";

Выводим небольшое сообщение о том, что все «пучком».

?>

Конец скрипта.

Ну вот, осталось только все это набрать, запустить и радоваться. Казалось бы, можно процитировать песню: «Ти просто молодчина, я просто молодець, на цьому все — казочці… кінець». Но тут донеслись крики: «Как это все? А практические рекомендации, советы, выезд на дом с установкой предлагаемого программного обеспечения!?»

Ладно, уговорили.

Первая проблема состоит в том, что некрасиво заваливать группы новостей всякими сообщениями типа «Test». Да и дорого это, тестировать скрипт в онлайне. Вывод напрашивается сам собой: нужно обзавестись NNTP-сервером. Не обязательно, чтобы он был суперкрутым, ведь мы хотим не администрировать новостной сервер, а всего лишь проверить скрипт. Мне в решении этой проблемы помог Eserv (http://www.eserv.ru/eserv/files/eserv299.exe, 1.60 Мб). Программа сочетает в себе функции WWW-, FTP-, POP3-, SMTP-, PROXY-, FINGER- и NNTP-серверов. Так сказать, «все в одном флаконе». Иногда софтина глючит, но в целом весьма функциональна.

Теперь посмотрим, чему мы научились. Во-первых, разобрали механизм использования сокетов в PHP на полезном примере. Таким образом, можно подсоединяться не только к 119 порту, но и к любому другому (за которым следит определенная программа). Частично разобрались с протоколом сетевых новостей. Я описал далеко не все команды протокола NNTP. Для того чтобы получить его полное описание, достаточно лишь зайти на страницу с описанием RFC977 (Request For Comment 977), URL которой указан выше. При желании можно написать крутой USENET-гейт.

Открою секрет: подобно NNTP-протоколу работают и POP, и SMTP (которые применяются для обмена почтой), и все остальные протоколы. Теперь перестает быть секретом, как работает web-интерфейс для почты. Даже HTTP, которым пользуются все (но не все знают об этом), тоже работает по аналогичному принципу.

Для того чтобы понять, как работают все эти протоколы, обратитесь к соответствующим описаниям RFC на сайте http://www.w3.org. Притом что наш скрипт работает на PHP, это вовсе не означает, что сей язык — единственный, поддерживающий сокеты. Их поддержка реализована, например, в том же Delphi, да и во многих других языках программирования, не применяющихся в web-программировании.

P.S. Проверить работу скрипта можно по адресу http://www.vodo-oblik.com.ua/nntp, а скачать его текст здесь: http://www.vodo-oblik.com.ua/nntp/nntp_client.zip(ЦЕЛЫХ два килобайта).