Как написать сервер на php

Примеры

Пример #1 Пример использования сокетов: Простой сервер TCP/IP

Этот пример показывает работу простого сервера. Измените
переменные address и port
в соответствии с вашими настройками и выполните. Затем вы можете соединиться с
сервером с командой, похожей на: telnet 192.168.1.53
10000
(где адрес и порт должны соответствовать вашим
настройкам). Всё, что вы наберёте на клавиатуре, будет затем выведено на сервере
и отправлено вам обратно. Для отключения наберите ‘выход’.


#!/usr/local/bin/php -q
<?php
error_reporting
(E_ALL);/* Позволяет скрипту ожидать соединения бесконечно. */
set_time_limit(0);/* Включает скрытое очищение вывода так, что мы видим данные
* как только они появляются. */
ob_implicit_flush();$address = '192.168.1.53';
$port = 10000;

if ((

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) {
echo
"Не удалось выполнить socket_create(): причина: " . socket_strerror(socket_last_error()) . "n";
}

if (

socket_bind($sock, $address, $port) === false) {
echo
"Не удалось выполнить socket_bind(): причина: " . socket_strerror(socket_last_error($sock)) . "n";
}

if (

socket_listen($sock, 5) === false) {
echo
"Не удалось выполнить socket_listen(): причина: " . socket_strerror(socket_last_error($sock)) . "n";
}

do {
if ((

$msgsock = socket_accept($sock)) === false) {
echo
"Не удалось выполнить socket_accept(): причина: " . socket_strerror(socket_last_error($sock)) . "n";
break;
}
/* Отправляем инструкции. */
$msg = "nДобро пожаловать на тестовый сервер PHP. n" .
"Чтобы отключиться, наберите 'выход'. Чтобы выключить сервер, наберите 'выключение'.n";
socket_write($msgsock, $msg, strlen($msg));

do {
if (

false === ($buf = socket_read($msgsock, 2048, PHP_NORMAL_READ))) {
echo
"Не удалось выполнить socket_read(): причина: " . socket_strerror(socket_last_error($msgsock)) . "n";
break
2;
}
if (!
$buf = trim($buf)) {
continue;
}
if (
$buf == 'выход') {
break;
}
if (
$buf == 'выключение') {
socket_close($msgsock);
break
2;
}
$talkback = "PHP: Вы сказали '$buf'.n";
socket_write($msgsock, $talkback, strlen($talkback));
echo
"$bufn";
} while (
true);
socket_close($msgsock);
} while (
true);socket_close($sock);
?>

Пример #2 Пример использования сокетов: Простой клиент TCP/IP

Этот пример показывает использование простого одноразового HTTP-клиента. Он просто
соединяется со страницей, отправляет запрос HEAD, выводит ответ
и завершает работу.


<?php
error_reporting
(E_ALL);

echo

"<h2>Соединение TCP/IP</h2>n";/* Получаем порт сервиса WWW. */
$service_port = getservbyname('www', 'tcp');/* Получаем IP-адрес целевого хоста. */
$address = gethostbyname('www.example.com');/* Создаём сокет TCP/IP. */
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (
$socket === false) {
echo
"Не удалось выполнить socket_create(): причина: " . socket_strerror(socket_last_error()) . "n";
} else {
echo
"OK.n";
}

echo

"Пытаемся соединиться с '$address' на порту '$service_port'...";
$result = socket_connect($socket, $address, $service_port);
if (
$result === false) {
echo
"Не удалось выполнить socket_connect().nПричина: ($result) " . socket_strerror(socket_last_error($socket)) . "n";
} else {
echo
"OK.n";
}
$in = "HEAD / HTTP/1.1rn";
$in .= "Host: www.example.comrn";
$in .= "Connection: Closernrn";
$out = '';

echo

"Отправляем HTTP-запрос HEAD...";
socket_write($socket, $in, strlen($in));
echo
"OK.n";

echo

"Читаем ответ:nn";
while (
$out = socket_read($socket, 2048)) {
echo
$out;
}

echo

"Закрываем сокет...";
socket_close($socket);
echo
"OK.nn";
?>

javier,javiern at gmail dot com

10 years ago


You can easily extend the first example to handle any number of connections instead of jsut one

#!/usr/bin/env php
<?php
error_reporting
(E_ALL);/* Permitir al script esperar para conexiones. */
set_time_limit(0);/* Activar el volcado de salida implícito, así veremos lo que estamo obteniendo
* mientras llega. */
ob_implicit_flush();$address = '127.0.0.1';
$port = 10000;

if ((

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) {
    echo
"socket_create() falló: razón: " . socket_strerror(socket_last_error()) . "n";
}

if (

socket_bind($sock, $address, $port) === false) {
    echo
"socket_bind() falló: razón: " . socket_strerror(socket_last_error($sock)) . "n";
}

if (

socket_listen($sock, 5) === false) {
    echo
"socket_listen() falló: razón: " . socket_strerror(socket_last_error($sock)) . "n";
}
//clients array
$clients = array();

do {

$read = array();
   
$read[] = $sock;$read = array_merge($read,$clients);// Set up a blocking call to socket_select
   
if(socket_select($read,$write = NULL, $except = NULL, $tv_sec = 5) < 1)
    {
       
//    SocketServer::debug("Problem blocking socket_select?");
       
continue;
    }
// Handle new Connections
   
if (in_array($sock, $read)) {       

                if ((

$msgsock = socket_accept($sock)) === false) {
            echo
"socket_accept() falló: razón: " . socket_strerror(socket_last_error($sock)) . "n";
            break;
        }
       
$clients[] = $msgsock;
       
$key = array_keys($clients, $msgsock);
       
/* Enviar instrucciones. */
       
$msg = "nBienvenido al Servidor De Prueba de PHP. n" .
       
"Usted es el cliente numero: {$key[0]}n" .
       
"Para salir, escriba 'quit'. Para cerrar el servidor escriba 'shutdown'.n";
       
socket_write($msgsock, $msg, strlen($msg));

            }

// Handle Input
   
foreach ($clients as $key => $client) { // for each client       
       
if (in_array($client, $read)) {
            if (
false === ($buf = socket_read($client, 2048, PHP_NORMAL_READ))) {
                echo
"socket_read() falló: razón: " . socket_strerror(socket_last_error($client)) . "n";
                break
2;
            }
            if (!
$buf = trim($buf)) {
                continue;
            }
            if (
$buf == 'quit') {
                unset(
$clients[$key]);
               
socket_close($client);
                break;
            }
            if (
$buf == 'shutdown') {
               
socket_close($client);
                break
2;
            }
           
$talkback = "Cliente {$key}: Usted dijo '$buf'.n";
           
socket_write($client, $talkback, strlen($talkback));
            echo
"$bufn";
        }

            }       
} while (

true);socket_close($sock);
?>


Timofey Bugaevsky

2 months ago


Good example from Javier.

For using in docker container you can use 0 as an address:
$address = '0';

To broadcast a message to all clients, you can use socket_write() for all required clients:
...
    // Handle Input
    foreach ($clients as $key => $client) {
        if (in_array($client, $read)) {
            ...
            if ($buf == 'message') {
                $talkback = "$buf $keyn";
                foreach ($clients as $curClient) {
                    socket_write($curClient, $talkback, strlen($talkback));
                }
                continue;
            }
            ...
        }
    }
...


zital at riseup dot net

5 years ago


due to PHP standars modify the code to:
<?php
//...
   
$write = NULL;
   
$except = NULL;
   
$tv_sec = 5;// Set up a blocking call to socket_select
   
if(socket_select($read, $write, $except, $tv_sec) < 1)
//...
?>

Сокеты: Сервер на PHP

В предыдущей статье я рассказывал про сокеты на PHP. И сказал, что необходимо написать сервер, принимающий запросы и отдающий ответы. И клиента, посылающего запросы к серверу. В этой статье мы разберём код для классического сервера, принимающего число, возводящий его в квадрат и возвращающий результат клиенту.

Сразу привожу код сервера на PHP с подробными комментариями:

<?php
  header('Content-Type: text/plain;'); //Мы будем выводить простой текст
  set_time_limit(0); //Скрипт должен работать постоянно
  ob_implicit_flush(); //Все echo должны сразу же отправляться клиенту
  $address = 'localhost'; //Адрес работы сервера
  $port = 1985; //Порт работы сервера (лучше какой-нибудь редкоиспользуемый)
  if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) < 0) {
    //AF_INET - семейство протоколов
    //SOCK_STREAM - тип сокета
    //SOL_TCP - протокол
    echo "Ошибка создания сокета";
  }
  else {
    echo "Сокет созданn";
  }
  //Связываем дескриптор сокета с указанным адресом и портом
  if (($ret = socket_bind($sock, $address, $port)) < 0) {
    echo "Ошибка связи сокета с адресом и портом";
  }
  else {
    echo "Сокет успешно связан с адресом и портомn";
  }
  //Начинаем прослушивание сокета (максимум 5 одновременных соединений)
  if (($ret = socket_listen($sock, 5)) < 0) {
    echo "Ошибка при попытке прослушивания сокета";
  }
  else {
    echo "Ждём подключение клиентаn";
  }
  do {
    //Принимаем соединение с сокетом
    if (($msgsock = socket_accept($sock)) < 0) {
      echo "Ошибка при старте соединений с сокетом";
    } else {
      echo "Сокет готов к приёму сообщенийn";
    }
    $msg = "Hello!"; //Сообщение клиенту
    echo "Сообщение от сервера: $msg";
    socket_write($msgsock, $msg, strlen($msg)); //Запись в сокет
    //Бесконечный цикл ожидания клиентов
    do {
      echo 'Сообщение от клиента: ';
      if (false === ($buf = socket_read($msgsock, 1024))) {
        echo "Ошибка при чтении сообщения от клиента";
      }
      else {
        echo $buf."n"; //Сообщение от клиента
      }
      //Если клиент передал exit, то отключаем соединение
      if ($buf == 'exit') {
        socket_close($msgsock);
        break 2;
      }
      if (!is_numeric($buf)) echo "Сообщение от сервера: передано НЕ числоn";
      else {
        $buf = $buf * $buf;
        echo "Сообщение от сервера: ($buf)n";
      }
      socket_write($msgsock, $buf, strlen($buf));
    } while (true);
  } while (true);
  //Останавливаем работу с сокетом
  if (isset($sock)) {
    socket_close($sock);
    echo "Сокет успешно закрыт";
  }
?>

Данный код я постарался тщательно прокомментировать, поэтому, надеюсь, Вам в нём всё понятно. Главное понять следующее: сервер — это непрерывно выполняющийся скрипт, который просто ждёт подключения клиентов. Затем принимающий запрос и на основании этого запроса, возвращающий клиенту ответ. Это самое главное, что Вам необходимо понять.

А в следующей статье мы с Вами напишем клиента на PHP, который и будет подключаться к данному серверу и отправлять различные запросы.

  • Создано 16.01.2012 14:51:02


  • Михаил Русаков

Копирование материалов разрешается только с указанием автора (Михаил Русаков) и индексируемой прямой ссылкой на сайт (http://myrusakov.ru)!

Добавляйтесь ко мне в друзья ВКонтакте: http://vk.com/myrusakov.
Если Вы хотите дать оценку мне и моей работе, то напишите её в моей группе: http://vk.com/rusakovmy.

Если Вы не хотите пропустить новые материалы на сайте,
то Вы можете подписаться на обновления: Подписаться на обновления

Если у Вас остались какие-либо вопросы, либо у Вас есть желание высказаться по поводу этой статьи, то Вы можете оставить свой комментарий внизу страницы.

Если Вам понравился сайт, то разместите ссылку на него (у себя на сайте, на форуме, в контакте):

  1. Кнопка:

    Она выглядит вот так: Как создать свой сайт

  2. Текстовая ссылка:

    Она выглядит вот так: Как создать свой сайт

  3. BB-код ссылки для форумов (например, можете поставить её в подписи):

Настраиваем веб-сервер для работы с PHP из браузера

Это урок для тех, кто уже знает основы языка PHP. В нем мы научимся отображать простейшие странички в браузере. Для этого мы узнаем как работает, установим и научимся запускать встроенный в PHP веб-сервер, а также веб-сервер Апач.

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

Как работает браузер

Браузер — это программа, которая загружает с веб-сервера и отображает на экране веб-страницы. Когда ты переходишь по ссылке или руками вводишь в адресную строку адрес вроде http://example.com/news.html, происходит следующее:

  • браузер устанавливает соединение через интернет с узлом example.com, с запущенной на нем программой под названием веб-сервер
  • браузер посылает программе-веб-серверу запрос по протоколу HTTP (протокол — это язык для общения программ между собой) с просьбой предоставить ему страницу по адресу example.com/news.html
  • веб-сервер в ответ отправляет файл в формате HTML (HTML — это текст, размеченный специальными тегами), содержащий тело страницы
  • браузер отображает этот файл на экране. Если страница включает в себя другие файлы, например, картинки или видеоролики, браузер для каждого из них устанавливает новое соединение и отправляет новый запрос на сервер

Протокол HTTP

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

Однако, протокол HTTP позволяет не только скачивать страницы или файлы с сервера, но и отправлять на сервер данные из форм (например при отправке комментария к видео или при регистрации на сайте) или загружать на сервер файлы (например в приложении-файлообменнике). В протоколе описан синтаксис, как именно должен выглядеть запрос, как его нужно обрабатывать, как должен выглядеть ответ. Тот, кто отправляет запрос, называется клиентом (то есть, браузер играет роль клиента), а тот, кто обрабатывает запрос и отвечает на него, называется сервером. Клиент и сервер — это программы, и они могут быть запущены как на одном, так и на разных компьютерах, при условии, что они связаны сетью.

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

Протокол HTTP поддерживает 2 основных вида запросов (они называются методы): GET и POST. Метод GET используется для получения (скачивания) с сервера файлов и страниц (это одно и то же, так как веб-страница — это просто файл в формате HTML). Браузер указывает в HTTP-запросе URL файла, который он хотел бы получить, и веб-сервер возвращает в HTTP-ответе либо запрошенный файл, либо сообщение об ошибке.

Метод POST позволяет отправлять на сервер данные из заполненных пользователем форм, в том числе с приложенными файлами. Браузер указывает URL, на который отправляются данные, и прикладывает значения, введенные пользователем в поля формы. В ответ сервер может вернуть какую-то страницу (например, с текстом об успешном приеме данных), которую браузер покажет пользователю.

Вот пример HTTP-запроса, который может послать браузер для получения данных с сервера. Он просит сервер передать ему содержимое страницы по адресу http://example.com/news.html:

GET /news.html HTTP/1.1
Host: example.com
User-Agent: Chromius/43
Accept: text/html,application/xhtml+xml,application/xml
Accept-Language: ru, en

В HTTP запросы и ответы могут состоять из 3 частей — стартовая строка, заголовки и тело. В примере выше стартовая строка GET /news.html HTTP/1.1 содержит метод запроса, путь к файлу из URL и версию протокола. В HTTP любой запрос начинается со стартовой строки, содержащей эти 3 элемента.

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

После стартовой строки идут заголовки запроса. Они содержат дополнительную информацию о запросе и о браузере. Например, заголовок User-Agent содержит название и версию клиента, а заголовок Accept-Language указывает, на каких языках (в данном примере — русский и английский) клиент предпочел бы получить информацию. Обязательный заголовок только один — Host, он указывает домен, с которого мы запрашиваем страницу, все остальные заголовки можно не указывать. Заголовки заканчиваются одной пустой строкой.

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

Вот как может выглядеть ответ на этот запрос, если запрашиваемая страница есть на сервере:

HTTP/1.1 200 Ok
Server: nginx/1.0.0
Date: Sat, 11 Jul 2015 01:54:57 GMT
Content-Type: text/html; charset=UTF-8

<h1>Hello World</h1>

Ответ точно также, как и запрос, начинается со стартовой строки. Она всегда содержит ровно 3 элемента: версию протокола, которую использует сервер (HTTP/1.1), код состояния, указывающий, успешно ли обработан запрос или была ошибка (в данном случае 200, успешно), и текстовое человекочитаемое пояснение кода состояния (Ok — все хорошо).

После стартовой строки идут заголовки. Заголовок Server описывает название и версию программы веб-сервера, которая обработала запрос. Заголовок Content-Type описывает тип файла, который содержится в теле ответа. В данном случае это text/html; charset=utf-8, то есть текстовый файл с разметкой на языке HTML в кодировке utf-8.

После заголовков идет пустая строка и за ней тело ответа, которое в данном примере содержит текстовый файл в формате HTML (<h1>Hello world</h1>). Браузер отобразит его содержимое на экране.

HTML файл содержит текст страницы и специальные теги (метки) вроде <h1>, которые разбивают этот текст на абзацы, заголовки, позволяют добавлять в него ссылки, картинки, формы и многое другое. Ты можешь увидеть полученный от сервера HTML-код любой страницы в интернете, зайдя на нее и нажав Ctrl + U в браузере или выбрав в меню что-то вроде «Вид» — «Показать исходный код страницы».

Заголовок Content-Type влияет на то, как именно браузер отобразит содержимое. Если там будет например указано image/png, то браузер будет воспринимать тело ответа как картинку в формате PNG и попытается его отобразить. Конструкция вроде image/png называется MIME-тип, она состоит из 2 частей: общего типа данных (image, text, audio) и конкретного формата файла. Подробнее про MIME-типы можно почитать в Википедии.

Как видишь, сервер в ответ сообщил что запрос обработан успешно. Вот пример ответа с сообщением об ошибке:

HTTP/1.1 404 Not Found
Server: nginx/1.0.0
Date: Sat, 11 Jul 2015 01:54:57 GMT
Content-Type: text/html; charset=UTF-8

Page /news.html was not found on this server.  

Этот ответ отличается от предыдущего кодом состояния. В данном случае он равен 404, что значит «запрошенный файл отсутствует на сервере». Тело ответа содержит HTML-код с текстом ошибки («Page … not found on this server»), которая будет показана пользователю.

Вот самые распространенные коды состояний:

  • 200 Ok — все ок, тело ответа содержит запрошенный файл
  • 500 Internal Server Error — сервер не смог обработать запрос из-за ошибки на нем
  • 404 Not Found — запрошенный файл не был найден на сервере (скорее всего, неправильно указан URL в запросе)
  • 403 Access Denied — доступ к запрошенному файлу запрещен
  • 301 Moved Permanently — запрошенный файл находится теперь по другому адресу, который указан в заголовке ответа Location. Браузер должен сделать новый запрос по этому адресу.
  • если тебе интересно, можешь глянуть полный список кодов состояний в Википедии

Рассмотрим напоследок еще пример POST запроса. Допустим, что где-то на сайте example.com есть форма логина, пользователь ввел туда свои логин и пароль и отправил ее. Вот как будет выглядеть POST запрос, который браузер отправит на сервер:

POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 26

login=ivan&password=123456

Этот запрос точно так же содержит стартовую строку, заголовки запроса, и тело запроса (в GET-запросе выше тела не было), в котором передаются введенные в форму пользователем данные (логин и пароль). Заголовок Content-Type указывает, что в теле содержатся данные из формы и указывает способ их кодирования, а заголовок Content-Length содержит размер данных в байтах.

Что отправит сервер в ответ на этот запрос, зависит от того, как программа на сервере будет обрабатывать эти данные.

Формат запросов и ответов, поведение веб-сервера описано в документации к протоколу HTTP. Если тебе интересны подробности, можно прочесть мой урок по HTTP или начать с Википедии: https://ru.wikipedia.org/wiki/HTTP.

Ты можешь увидеть какой именно запрос отправляет твой браузер, зайдя на любой сайт, открыв developer tools на вкладке Network (Ctrl + Shift + I в Хроме (и его клонах вроде яндекс-браузера), Опере, Фаерфоксе, F12 в ИЕ, через меню «инструменты» в Сафари) и перезагрузив страницу. Ты увидишь список отправленных HTTP запросов, а при клике по ним сможешь их посмотреть.

Таким образом, задача браузера — отправить запрос на сервер, получить HTML файл и отобразить на экране. А задача веб-сервера — принимать и выполнять запросы от браузера, например предоставлять (или генерировать) запрошенные файлы и страницы.

Статические и динамические страницы

Есть 2 варианта, откуда веб-сервер может получать файлы, которые он отдает в браузер. Статический файл — это файл, который расположен на жестком диске сервера. Разработчик подготавливает файлы и загружает их на сервер, а тот по HTTP-запросу отдает их в браузер. Преимущество статических сайтов в том, что их просто создавать, они быстро работают, они очень надежны и там редко возникают ошибки или уязвимости, но у них есть недостаток: они не интерактивны, на них нельзя сделать регистрацию, добавление комментариев, постов, лайки и другие возможности, которые требуют многовенного обновления информации. Чтобы изменить что-то на таком сайте, веб-мастер должен вручную отредактировать эти HTML файлы и выгрузить измененные копии на сервер. Не-программист не может это сделать.

Например, статический сайт хорошо подходит для публикации документации, информационного сайта, финансового отчёта, книги, какой-то информации, которая редко обновляется.

Также, статический сайт имеет одну особенность: так как это просто набор файлов, то его можно просматривать даже без веб-сервера и связи с интернетом. Достаточно сохранить HTML-файл со страницей себе на компьютер (например, нажав Ctrl + S в браузере), после этого её можно открыть и просмотреть в браузере (дважды кликнув или перетащив его в окно браузера). Или можно сделать архив с содержимым сайта и раздавать пользователям.

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

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

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

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

Язык PHP заточен под создание динамических сайтов, что и сделало его очень популярным. Есть и другие языки, пригодные для этого — например, Java, Ruby, Python, Javascript, C# — но на PHP простые страницы сделать проще.

Веб-сервер

Итак, для того, чтобы отобразить в браузере какую-то страницу, нам нужен веб-сервер (чтобы отвечать на запросы браузера). Есть несколько разных программ-серверов, например Apache, Nginx, но мы начнем с изучения простого веб-сервера, встроенного в интерпретатор php.

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

  • для Windows — инструкции описаны в уроке по установке PHP на Windows
  • для линукс — способ зависит от используемого дистрибутива. Например, в дебиан или убунту это делается командой sudo apt-get install php5, в других дистрибутивах — немного другой командой. Погугли
  • для MacOS X — погугли
  • для андроида — придется помучиться. На июль 2016 года актуальна такая последовательность действий: для начала надо установить scripting layer for android — скорее всего его нет в Google Play, и надо установить apk файл вручную (будь осторожен, то что в Google Play находится по словам sl4a — это какие-то посторонние приложения). Затем из этого приложения надо установить PHP. И тогда появится возможность открыть окно командной строки и запускать сам PHP.

PHP — это программа командной строки. Это значит, что у него нет никакого графического интерфейса с кнопками и окнами, а для выполнения какого-то действия надо набрать в командной строке правильную команду. Потому сначала рекомендую изучить наш урок по использованию командной строки. Настройки php задаются в файле php.ini, который в linux лежит в /etc/php/, а в Windows — в папке с PHP, но нам пока не требуется их менять.

Проверь, что ты смог правильно установить PHP. Для этого набери команду

Если все верно, то она выведет информацию о версии установленного интерпретатора PHP (если у тебя PHP не находится в PATH, то вместо php придется писать полный путь к файлу, например c:phpphp.exe). Еще одна полезная команда — это php -i — она выводит информацию о текущих настройках PHP. Все возможные опции, которые можно указать, перечислены в мануале по использованию php.

Встроенный в PHP сервер

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

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

Итак, в PHP встроен простейший веб-сервер для разработчиков. Чтобы запустить его, создай папку, из которой будут раздаваться файлы, она называется корень сервера или document root. Допустим, это d:server. Открой командную строку и перейди в эту папку, а затем запусти веб-сервер, набрав следующие команды. После каждой команды надо нажимать клавишу Enter. Будь внимателен при их наборе, не пропускай пробелы, не путай прямую и обратную наклонную черту:

d:
cd server
c:phpphp.exe -S localhost:9001

(если ты работаешь не под Windows, то команды будут иметь немного другой вид). Если тебе лень писать каждый раз эти команды вручную, то в уроке по командной строке описано, как можно поместить их в bat-файл, который можно запускать просто двойным кликом по иконке.

-S обозначает «запуститься в режиме веб-сервера». Надо написать именно заглавную S, c маленькой буквой не заработает. localhost (вместо него можно еще писать 127.0.0.1 — это твой собственный адрес) обозначает принимать соединения только со своего компьютера, и не принимать соединения с других устройств (если хочешь чтобы твой сервер был доступен во всей локальной сети, пиши вместо localhost адрес 0.0.0.0 — после этого к тебе можно будет зайти по ip).

9001 — это номер порта, на котором сервер будет ждать соединения от браузера. Если произойдет ошибка и будет написано что этот порт уже занят, введи другое число (от 1 до 65534), например 9002. Вообще-то обычно для веб-сервера используется порт 80, но у тебя он может быть занят другими программами — например, скайпом, торрентокачалкой или чем-то еще (если это так, стоит зайти в их настройки и запретить его использовать на будущее). Также ты можешь увидеть список занятых портов командой netstat -an, а команда netstat -abn покажет программу, занявшую порт (нужно запускать эту команду из консоли с повышенными привилегиями).

Учти что в линуксе и маке, чтобы открыть порт ниже 1024, нужны права администратора (то есть сервер надо запускать через sudo: sudo php ..., что не очень безопасно и не рекомендуется).

Номер порта нужен для того, чтобы на компьютере можно было одновременно запустить несколько работающих с сетью программ. Мы назначаем каждой из них свой номер порта. Когда на компьютер приходят данные из сети, ОС по номеру порта понимает какой именно программе они адресованы. Две программы не могут использовать одновременно один и тот же порт. Подробнее о TCP-портах.

Завершить работу сервера можно, нажав Ctrl + C или закрыв окно консоли. Сервер будет в процессе работы писать в консоль информацию о поступающих от браузера запросах и информацию о возникающих ошибках.

Теперь надо проверить, как работает наш сервер. По умолчанию веб-сервер просто отдает файлы из корневой папки, путь к которым указан в URL. Создай в папке сервера файл, например 1.txt и напиши в нем текст, например hello world (латиницей, чтобы не беспокоиться о кодировках). После этого открой браузер и введи в адресную строку адрес

http://localhost:9001/1.txt

В этом URL мы указываем, что браузер должен отправить запрос по протоколу HTTP на узел localhost (то есть твой собственный компьютер), на порт 9001 (который использует веб-сервер) и запросить у него файл /1.txt. Если ты забыл что такое URL, то у нас есть урок по ним: ../network/urls.md.

Если все верно, ты должен увидеть содержимое текстового файла, а в консоли появится строчка с этим запросом. Если что-то не работает — перепроверяй, запущен ли php, что он пишет в консоль, правильно ли ты написал слово localhost и номер порта.

Попробуй теперь скопировать в корневую папку какую-нибудь картинку, например, cat.jpg, и открыть в браузере URL http://localhost:9001/cat.jpg . Ты должен увидеть эту картинку.

Повторим еще раз, что происходит в этом случае:

  • браузер соединяется с веб-сервером на порту 9001
  • браузер отправляет запрос на получение файла cat.jpg
  • сервер обрабатывает запрос, находит файл и отправляет ответ с кодом 200, заголовком Content-Type: image/jpg и содержимым файла в теле ответа
  • браузер извлекает картинку из тела ответа и отображает на экране

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

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

Создадим в корневой папке файл 1.php с таким содержимым:

<?php
header("Content-Type: text/plain; charset=utf-8");
echo mt_rand(1, 100);

Функция header() позволяет добавить в HTTP-ответ произвольный заголовок. В данном случае мы используем ее, чтобы сказать браузеру, что данные, которые мы выводим, являются обычным текстом в кодировке utf-8 без HTML-разметки и надо отобразить его как есть. Если это не сделать, то по умолчанию PHP укажет тип text/html, и браузер будет думать что перед ним текст в формате HTML. Из-за этого, например, не будут отображаться переводы строк.

Если ты знаешь язык PHP, то наверно догадываешься, что выведет этот скрипт. Набери в браузере URL http://localhost:9001/1.php. Если все верно, ты увидишь на экране случайное число. Обнови страницу и число поменяется. Это происходит из-за того, что каждый раз браузер отправляет новый HTTP-запрос, сервер видит что запрашивается php файл, и запускает написанную в нем программу, и отдает в браузер то, что выводит эта программа. Ну а браузер отображает полученный текст на экране.

Обрати внимание, что браузер сам не умеет выполнять PHP-код. Он лишь посылает запрос на сервер и отображает то, что придет в ответ. PHP-код выполняет именно сервер.

Если ты видишь вместо него белую страницу, а исходный код в браузере показывает текст скрипта — значит скрипт не выполнился. Проверь, правильный ли URL в адресной строке браузера. Если там что-то вроде file://d:/server/1.php — значит ты невнимательно прочел инструкции выше. Протокол file:// обозначает, что браузер открывает файл напрямую с диска, а не запрашивает с веб-сервера, и потому PHP-код не будет выполняться.

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

Наконец, давай сделаем еще один файл, который показывает текущие настройки PHP и который пригодится нам если что-то пойдет не так. Создай файл info.php с текстом:

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

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

Для того, чтобы передать нашей PHP-программе какие-то данные, мы можем вписать их в query string (перечитай урок про URL, если не знаешь, что это такое). Это часть URL, которая идет после знака вопроса, например: http://localhost:9001/add.php?x=10&y=20. Перед запуском программы PHP анализирует query string и извлекает значения из нее в специальный массив $_GET. Напишем скрипт add.php, который выводит на экран сумму переданных значений:

<?php 
header("Content-Type: text/plain; charset=utf-8");

$x = array_key_exists('x', $_GET) ? floatval($_GET['x']) : 0;
$y = array_key_exists('y', $_GET) ? floatval($_GET['y']) : 0;
$sum = $x + $y;

echo "$x + $y = $sumn";

Если пользователь не укажет в URL значения x и y, то PHP не поместит их в массив $_GET. Когда наш скрипт попытается обратиться к $_GET['x'], произойдет ошибка. Потому мы делаем проверку, что в массиве есть такой элемент, и если его нет, то присваиваем переменной значение 0. Если элемент есть, то мы обрабатываем значение функцией floatval, которая преобразует любое значение в целое или дробное число. Даже если пользователь вместо числа напишет бессмысленное значение вроде x=xyz, в программе не произойдет ошибки.

В PHP7 добавили новый оператор объединения с null, с помощью которого можно чуть упростить код:

$x = floatval($_GET['x'] ?? 0);

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

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

Пробелы и перевод строки

Чтобы переносы строк нормально работали и в браузере, и при запуске скрипта в консоли, можно использовать для них традиционный n, а в начале программы поставить

header("Content-Type: text/plain; charset=utf-8");

Это заставит браузер воспринимать то, что выводит твоя программа, как обычный текст, а не HTML-код, и уважать переносы строк в нем. Иначе перенос строки будет в исходном коде страницы (его можно увидеть нажав Ctrl + U), но на самой странице его не будет.

Ведь по умолчанию веб-сервер отдает результат в браузер, говоря что это HTML-файл, а в этом языке любое число пробелов и переводов строк выводится как один пробел. Отдавая заголовок Content-Type, мы говорим браузеру что наш файл содержит обычный текст и не должен интерпретироваться как HTML код.

Индексный файл

Если ты попытаешься открыть URL, в котором не указано имя файла, например: http://localhost:9001/, то сервер будет искать файлы с названием index.php или index.html в корневой папке. Это так называемый «индексный» файл, который отдается по умолчанию, если конкретное имя файла не указано.

Если в URL указано только имя папки, без файла, то сервер будет искать индексный файл в ней. Ну, например, для URL http://localhost:9001/some/folder/ сервер будет искать файлы d:serversomefolderindex.php или d:serversomefolderindex.html.

Произвольные URL

До сих пор мы указывали путь к файлу или папке в URL. Но, что если мы хотим, чтобы наша страница имела бы более красивый URL, не /1.php, а например http://localhost:9001/latest-news, который не соответствует папке или файлу на диске? Для этого нам придется написать свой скрипт, который будет анализировать запрашиваемый URL и решать, что делать. Скрипт должен либо обработать запрос и что-то вывести, либо вернуть значение false для того, чтобы обработать запрос стандартным образом.

Эта возможность описана в мануале PHP по встроенному серверу.

Имя скрипта маршрутизации надо указать при запуске веб-сервера. Попробуем написать простой скрипт с такой логикой:

  • если запрошен URL /latest-news, то выполнить скрипт news.php
  • если запрошен URL /hello, то вывести фразу «hello world»
  • иначе искать указанный в URL файл

Создадим в корневой папке скрипт router.php с таким кодом. Если ты видишь тут незнакомые функции и команды, погугли их:

<?php
// Получаем запрошенный URL и вырезаем из него путь
$url = $_SERVER['REQUEST_URI'];
$path = parse_url($url, PHP_URL_PATH);

if ($path == '/latest-news') {
    // Выполняем программу из указанного файла
    require __DIR__ . '/news.php';
    exit;
} elseif ($path == '/hello') {
    header("Content-Type: text/plain; charset=utf-8");
    echo "Hello world";
    exit;
}

// Возвращаем управление веб-серверу, если мы не смогли обработать запрос
// Веб-сервер попытается самостоятельно найти указанный в запросе файл
return false;

Чтобы веб-сервер использовал скрипт маршрутизации, его надо запускать такой командой:

php -S localost:9001 router.php

Попробуй открыть URL вроде http://localhost:9001/hello и проверить, что все работает, как и задумано.

Вот краткое объяснение использованных в скрипте выше конструкций:

  • $_SERVER — это специальный массив, в который перед выполнением скрипта PHP помещает информацию о конфигурации сервера и параметрах пришедшего HTTP-запроса. Ну например, пришедшие от браузера заголовки помещаются в этот массив. В нем есть элемент REQUEST_URI, который содержит указанный в запросе путь из URL. Если тебе любопытно, что в этом массиве есть еще, сделай скрипт с командой var_dump($_SERVER) и посмотри.
  • parse_url — это функция, которая извлекает из URL указанную часть
  • require — это команда PHP, которая выполняет код из указанного в ней файла.
  • __DIR__ — это встроенная в PHP константа, которая содержит полный путь к папке, где находится данный PHP-файл (например, c:programsphp)
  • exit — команда завершения PHP программы
  • header — это функция, которая добавляет в HTTP-ответ (который отдаст веб-сервер) указанный заголовок

Установка Апача

Апач сложнее чем встроенный сервер, но дает больше возможностей.

Урок: Установка Апача

Что делать дальше

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

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

Наконец, стоит почитать туториал в официальном мануале PHP и научиться добавлять в HTML странички PHP код, а также обрабатывать данные из форм:

  • http://php.net/manual/ru/tutorial.php
  • http://php.net/manual/ru/language.basic-syntax.php

После этого прочитай урок про шаблоны.

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

Полезные функции и конструкции PHP, которые стоит изучить

  • require, require_once
  • header()
  • setcookie()

Задачи

Отправка HTTP запроса

Изучив протокол HTTP, попробуем самостоятельно отправить HTTP запрос и получить на него ответ. Для этого мы будем использовать программу telnet. Она работает под Windows и под linux (и возможно под маком). Вообще, она предназначена не для выполнения HTTP запросов, а использовалась много лет назад для выполнения команд на удаленном сервере. Но так как она просто передает вводимые данные на сервер и выводит полученный ответ, то мы можем использовать ее для своих целей.

Запускается она так: telnet хост порт, например telnet wikipedia.org 80. Обрати внимание, что надо писать именно имя хоста или IP-адрес, а не URL. Нельзя написать telnet http://wikipedia.org — это работать не будет. Номер порта для протокола HTTP по умолчанию — это 80.

Если запустить эту команду, то она соединится с указанным сервером на указанном порту и будет передавать на сервер все, что мы напечатаем, а затем отобразит присланный сервером ответ. То есть мы можем набрать текст HTTP-запроса и увидеть на экране HTTP-ответ от сервера. Но руками набирать HTTP-запрос не очень удобно, так как легко ошибиться (а исправить ошибку нельзя), и на сервере может стоять ограничение по времени, после которого он разорвет соединение, не дождавшись окончания запроса. Потому лучше заранее записать запрос в текстовый файл, и запустить команду, чтобы она читала бы запрос из него, вот так:

telnet example.com 80 < c:file.txt

При сохранении файла выбери в редакторе опцию «кодирование перевода строк» как «Windows». В протоколе HTTP перевод строки должен кодироваться как rn (CR LF). Кстати, Блокнот Windows использует именно такой формат, и потому подойдет для набора запроса.

В файле с запросом должно быть примерно такое содержимое:

GET / HTTP/1.1
Host: example.com
User-Agent: human

Не забудь, что запрос должен как минимум содержать стартовую строку и заголовок Host. Не забудь заменить название домена на правильное и добавить одну пустую строку после заголовков. Создав файл, попробуй выполнить команду telnet. Если все верно, то ты должен увидеть ответ от сервера, начинающийся со строки вроде HTTP/1.1 200 Ok. Попробуй добиться именно ответа с кодом 200, пробуя разные сайты.

Так как сейчас (в 2016 году) многие сайты уже перешли на протокол HTTPS (который отличается от HTTP использованием шифрования и портом 443 по умолчанию), то часто ты будешь получать ответы, требующие перейти на зашифрованное соединение, вроде такого:

HTTP/1.1 303 See Other
Location: https://example.com/

К сожалению, написать зашифрованный запрос руками в текстовом редакторе вряд ли возможно. Однако, есть программы, которые могут взять шифрование на себя. Для этого тебе надо установить библиотеку OpenSSL. Найти сборку под Windows не так-то просто. На момент написания статьи (2016) ссылки на скачивание библиотеки под Windows доступны на этой странице: https://wiki.openssl.org/index.php/Binaries (отсюда переходим на сайт https://indy.fulgan.com/SSL/ и скачиваем самую новую версию вроде openssl-1.0.2m-i386-win32.zip или openssl-1.0.2m-x64_86-win64.zip в зависимости от разрядности ОС. Впрочем, 32-битная версия должна работать везде. Скачанный архив распаковываем в любую папку и при желании добавляем ее в PATH). Под linux библиотека устанавливается командой вроде sudo apt-get install openssl в зависимости от используемого дистрибутива.

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

openssl s_client -connect example.com:443 < file.txt

Если исполняемый файл openssl не находится в PATH, то надо писать полный путь к нему, например, c:opensslopenssl.exe. Обрати внимание, что HTTPS по умолчанию использует порт 443 вместо 80. Openssl возьмет на себя шифрование твоего запроса и расшифровку ответа.

При соединении openssl выведет довольно много отладочной информации об используемом сервером сертификате и параметрах SSL/TLS шифрования. Ее вывод можно отключить, дописав к команде (до символа <) флаг -quiet. Про другие опции можно прочесть в мануале по openssl s_client (англ., сложный).

Разумеется, для отправки HTTP-запросов есть и программы с графическим интерфейсом. Они гуглятся по словам «GUI HTTP client». Однако, чтобы ими пользоваться, надо понимать основы протокола HTTP и стоит все-таки хотя бы раз попробовать отправить запрос вручную. Примеры приложений (и расширений для ФФ):

  • https://www.getpostman.com/postman (сложный)
  • http://www.swensensoftware.com/im-only-resting
  • https://addons.mozilla.org/en-US/firefox/addon/restclient/ (расширение для Firefox)

Еще один кредит

Для решения этой задачи тебе надо изучить основы HTML, HTML-формы и глобальную переменную $_GET в PHP.

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

  • надпись «поле X заполнено неверно», если введены неправильные данные
  • надпись «выплатить кредит невозможно так как ежемесячный прирост X больше ежемесячной выплаты», если выплатить кредит не получится
  • надпись «время выплаты: N месяцев, сумма выплаты: X»

Экранирование

Для решения этой задачи надо изучить основных HTML, HTML-формы и функцию htmlspecialchars().

Сделай страницу с формой (использующей метод GET) из поля ввода textarea (с именем text) и кнопки отправки. Никакого оформления и CSS не требуется, просто черный текст на белом фоне. При вводе любого текста и нажатия кнопки внизу под textarea должен отобразиться введенные текст и ссылка.

Текст должен точно соответствовать введенному, корректно отображать все символы, включая пробелы и переводы строк (тут тебе поможет тег pre), точно так как их ввел пользователь. Проверь что сочетания вроде

%20%20
<b>test
&amp;amp;
"'<>
&t=1&

Отображаются ровно в том же виде как введены.

Кроме текста, надо выводить ссылку вида script.php?text=…..&lt=1, которая содержит в параметре text введенный текст, а в параметре lt единицу и открыв которую, мы можем увидеть его на странице.

Это задача на умение правильно экранировать символы. Например, в языке HTML символы вроде < или & имеют специальное значение (они открывают тег или HTML мнемонику) и их надо правильно экранировать. Аналогично с параметрами ссылки.

Подсказки:

  • про то, как корректно вставить текст с любыми символами в HTML-код, написано в уроке про XSS
  • по умолчанию в HTML любое число переводов строк и пробелов воспринимается как один пробел. Чтобы сохранить все пробелы и переводы строк при выводе, нужно либо использовать HTML-тег pre, либо CSS-свойство white-space
  • при подстановке параметра в ссылку вроде x.php?a=... необходимо корректно экранировать спецсимволы. Как это сделать, описано в уроке про структуру URL
  • при подстановке ссылки в HTML-атрибут href ее, разумеется, тоже нужно корректно экранировать

Я тебя помню

Для решения этой задачи необходимо изучить куки, переменную $_COOKIE и функцию setcookie().

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

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

Просмотреть куки можно с помощью developer tools в браузере.

Протокол WebSocket предназначен для решения разных задач и снятия ограничений обмена данными между браузером и сервером. Он позволяет пересылать любые данные, на любой домен, безопасно и почти без лишнего сетевого трафика. Для установления соединения WebSocket клиент и сервер используют протокол, похожий на HTTP. Клиент формирует особый HTTP-запрос, на который сервер отвечает определенным образом.

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

В первую очередь надо в файле php.ini расскомментировать строку, позволяющую работать с сокетами и перезапустить сервер:

extension = php_sockets.dll

Вот как выглядит простейший сокет-сервер:

<?php
function SocketServer($limit = 0) {
    $starttime = time();
    echo 'SERVER START' . PHP_EOL;

    echo 'Socket create...' . PHP_EOL;
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

    if (false === $socket) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    echo 'Socket bind...' . PHP_EOL;
    $bind = socket_bind($socket, '127.0.0.1', 7777); // привязываем к ip и порту
    if (false === $bind) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    echo 'Set options...' . PHP_EOL;
    // разрешаем использовать один порт для нескольких соединений
    $option = socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
    if (false === $option) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    echo 'Listening socket...' . PHP_EOL;
    $listen = socket_listen($socket); // слушаем сокет
    if (false === $listen) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    while (true) { // бесконечный цикл ожидания подключений
        echo 'Waiting for connections...' . PHP_EOL;
        $connect = socket_accept($socket); // зависаем, пока не получим ответа
        if ($connect !== false) {
            echo 'Client connected...' . PHP_EOL;
            echo 'Send message to client...' . PHP_EOL;
            socket_write($connect, 'Hello, Client!');
        } else {
            echo 'Error: ' . socket_strerror(socket_last_error()) . PHP_EOL;
            usleep(1000);
        }

        // останавливаем сервер после $limit секунд
        if ($limit && (time() - $starttime > $limit)) {
            echo 'Closing connection...' . PHP_EOL;
            socket_close($socket);
            echo 'SERVER STOP' . PHP_EOL;
            return;
        }
    }
}

error_reporting(E_ALL); // выводим все ошибки и предупреждения
set_time_limit(0);      // бесконечное время работы скрипта
ob_implicit_flush();    // включаем вывод без буферизации

// Запускаем сервер в работу, завершение работы через 60 секунд
SocketServer(60);

Запустим его в работу:

> php.exe -f simple.php
SERVER START
Socket create...
Socket bind...
Set option...
Listening socket...
Waiting for connections...

Попробуем пообщаться с сервером с помощью telnet:

> telnet

Получив приглашение telnet, даем команду:

> open 127.0.0.1 7777

И видим сообщение от сервера:

Наш сервер в другом окне тоже встрепенулся:

WebSocket сервер

Протокол WebSocket работает над TCP. Это означает, что при соединении браузер отправляет по HTTP специальные заголовки, спрашивая: «Поддерживает ли сервер WebSocket?». Если сервер в ответных заголовках отвечает «Да, поддерживаю», то дальше HTTP прекращается и общение идёт на специальном протоколе WebSocket, который уже не имеет с HTTP ничего общего.

GET /chat HTTP/1.1
Host: websocket.server.com
Upgrade: websocket
Connection: Upgrade
Origin: http://www.example.com
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13

Здесь GET и Host — стандартные HTTP-заголовки, а Upgrade и Connection указывают, что браузер хочет перейти на WebSocket.

Сервер может проанализировать эти заголовки и решить, разрешает ли он WebSocket с данного домена Origin. Ответ сервера, если он понимает и разрешает WebSocket-подключение:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Для тестирования работы сервера нам нужен клиент:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Простой WebSocket клиент</title>
    <link rel="stylesheet" href="style.css" type="text/css" />
    <script src="socket.js" type="text/javascript"></script>
</head>
<body>
    <div>
        <span>Сервер</span>
        <input id="server" type="text" value="" />
    </div>
    <div>
        <input id="connect" type="button" value="Установить соединение" />
        <input id="disconnect" type="button" value="Разорвать соединение" />
    </div>
    <div>
        <span>Сообщение</span>
        <input id="message" type="text" value="" />
        <input id="send-msg" type="button" value="Отправить сообщение" />
    </div>
    <div>
        <span>Информация</span>
        <div id="socket-info"></div>
    </div>
</body>
</html>
window.addEventListener('DOMContentLoaded', function () {

    var socket;

    // показать сообщение в #socket-info
    function showMessage(message) {
        var div = document.createElement('div');
        div.appendChild(document.createTextNode(message));
        document.getElementById('socket-info').appendChild(div);
    }

    /*
     * Установить соединение с сервером и назначить обработчики событий
     */
    document.getElementById('connect').onclick = function () {
        // новое соединение открываем, если старое соединение закрыто
        if (socket === undefined || socket.readyState !== 1) {
            socket = new WebSocket(document.getElementById('server').value);
        } else {
            showMessage('Надо закрыть уже имеющееся соединение');
        }

        /*
         * четыре функции обратного вызова: одна при получении данных и три – при изменениях в состоянии соединения
         */
        socket.onmessage = function (event) { // при получении данных от сервера
            showMessage('Получено сообщение от сервера: ' + event.data);
        }
        socket.onopen = function () { // при установке соединения с сервером
            showMessage('Соединение с сервером установлено');
        }
        socket.onerror = function(error) { // если произошла какая-то ошибка
            showMessage('Произошла ошибка: ' + error.message);
        };
        socket.onclose = function(event) { // при закрытии соединения с сервером
            showMessage('Соединение с сервером закрыто');
            if (event.wasClean) {
                showMessage('Соединение закрыто чисто');
            } else {
                showMessage('Обрыв соединения'); // например, «убит» процесс сервера
            }
            showMessage('Код: ' + event.code + ', причина: ' + event.reason);
        };
    };

    /*
     * Отправка сообщения серверу
     */
    document.getElementById('send-msg').onclick = function () {
        if (socket !== undefined && socket.readyState === 1) {
            var message = document.getElementById('message').value;
            socket.send(message);
            showMessage('Отправлено сообщение серверу: ' + message);
        } else {
            showMessage('Невозможно отправить сообщение, нет соединения');
        }
    };

    /*
     * Закрыть соединение с сервером
     */
    document.getElementById('disconnect').onclick = function () {
        if (socket !== undefined && socket.readyState === 1) {
            socket.close();
        } else {
            showMessage('Соединение с сервером уже было закрыто');
        }
    };

});
body > div {
    margin-bottom: 15px;
    overflow: hidden;
}
span {
    display: block;
    margin-bottom: 2px;
}
input {
    padding: 5px;
    box-sizing: border-box;
}
input[type="text"] {
    width: 100%;
}
input[type="button"] {
    width: 25%;
    float: left;
    margin-top: 5px;
    margin-right: 5px;
}
div#socket-info {
    padding: 5px;
    border: 1px solid #ddd;
}

Проверим его в работе. Открываем HTML-страницу в браузере и заполняем первое поле «Сервер»:

ws://echo.websocket.org

Это гарантированно работающий WebSocket echo-сервер, которые отправляет все сообщения обратно. Жмем кнопку «Установить соединение», набираем текст сообщения в поле «Сообщение», жмем кнопку «Отправить сообщение»:

А теперь код WebSocket сервера на PHP:

<?php
/**
 * Класс WebSocket сервера
 */
class WebSocketServer {

    /**
     * Функция вызывается, когда получено сообщение от клиента
     */
    public $handler;

    /**
     * IP адрес сервера
     */
    private $ip;
    /**
     * Порт сервера
     */
    private $port;
    /**
     * Сокет для принятия новых соединений, прослушивает указанный порт
     */
    private $connection;
    /**
     * Для хранения всех подключений, принятых слушающим сокетом
     */
    private $connects;

    /**
     * Ограничение по времени работы сервера
     */
    private $timeLimit = 0;
    /**
     * Время начала работы сервера
     */
    private $startTime;
    /**
     * Выводить сообщения в консоль?
     */
    private $verbose = false;
    /**
     * Записывать сообщения в log-файл?
     */
    private $logging = false;
    /**
     * Имя log-файла
     */
    private $logFile = 'ws-log.txt';
    /**
     * Ресурс log-файла
     */
    private $resource;


    public function __construct($ip = '127.0.0.1', $port = 7777) {
        $this->ip = $ip;
        $this->port = $port;

        // эта функция вызывается, когда получено сообщение от клиента;
        // при создании экземпляра класса должна быть переопределена
        $this->handler = function($connection, $data) {
            $message = '[' . date('r') . '] Получено сообщение от клиента: ' . $data . PHP_EOL;
            if ($this->verbose) {
                echo $message;
            }
            if ($this->logging) {
                fwrite($this->resource, $message);
            }
        };
    }

    public function __destruct() {
        if (is_resource($this->connection)) {
            $this->stopServer();
        }
        if ($this->logging) {
            fclose($this->resource);
        }
    }

    /**
     * Дополнительные настройки для отладки
     */
    public function settings($timeLimit = 0, $verbose = false, $logging = false, $logFile = 'ws-log.txt') {
        $this->timeLimit = $timeLimit;
        $this->verbose = $verbose;
        $this->logging = $logging;
        $this->logFile = $logFile;
        if ($this->logging) {
            $this->resource = fopen($this->logFile, 'a');
        }
    }

    /**
     * Выводит сообщение в консоль и/или записывает в лог-файл
     */
    private function debug($message) {
        $message = '[' . date('r') . '] ' . $message . PHP_EOL;
        if ($this->verbose) {
            echo $message;
        }
        if ($this->logging) {
            fwrite($this->resource, $message);
        }
    }

    /**
     * Отправляет сообщение клиенту
     */
    public static function response($connect, $data) {
        socket_write($connect, self::encode($data));
    }

    /**
     * Запускает сервер в работу
     */
    public function startServer() {

        $this->debug('Try start server...');

        $this->connection = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

        if (false === $this->connection) {
            $this->debug('Error socket_create(): ' . socket_strerror(socket_last_error()));
            return;
        }

        $bind = socket_bind($this->connection, $this->ip, $this->port); // привязываем к ip и порту
        if (false === $bind) {
            $this->debug('Error socket_bind(): ' . socket_strerror(socket_last_error()));
            return;
        }

        // разрешаем использовать один порт для нескольких соединений
        $option = socket_set_option($this->connection, SOL_SOCKET, SO_REUSEADDR, 1);
        if (false === $option) {
            $this->debug('Error socket_set_option(): ' . socket_strerror(socket_last_error()));
            return;
        }

        $listen = socket_listen($this->connection); // слушаем сокет
        if (false === $listen) {
            $this->debug('Error socket_listen(): ' . socket_strerror(socket_last_error()));
            return;
        }

        $this->debug('Server is running...');

        $this->connects = array($this->connection);
        $this->startTime = time();

        while (true) {

            $this->debug('Waiting for connections...');

            // создаем копию массива, так что массив $this->connects не будет изменен функцией socket_select()
            $read = $this->connects;
            $write = $except = null;

            /*
             * Сокет $this->connection только прослушивает порт на предмет новых соединений. Как только поступило
             * новое соединение, мы создаем новый ресурс сокета с помощью socket_accept() и помещаем его в массив
             * $this->connects для дальнейшего чтения из него.
             */

            if ( ! socket_select($read, $write, $except, null)) { // ожидаем сокеты, доступные для чтения (без таймаута)
                break;
            }

            // если слушающий сокет есть в массиве чтения, значит было новое соединение
            if (in_array($this->connection, $read)) {
                // принимаем новое соединение и производим рукопожатие
                if (($connect = socket_accept($this->connection)) && $this->handshake($connect)) {
                    $this->debug('New connection accepted');
                    $this->connects[] = $connect; // добавляем его в список необходимых для обработки
                }
                // удаляем слушающий сокет из массива для чтения
                unset($read[ array_search($this->connection, $read) ]);
            }

            foreach ($read as $connect) { // обрабатываем все соединения, в которых есть данные для чтения
                $data = socket_read($connect, 100000);
                $decoded = self::decode($data);
                // если клиент не прислал данных или хочет разорвать соединение
                if (false === $decoded || 'close' === $decoded['type']) {
                    $this->debug('Connection closing');
                    socket_write($connect, self::encode('  Closed on client demand', 'close'));
                    socket_shutdown($connect);
                    socket_close($connect);
                    unset($this->connects[ array_search($connect, $this->connects) ]);
                    $this->debug('Closed successfully');
                    continue;
                }
                // получено сообщение от клиента, вызываем пользовательскую
                // функцию, чтобы обработать полученные данные
                if (is_callable($this->handler)) {
                    call_user_func($this->handler, $connect, $decoded['payload']);
                }
            }

            // если истекло ограничение по времени, останавливаем сервер
            if ($this->timeLimit && time() - $this->startTime > $this->timeLimit) {
                $this->debug('Time limit. Stopping server.');
                $this->stopServer();
                return;
            }

        }

    }

    /**
     * Останавливает работу сервера
     */
    public function stopServer() {
        // закрываем слушающий сокет
        socket_close($this->connection);
        if (!empty($this->connects)) { // отправляем все клиентам сообщение о разрыве соединения
            foreach ($this->connects as $connect) {
                if (is_resource($connect)) {
                    socket_write($connect, self::encode('  Closed on server demand', 'close'));
                    socket_shutdown($connect);
                    socket_close($connect);
                }
            }
        }
    }

    /**
     * Для кодирования сообщений перед отправкой клиенту
     */
    private static function encode($payload, $type = 'text', $masked = false) {
        $frameHead = array();
        $payloadLength = strlen($payload);

        switch ($type) {
            case 'text':
                // first byte indicates FIN, Text-Frame (10000001):
                $frameHead[0] = 129;
                break;
            case 'close':
                // first byte indicates FIN, Close Frame(10001000):
                $frameHead[0] = 136;
                break;
            case 'ping':
                // first byte indicates FIN, Ping frame (10001001):
                $frameHead[0] = 137;
                break;
            case 'pong':
                // first byte indicates FIN, Pong frame (10001010):
                $frameHead[0] = 138;
                break;
        }

        // set mask and payload length (using 1, 3 or 9 bytes)
        if ($payloadLength > 65535) {
            $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8);
            $frameHead[1] = ($masked === true) ? 255 : 127;
            for ($i = 0; $i < 8; $i++) {
                $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
            }
            // most significant bit MUST be 0
            if ($frameHead[2] > 127) {
                return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)');
            }
        } elseif ($payloadLength > 125) {
            $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8);
            $frameHead[1] = ($masked === true) ? 254 : 126;
            $frameHead[2] = bindec($payloadLengthBin[0]);
            $frameHead[3] = bindec($payloadLengthBin[1]);
        } else {
            $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
        }

        // convert frame-head to string:
        foreach (array_keys($frameHead) as $i) {
            $frameHead[$i] = chr($frameHead[$i]);
        }
        if ($masked === true) {
            // generate a random mask:
            $mask = array();
            for ($i = 0; $i < 4; $i++) {
                $mask[$i] = chr(rand(0, 255));
            }
            $frameHead = array_merge($frameHead, $mask);
        }
        $frame = implode('', $frameHead);

        // append payload to frame:
        for ($i = 0; $i < $payloadLength; $i++) {
            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
        }

        return $frame;
    }

    /**
     * Для декодирования сообщений, полученных от клиента
     */
    private static function decode($data) {
        if ( ! strlen($data)) {
            return false;
        }

        $unmaskedPayload = '';
        $decodedData = array();

        // estimate frame type:
        $firstByteBinary = sprintf('%08b', ord($data[0]));
        $secondByteBinary = sprintf('%08b', ord($data[1]));
        $opcode = bindec(substr($firstByteBinary, 4, 4));
        $isMasked = ($secondByteBinary[0] == '1') ? true : false;
        $payloadLength = ord($data[1]) & 127;

        // unmasked frame is received:
        if (!$isMasked) {
            return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)');
        }

        switch ($opcode) {
            // text frame:
            case 1:
                $decodedData['type'] = 'text';
                break;
            case 2:
                $decodedData['type'] = 'binary';
                break;
            // connection close frame:
            case 8:
                $decodedData['type'] = 'close';
                break;
            // ping frame:
            case 9:
                $decodedData['type'] = 'ping';
                break;
            // pong frame:
            case 10:
                $decodedData['type'] = 'pong';
                break;
            default:
                return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)');
        }

        if ($payloadLength === 126) {
            $mask = substr($data, 4, 4);
            $payloadOffset = 8;
            $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset;
        } elseif ($payloadLength === 127) {
            $mask = substr($data, 10, 4);
            $payloadOffset = 14;
            $tmp = '';
            for ($i = 0; $i < 8; $i++) {
                $tmp .= sprintf('%08b', ord($data[$i + 2]));
            }
            $dataLength = bindec($tmp) + $payloadOffset;
            unset($tmp);
        } else {
            $mask = substr($data, 2, 4);
            $payloadOffset = 6;
            $dataLength = $payloadLength + $payloadOffset;
        }

        /**
         * We have to check for large frames here. socket_recv cuts at 1024 bytes
         * so if websocket-frame is > 1024 bytes we have to wait until whole
         * data is transferd.
         */
        if (strlen($data) < $dataLength) {
            return false;
        }

        if ($isMasked) {
            for ($i = $payloadOffset; $i < $dataLength; $i++) {
                $j = $i - $payloadOffset;
                if (isset($data[$i])) {
                    $unmaskedPayload .= $data[$i] ^ $mask[$j % 4];
                }
            }
            $decodedData['payload'] = $unmaskedPayload;
        } else {
            $payloadOffset = $payloadOffset - 4;
            $decodedData['payload'] = substr($data, $payloadOffset);
        }

        return $decodedData;
    }

    /**
     * «Рукопожатие», т.е. отправка заголовков согласно протоколу WebSocket
     */
    private function handshake($connect) {

        $info = array();

        $data = socket_read($connect, 1000);
        $lines = explode("rn", $data);
        foreach ($lines as $i => $line) {
            if ($i) {
                if (preg_match('/A(S+): (.*)z/', $line, $matches)) {
                    $info[$matches[1]] = $matches[2];
                }
            } else {
                $header = explode(' ', $line);
                $info['method'] = $header[0];
                $info['uri'] = $header[1];
            }
            if (empty(trim($line))) break;
        }

        // получаем адрес клиента
        $ip = $port = null;
        if ( ! socket_getpeername($connect, $ip, $port)) {
            return false;
        }
        $info['ip'] = $ip;
        $info['port'] = $port;

        if (empty($info['Sec-WebSocket-Key'])) {
            return false;
        }

        // отправляем заголовок согласно протоколу вебсокета
        $SecWebSocketAccept = 
            base64_encode(pack('H*', sha1($info['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
        $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshakern" .
                   "Upgrade: websocketrn" .
                   "Connection: Upgradern" .
                   "Sec-WebSocket-Accept:".$SecWebSocketAccept."rnrn";
        socket_write($connect, $upgrade);

        return true;

    }

}

Для тестирования напишем небольшой PHP-скрипт, который запускает в работу сервер и все сообщения клиента отправляет обратно (echo-сервер):

<?php 
error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();

require 'WebSocketServer.class.php';

$server = new WebSocketServer('127.0.0.1', 7777);
// максимальное время работы 100 секунд, выводить сообщения в консоль
$server->settings(100, true);

// эта функция вызывается, когда получено сообщение от клиента
$server->handler = function($connect, $data) {
    // полученные от клиента данные отправляем обратно
    WebSocketServer::response($connect, $data);
};

$server->startServer();

Запускаем скрипт в работу:

> php.exe -f echo-server.php
[Fri, 12 Oct 2018 15:08:13 +0300] Try start server...
[Fri, 12 Oct 2018 15:08:13 +0300] Server is running...
[Fri, 12 Oct 2018 15:08:13 +0300] Waiting for connections...

Еще один пример использования сервера — клиент отправляет команды, а сервер их выполняет:

<?php 
error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();

require 'WebSocketServer.class.php';

$server = new WebSocketServer('127.0.0.1', 7777);
// максимальное время работы 100 секунд, выводить сообщения в консоль
$server->settings(100, true);

// эта функция вызывается, когда получено сообщение от клиента
$server->handler = function($connect, $data) {
    // анализируем поступившую команду и даем ответ
    if ( ! in_array($data, array('date', 'time', 'country', 'city'))) {
        WebSocketServer::response($connect, 'Неизвестная команда');
        return;
    }
    switch ($data) {
        case 'date'   : $response = date('d.m.Y'); break;
        case 'time'   : $response = date('H:i:s'); break;
        case 'country': $response = 'Россия';      break;
        case 'city'   : $response = 'Москва';      break;
    }
    WebSocketServer::response($connect, $response);
};

$server->startServer();

Альтернативная реализация WebSocket сервера с использованием функций для работы с потоками:

<?php
/**
 * Класс WebSocket сервера
 */
class WebSocketServer {

    /**
     * Функция вызывается, когда получено сообщение от клиента
     */
    public $handler;

    /**
     * IP адрес сервера
     */
    private $ip;
    /**
     * Порт сервера
     */
    private $port;
    /**
     * Для хранения слушающего сокета потока
     */
    private $connection;
    /**
     * Для хранения всех подключений
     */
    private $connects;

    /**
     * Ограничение по времени работы сервера
     */
    private $timeLimit = 0;
    /**
     * Время начала работы сервера
     */
    private $startTime;
    /**
     * Выводить сообщения в консоль?
     */
    private $verbose = false;
    /**
     * Записывать сообщения в log-файл?
     */
    private $logging = false;
    /**
     * Имя log-файла
     */
    private $logFile = 'ws-log.txt';
    /**
     * Ресурс log-файла
     */
    private $resource;


    public function __construct($ip = '127.0.0.1', $port = 7777) {
        $this->ip = $ip;
        $this->port = $port;

        // эта функция вызывается, когда получено сообщение от клиента;
        // при создании экземпляра класса должна быть переопределена
        $this->handler = function($connection, $data) {
            $message = '[' . date('r') . '] Получено сообщение от клиента: ' . $data . PHP_EOL;
            if ($this->verbose) {
                echo $message;
            }
            if ($this->logging) {
                fwrite($this->resource, $message);
            }
        };
    }

    public function __destruct() {
        if (is_resource($this->connection)) {
            $this->stopServer();
        }
        if ($this->logging) {
            fclose($this->resource);
        }
    }

    /**
     * Дополнительные настройки для отладки
     */
    public function settings($timeLimit = 0, $verbose = false, $logging = false, $logFile = 'ws-log.txt') {
        $this->timeLimit = $timeLimit;
        $this->verbose = $verbose;
        $this->logging = $logging;
        $this->logFile = $logFile;
        if ($this->logging) {
            $this->resource = fopen($this->logFile, 'a');
        }
    }

    /**
     * Выводит сообщение в консоль или записывает в лог-файл
     */
    private function debug($message) {
        $message = '[' . date('r') . '] ' . $message . PHP_EOL;
        if ($this->verbose) {
            echo $message;
        }
        if ($this->logging) {
            fwrite($this->resource, $message);
        }
    }

    /**
     * Отправляет сообщение клиенту
     */
    public static function response($connect, $data) {
        fwrite($connect, self::encode($data));
    }

    /**
     * Запускает сервер в работу
     */
    public function startServer() {
        
        $this->debug('Try start server...');

        $this->connection = stream_socket_server('tcp://' . $this->ip . ':' . $this->port, $errno, $errstr);
        
        if ( ! $this->connection) {
            $this->debug('Cannot start server: ' .$errstr. '(' .$errno. ')');
            return false;
        }

        $this->debug('Server is running...');

        $this->connects = array();
        $this->startTime = time();

        while (true) {

            $this->debug('Waiting for connections...');

            // формируем массив прослушиваемых сокетов
            $read = $this->connects;
            $read[] = $this->connection;
            $write = $except = null;

            if ( ! stream_select($read, $write, $except, null)) { // ожидаем сокеты доступные для чтения (без таймаута)
                break;
            }

            if (in_array($this->connection, $read)) { // есть новое соединение
                // принимаем новое соединение и производим рукопожатие
                if (($connect = stream_socket_accept($this->connection, -1)) && $this->handshake($connect)) {
                    $this->debug('New connection accepted');
                    $this->connects[] = $connect; // добавляем его в список необходимых для обработки
                }
                unset($read[ array_search($this->connection, $read) ]);
            }

            foreach ($read as $connect) { // обрабатываем все соединения
                $data = fread($connect, 100000);
                $decoded = self::decode($data);
                // если клиент не прислал данных или хочет разорвать соединение
                if (false === $decoded || 'close' === $decoded['type']) {
                    $this->debug('Connection closing');
                    fwrite($connect, self::encode('  Closed on client demand', 'close'));
                    fclose($connect);
                    unset($this->connects[ array_search($connect, $this->connects) ]);
                    $this->debug('Closed successfully');
                    continue;
                }
                // получено сообщение от клиента, вызываем пользовательскую
                // функцию, чтобы обработать полученные данные
                if (is_callable($this->handler)) {
                    call_user_func($this->handler, $connect, $decoded['payload']);
                }
            }

            // если истекло ограничение по времени, останавливаем сервер
            if ($this->timeLimit && time() - $this->startTime > $this->timeLimit) {
                $this->debug('Time limit. Stopping server.');
                $this->stopServer();
                return;
            }
        }
    }

    /**
     * Останавливает работу сервера
     */
    public function stopServer() {
        fclose($this->connection); // закрываем слушающий сокет
        if (!empty($this->connects)) { // отправляем все клиентам сообщение о разрыве соединения
            foreach ($this->connects as $connect) {
                if (is_resource($connect)) {
                    fwrite($connect, self::encode('  Closed on server demand', 'close'));
                    fclose($connect);
                }
            }
        }
    }

    /**
     * Для кодирования сообщений перед отправкой клиенту
     */
    private static function encode($payload, $type = 'text', $masked = false) {
        $frameHead = array();
        $payloadLength = strlen($payload);

        switch ($type) {
            case 'text':
                // first byte indicates FIN, Text-Frame (10000001):
                $frameHead[0] = 129;
                break;
            case 'close':
                // first byte indicates FIN, Close Frame(10001000):
                $frameHead[0] = 136;
                break;
            case 'ping':
                // first byte indicates FIN, Ping frame (10001001):
                $frameHead[0] = 137;
                break;
            case 'pong':
                // first byte indicates FIN, Pong frame (10001010):
                $frameHead[0] = 138;
                break;
        }

        // set mask and payload length (using 1, 3 or 9 bytes)
        if ($payloadLength > 65535) {
            $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8);
            $frameHead[1] = ($masked === true) ? 255 : 127;
            for ($i = 0; $i < 8; $i++) {
                $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
            }
            // most significant bit MUST be 0
            if ($frameHead[2] > 127) {
                return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)');
            }
        } elseif ($payloadLength > 125) {
            $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8);
            $frameHead[1] = ($masked === true) ? 254 : 126;
            $frameHead[2] = bindec($payloadLengthBin[0]);
            $frameHead[3] = bindec($payloadLengthBin[1]);
        } else {
            $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
        }

        // convert frame-head to string:
        foreach (array_keys($frameHead) as $i) {
            $frameHead[$i] = chr($frameHead[$i]);
        }
        if ($masked === true) {
            // generate a random mask:
            $mask = array();
            for ($i = 0; $i < 4; $i++) {
                $mask[$i] = chr(rand(0, 255));
            }
            $frameHead = array_merge($frameHead, $mask);
        }
        $frame = implode('', $frameHead);

        // append payload to frame:
        for ($i = 0; $i < $payloadLength; $i++) {
            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
        }

        return $frame;
    }

    /**
     * Для декодирования сообщений, полученных от клиента
     */
    private static function decode($data) {
        if ( ! strlen($data)) {
            return false;
        }

        $unmaskedPayload = '';
        $decodedData = array();

        // estimate frame type:
        $firstByteBinary = sprintf('%08b', ord($data[0]));
        $secondByteBinary = sprintf('%08b', ord($data[1]));
        $opcode = bindec(substr($firstByteBinary, 4, 4));
        $isMasked = ($secondByteBinary[0] == '1') ? true : false;
        $payloadLength = ord($data[1]) & 127;

        // unmasked frame is received:
        if (!$isMasked) {
            return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)');
        }

        switch ($opcode) {
            // text frame:
            case 1:
                $decodedData['type'] = 'text';
                break;
            case 2:
                $decodedData['type'] = 'binary';
                break;
            // connection close frame:
            case 8:
                $decodedData['type'] = 'close';
                break;
            // ping frame:
            case 9:
                $decodedData['type'] = 'ping';
                break;
            // pong frame:
            case 10:
                $decodedData['type'] = 'pong';
                break;
            default:
                return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)');
        }

        if ($payloadLength === 126) {
            $mask = substr($data, 4, 4);
            $payloadOffset = 8;
            $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset;
        } elseif ($payloadLength === 127) {
            $mask = substr($data, 10, 4);
            $payloadOffset = 14;
            $tmp = '';
            for ($i = 0; $i < 8; $i++) {
                $tmp .= sprintf('%08b', ord($data[$i + 2]));
            }
            $dataLength = bindec($tmp) + $payloadOffset;
            unset($tmp);
        } else {
            $mask = substr($data, 2, 4);
            $payloadOffset = 6;
            $dataLength = $payloadLength + $payloadOffset;
        }

        /**
         * We have to check for large frames here. socket_recv cuts at 1024 bytes
         * so if websocket-frame is > 1024 bytes we have to wait until whole
         * data is transferd.
         */
        if (strlen($data) < $dataLength) {
            return false;
        }

        if ($isMasked) {
            for ($i = $payloadOffset; $i < $dataLength; $i++) {
                $j = $i - $payloadOffset;
                if (isset($data[$i])) {
                    $unmaskedPayload .= $data[$i] ^ $mask[$j % 4];
                }
            }
            $decodedData['payload'] = $unmaskedPayload;
        } else {
            $payloadOffset = $payloadOffset - 4;
            $decodedData['payload'] = substr($data, $payloadOffset);
        }

        return $decodedData;
    }

    /**
     * «Рукопожатие», т.е. отправка заголовков согласно протоколу WebSocket
     */
    private function handshake($connect) {
        $info = array();

        $line = fgets($connect);
        $header = explode(' ', $line);
        $info['method'] = $header[0];
        $info['uri'] = $header[1];

        // считываем заголовки из соединения
        while ($line = rtrim(fgets($connect))) {
            if (preg_match('/A(S+): (.*)z/', $line, $matches)) {
                $info[$matches[1]] = $matches[2];
            } else {
                break;
            }
        }

        // получаем адрес клиента
        $address = explode(':', stream_socket_get_name($connect, true));
        $info['ip'] = $address[0];
        $info['port'] = $address[1];

        if (empty($info['Sec-WebSocket-Key'])) {
            return false;
        }

        // отправляем заголовок согласно протоколу вебсокета
        $SecWebSocketAccept = 
            base64_encode(pack('H*', sha1($info['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
        $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshakern" .
                   "Upgrade: websocketrn" .
                   "Connection: Upgradern" .
                   "Sec-WebSocket-Accept:".$SecWebSocketAccept."rnrn";
        fwrite($connect, $upgrade);

        return $info;
    }

}

Дополнительно

  • Исходные коды сервера на GITHUB.COM
  • Описание WebSocket на сайте JAVASCRIPT.RU

Поиск:
HandShake • JavaScript • PHP • Server • Socket • Web-разработка • WebSocket • Клиент • Протокол • Сервер • Сокет

Well, this is pretty useless, but it is possible. But again its pretty.. uesless. This tutorial will hopefully help you to better understand how a simple webserver could work and that it’s no problem writing one in PHP. But again using this in production would be trying to eat a soup with a fork. So just, …. just don’t. Let me shortly explain why this is not a that good idea.

PHP is a scripting language that simply is not really designed for such tasks.

A webserver is a long running process which PHP is simply not made for. Also PHP does not natively support multi-threading ( pthreads ), which will make developing a good performing web-server a really hard/impossible task. PHPs memory allocations can be quite wasteful and even if the interpreter is incredibly fast it still has the overhead of an interpreter.
But these things might change in the future.
If you met some programmers, there was at least one who made jokes about how bad PHP is and there are obvious reasons why PHP became such an industry inside joke. But the language and its eco-system has evolved, …alot.
There are still people who can’t see behind the jokes, a lot of them either didn’t touch PHP for at least 10 years or copycat other peoples opinions to be a cool kid.
I would bet a few bucks that things like long running php applications with embedded web servers and other things will show up more and more.

Use the Source, Luke

The entire source is available on Github: https://github.com/ClanCatsStation/PHPWebserver

Basics

Enough warnings about doing what I’m about to explain how to do. let’s get started eating the god damit soup with god damit chopsticks.

How does a webserver basically work?

  1. The server listens for incoming connections.
  2. A client connects to the server.
  3. The server accepts the connection and handles the input.
  4. The server responds to the client.

Structure

I’m going to build this as an abstraction of the Request and Response. There are many ways designing an application, As a first step I prefer writing the part of application that consumes my API. Followed by writing the actual API.

So later I wan’t to be able to use the thing like this:

// create a new server instance
$server = new Server( '127.0.0.1', 80 );

// start listening
$server->listen( function( Request $request ) 
{
    return new Response( 'Hello Dude' );
});

The directory structure (just as help):

server  
composer.json  
src/Request.php  
src/Response.php  
src/Server.php  
src/Exception.php  

I’m going to use PSR-4 autoloading. So start by creating a new composer.json file. ( Don’t forget to run composer install afterwards )

{
    "autoload":
    {
        "psr-4": 
        {
            "ClanCats\Station\PHPServer\": "src/"    
        }
    }
}

 Initializing

Next we create the script file (server) which will take care of starting the server. We don’t add the .php extension. So that bash knows what do do, add the following header:

#!/usr/bin/env php
<?php  

We are always going to bind the server to localhost but we wan’t to be able to define the port as command line argument.

// we never need the first argument
array_shift( $argv );

// the next argument should be the port if not use 80
if ( empty( $argv ) )  
{
    $port = 80;
} else {
    $port = array_shift( $argv );
}

This allows as to start the server later like this:

$ sudo php server 8008

If we combine that with what we defined before we get the following file:

#!/usr/bin/env php
<?php  
use ClanCatsStationPHPServerServer;  
use ClanCatsStationPHPServerRequest;  
use ClanCatsStationPHPServerResponse;

// we never need the first argument
array_shift( $argv );

// the next argument should be the port if not use 80
if ( empty( $argv ) )  
{
    $port = 80;
} else {
    $port = array_shift( $argv );
}

require 'vendor/autoload.php';

// create a new server instance
$server = new Server( '127.0.0.1', $port );

// start listening
$server->listen( function( Request $request ) 
{
    return new Response( 'Hello Dude' );
});

Server Object

Next lets create our src/Server.php file which is going to handle the socket.

<?php namespace ClanCatsStationPHPServer;

class Server  
{
}

Our server class is going to hold the host, the port and the socket resource so add the following class variables.

protected $host = null;  
protected $port = null;  
protected $socket = null;  

 Create a socket

To bind a socket we need to have one so create the createSocket function.

protected function createSocket()  
{
    $this->socket = socket_create( AF_INET, SOCK_STREAM, 0 );
}

The first argument specifies the domain / protocol family of the socket. AF_INET is for IPv4 TCP and UDP protocols.

The second argument defines the communication type of the socket. SOCK_STREAM is a simple full-duplex connection based byte stream.

The third argument sets the protocol.

Bind the socket

This is pretty self explaining. The socket_bind function returns false when something goes wrong. Because this should never happen we throw an exception with the socket error message.

protected function bind()  
{
    if ( !socket_bind( $this->socket, $this->host, $this->port ) )
    {
        throw new Exception( 'Could not bind: '.$this->host.':'.$this->port.' - '.socket_strerror( socket_last_error() ) );
    }
}

Create and bind the socket on construct

We could also create a connect function, but to keep stuff simple we just do it in the constructor.

public function __construct( $host, $port )  
{
    $this->host = $host;
    $this->port = (int) $port;

    // create a socket
    $this->createSocket();

    // bind the socket
    $this->bind();
}

Listen for connections

Beacuse I don’t want to split this function in 20 segments just to explain what happens, I added my bullshit to the comments.

public function listen( $callback )  
{
    // check if the callback is valid. Throw an exception
    // if not.
    if ( !is_callable( $callback ) )
    {
        throw new Exception('The given argument should be callable.');
    }

    // Now here comes the thing that makes this process
    // long, infinite, never ending..
    while ( 1 ) 
    {
        // listen for connections
        socket_listen( $this->socket );

        // try to get the client socket resource
        // if false we got an error close the connection and skip
        if ( !$client = socket_accept( $this->socket ) ) 
        {
            socket_close( $client ); continue;
        }

        // create new request instance with the clients header.
        // In the real world of course you cannot just fix the max size to 1024..
        $request = Request::withHeaderString( socket_read( $client, 1024 ) );

        // execute the callback 
        $response = call_user_func( $callback, $request );

        // check if we really recived an Response object
        // if not return a 404 response object
        if ( !$response || !$response instanceof Response )
        {
            $response = Response::error( 404 );
        }

        // make a string out of our response
        $response = (string) $response;

        // write the response to the client socket
        socket_write( $client, $response, strlen( $response ) );

        // close the connetion so we can accept new ones
        socket_close( $client );
    }
}

Request Object

Now it’s time to create the src/Request.php file which is going to handle the user input.

<?php namespace ClanCatsStationPHPServer;

class Request  
{
}

Our Request is going to hold the HTTP request method, the uri, parameters and headers. So add these class variables:

protected $method = null;  
protected $uri = null;  
protected $parameters = [];  
protected $headers = [];  

Now in our listen function we already pass the socket input / request header to the withHeaderString function. A http header looks like this:

GET / HTTP/1.1  
Host: 127.0.0.1:8008  
Connection: keep-alive  
Accept: text/html  
User-Agent: Chrome/41.0.2272.104  
Accept-Encoding: gzip, deflate, sdch  
Accept-Language: en-US,en;q=0.8,de;q=0.6  

So what we need to do is parse that data. The first line indicates the request method, uri and protocol. Followed by key, value header parameters.

public static function withHeaderString( $header )  
{
    // explode the string into lines.
    $lines = explode( "n", $header );

    // extract the method and uri
    list( $method, $uri ) = explode( ' ', array_shift( $lines ) );

    $headers = [];

    foreach( $lines as $line )
    {
        // clean the line
        $line = trim( $line );

        if ( strpos( $line, ': ' ) !== false )
        {
            list( $key, $value ) = explode( ': ', $line );
            $headers[$key] = $value;
        }
    }   

    // create new request object
    return new static( $method, $uri, $headers );
}

Our constructor recives $method, $uri and $headers. We could simply just assign them to a class variable. But for this example I want to split and parse the query parameters.

public function __construct( $method, $uri, $headers = [] )  
{
    $this->headers = $headers;
    $this->method = strtoupper( $method );

    // split uri and parameters string
    @list( $this->uri, $params ) = explode( '?', $uri );

    // parse the parmeters
    parse_str( $params, $this->parameters );
}

Create request getter methods

Because our class variables method, uri, parameters and headers are protected we need to create some getters to make the request data accessible.

There is nothing specail with the method and uri getters. They just return..

public function method()  
{
    return $this->method;
}
public function uri()  
{
    return $this->uri;
}

Now the header and param getter should allow giving a default value. Which get return if no data with the given key is found.

public function header( $key, $default = null )  
{
    if ( !isset( $this->headers[$key] ) )
    {
        return $default;
    }

    return $this->headers[$key];
}
public function param( $key, $default = null )  
{
    if ( !isset( $this->parameters[$key] ) )
    {
        return $default;
    }

    return $this->parameters[$key];
}

Response Object

Being muted isn’t much fun. Of course we wan’t to be able to respond to our request. As you see in the listen function, the given callback has to return a Response object. Otherwise a 404 response is returend.

How does a http response look like? Actually pretty much the same as the request. We have a header and a body. And we will simply write them both into the socket to respond to the client.

Again this is not the optimal way for a solid implementation its an example..

Create a new file src/Response.php.

<?php namespace ClanCatsStationPHPServer;

class Response  
{
}

Status codes

404 in tha house! To be able to build our header string we need to know the http status codes. We could also set them manually, but who the hell wants to write stuff manually?

This array pretty much covers the http status codes definitions. Taken from CCF.

protected static $statusCodes = [  
    // Informational 1xx
    100 => 'Continue',
    101 => 'Switching Protocols',

    // Success 2xx
    200 => 'OK',
    201 => 'Created',
    202 => 'Accepted',
    203 => 'Non-Authoritative Information',
    204 => 'No Content',
    205 => 'Reset Content',
    206 => 'Partial Content',

    // Redirection 3xx
    300 => 'Multiple Choices',
    301 => 'Moved Permanently',
    302 => 'Found', // 1.1
    303 => 'See Other',
    304 => 'Not Modified',
    305 => 'Use Proxy',
    // 306 is deprecated but reserved
    307 => 'Temporary Redirect',

    // Client Error 4xx
    400 => 'Bad Request',
    401 => 'Unauthorized',
    402 => 'Payment Required',
    403 => 'Forbidden',
    404 => 'Not Found',
    405 => 'Method Not Allowed',
    406 => 'Not Acceptable',
    407 => 'Proxy Authentication Required',
    408 => 'Request Timeout',
    409 => 'Conflict',
    410 => 'Gone',
    411 => 'Length Required',
    412 => 'Precondition Failed',
    413 => 'Request Entity Too Large',
    414 => 'Request-URI Too Long',
    415 => 'Unsupported Media Type',
    416 => 'Requested Range Not Satisfiable',
    417 => 'Expectation Failed',

    // Server Error 5xx
    500 => 'Internal Server Error',
    501 => 'Not Implemented',
    502 => 'Bad Gateway',
    503 => 'Service Unavailable',
    504 => 'Gateway Timeout',
    505 => 'HTTP Version Not Supported',
    509 => 'Bandwidth Limit Exceeded'
];

Just a little thing aside there is a repository that covering the 7xx http status codes: https://github.com/joho/7XX-rfc They are hilarious :)

Response constructor

The general parameters our response object should implement are the http status, body and other headers.

protected $status = 200;  
protected $body = '';  
protected $headers = [];  

Body should be a must argument in the constructor, while the status and other headers should be optional. Also the constructor should set some default values like the current Date or the Server header.

public function __construct( $body, $status = null )  
{
    if ( !is_null( $status ) )
    {
        $this->status = $status;
    }

    $this->body = $body;

    // set inital headers
    $this->header( 'Date', gmdate( 'D, d M Y H:i:s T' ) );
    $this->header( 'Content-Type', 'text/html; charset=utf-8' );
    $this->header( 'Server', 'PHPServer/1.0.0 (Whateva)' );
}

To be able to add new header parameters to the object we need to create a setter method.

public function header( $key, $value )  
{
    $this->headers[ucfirst($key)] = $value;
}

ucfirst is loved by lazy folks like me. It uppercases ( if thats actually a word ) the first character of a string. This way you can create new responses like this:

$response = new Response( 'Hello World' );
$response->header( 'date', '13.09.1959' );

We made eveything so fancy abstracted but we cannot simply pass our response object to the socket writer. We need to build a string out of our data.

A http header response string will look like the following:

HTTP/1.1 200 OK  
Date: 13.09.1959  
Server: PHPServer  

We have all the data we need so we can create the following function:

public function buildHeaderString()  
{
    $lines = [];

    // response status 
    $lines[] = "HTTP/1.1 ".$this->status." ".static::$statusCodes[$this->status];

    // add the headers
    foreach( $this->headers as $key => $value )
    {
        $lines[] = $key.": ".$value;
    }

    return implode( " rn", $lines )."rnrn";
}

Let the magic happen

And because again we are all lazy fucks that don’t want to execute buildHeaderString just to build a header string, we create the magic __toString method that returns the entire string written to the open connection.

public function __toString()  
{
    return $this->buildHeaderString().$this->body;
}

Thats it!

Well thats everything, hopefully… You should now be able to start your server just like this:

$ sudo php server 8008

And access it with your browser:

http://127.0.0.1:8008/everything/you/want/?cool=yes  

post-scriptum

When I started this article I did not thought it would be that that long and would consume so much of my time. So I have to admit that I got a bit annoyed after the first hour or so. Please excuse that the quality is not consistent or even close to good. But I hope the tutorial and especially the source might still help people who work primarily with PHP and are interested to better understand how a Webserver works.

Is there a reason to implement an HTTP server in PHP (of all things)? There’s no threading, etc. It would be a pain… (unless this is some sort of academic thing…)

PHP 5.4 ships with a built-in webserver. Maybe that is what you’re looking for…


Update:

While I understand your motivation to learn this sort of stuff, I believe you’re on the wrong track trying this sort of stuff with PHP. PHP is not really designed for long running processes (like a server would be), it is not equipped for parallel processing (threads). Even multi-processing would require PCNTL, specifically pcntl_fork() and limit your educational walkabout to a Unix based system (which may not be a problem, though).

If your goal is to understand how servers deal with concurrency, I suggest playing with a language designed for that (Erlang, Go, Scala, …). Or play with a language that at least sort of emulates parallel processing (Python, Ruby, … [sort of, because of their GILs]).

If your goal is to understand HTTP (and let me tell you HTTP is a beast, if you’re going past HTTP/1.0 and want to do it right), fiddling with PHP may be fine, if it’s the only language you’re firm in. If so, have a look at the example code (chat server) in this (sadly German) article on Socket Servers in PHP to get the basic socket stuff running an concentrate on the actual HTTP.


Update 2:

To answer your question regarding the headers…
I have no clue how apache would fit into the scenario described in your question. But I see you’re using line-breaks to delimit headers and a double line-break to delimit headers from body. Unless you’ve saved your php file using rn as the default line-break (windows-style), you’re header-part is malformed and would thus be recognized as the body. Depending on the http client (user agent, may it be your browser, or curl, or whatever) this may be treated with «insert some default headers». Replace your line-breaks with rn and try again.

If your server is reachable from the internet, try some header test tools to verify your HTTP is sound. If it is localhost-only, see what curl -I http://ip-of-my-server:9000 spits out.

Пример #1 Пример использования сокетов: Простой TCP/IP сервер

Этот пример показывает работу простого сервера отзывов. Измените
переменные address и port
в соответствии с вашими настройками и выполните. Затем вы можете соединиться с
сервером с командой, похожей на: telnet 192.168.1.53
10000
(где адрес и порт должны соответствовать вашим
настройкам). Всё, что вы наберёте на клавиатуре, будет затем выведено на сервере
и отправлено вам обратно. Для отключения наберите ‘выход’.


#!/usr/local/bin/php -q
<?php
error_reporting
(E_ALL);/* Позволяет скрипту ожидать соединения бесконечно. */
set_time_limit(0);/* Включает скрытое очищение вывода так что мы получаем данные
 * как только они появляются. */
ob_implicit_flush();$address '192.168.1.53';
$port 10000;

if ((

$sock socket_create(AF_INETSOCK_STREAMSOL_TCP)) === false) {
    echo 
"Не удалось выполнить socket_create(): причина: " socket_strerror(socket_last_error()) . "n";
}

if (

socket_bind($sock$address$port) === false) {
    echo 
"Не удалось выполнить socket_bind(): причина: " socket_strerror(socket_last_error($sock)) . "n";
}

if (

socket_listen($sock5) === false) {
    echo 
"Не удалось выполнить socket_listen(): причина: " socket_strerror(socket_last_error($sock)) . "n";
}

do {
    if ((

$msgsock socket_accept($sock)) === false) {
        echo 
"Не удалось выполнить socket_accept(): причина: " socket_strerror(socket_last_error($sock)) . "n";
        break;
    }
    
/* Отправляем инструкции. */
    
$msg "nДобро пожаловать на тестовый сервер PHP. n" .
        
"Чтобы отключиться, наберите 'выход'. Чтобы выключить сервер, наберите 'выключение'.n";
    
socket_write($msgsock$msgstrlen($msg));

    do {
        if (

false === ($buf socket_read($msgsock2048PHP_NORMAL_READ))) {
            echo 
"Не удалось выполнить socket_read(): причина: " socket_strerror(socket_last_error($msgsock)) . "n";
            break 
2;
        }
        if (!
$buf trim($buf)) {
            continue;
        }
        if (
$buf == 'выход') {
            break;
        }
        if (
$buf == 'выключение') {
            
socket_close($msgsock);
            break 
2;
        }
        
$talkback "PHP: Вы сказали '$buf'.n";
        
socket_write($msgsock$talkbackstrlen($talkback));
        echo 
"$bufn";
    } while (
true);
    
socket_close($msgsock);
} while (
true);socket_close($sock);
?>

Пример #2 Пример использования сокетов: Простой TCP/IP клиент

Этот пример показывает использование простого одноразового HTTP-клиента. Он просто
соединяется со страницей, отправляет запрос HEAD, выводит ответ
и завершает работу.


<?php
error_reporting
(E_ALL);

echo 

"<h2>Соединение TCP/IP</h2>n";/* Получаем порт сервиса WWW. */
$service_port getservbyname('www''tcp');/* Получаем  IP адрес целевого хоста. */
$address gethostbyname('www.example.com');/* Создаём  TCP/IP сокет. */
$socket socket_create(AF_INETSOCK_STREAMSOL_TCP);
if (
$socket === false) {
    echo 
"Не удалось выполнить socket_create(): причина: " socket_strerror(socket_last_error()) . "n";
} else {
    echo 
"OK.n";
}

echo 

"Пытаемся соединиться с '$address' на порту '$service_port'...";
$result socket_connect($socket$address$service_port);
if (
$result === false) {
    echo 
"Не удалось выполнить socket_connect().nПричина: ($result) " socket_strerror(socket_last_error($socket)) . "n";
} else {
    echo 
"OK.n";
}
$in "HEAD / HTTP/1.1rn";
$in .= "Host: www.example.comrn";
$in .= "Connection: Closernrn";
$out '';

echo 

"Отправляем  HTTP HEAD запрос...";
socket_write($socket$instrlen($in));
echo 
"OK.n";

echo 

"Читаем ответ:nn";
while (
$out socket_read($socket2048)) {
    echo 
$out;
}

echo 

"Закрываем сокет...";
socket_close($socket);
echo 
"OK.nn";
?>

Вернуться к: Сокеты

Понравилась статья? Поделить с друзьями:
  • Как написать сервер гта 5 рп
  • Как написать сервер входящей почты
  • Как написать сеошный текст
  • Как написать сео текст для сайта
  • Как написать сео оптимизированную статью