Для передачи данных сервером на php клиенту можно использовать следующий алгоритм:
- Сервер php публикует данные в канал redis.
- Сервер node подписывается на события в соответствующем канале redis и при
наступлении события поступления данных публикует эти данные уже в
socket.io - Клиент подписывается на сообщения socket.io и обрабатывает их при поступлении
Исходный код проекта можно найти на github
Здесь я буду двигаться очень маленькими шагами.
В проекте будет использоваться связка nginx и php-fpm и начну я с настройки
nginx.
Настройка nginx
Начнем создавать docker-compose.yml
в корневой папке нашего проекта.
# docker-compose.yml
version: '3'
services:
nginx:
image: nginx
ports:
- 4400:80
Откроем в браузере: http://localhost:4400
и увидим стандартное приветствие
nginx.
Теперь настроим, чтобы nginx отдавал статическое содержимое папки
./www/public
.
Сначала создадим папки
mkdir -pv www/public
Создадим файл ./www/pulbic/index.html
<!-- www/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Создадим файл конфигурации nginx — nginx/conf/custom.conf
. Для начала
скопируем стандартный /etc/nginx/conf.d/default.conf
.
Изменим docker-compose.yml
services:
nginx:
image: nginx
+ volumes:
+ - ./nginx/conf/custom.conf:/etc/nginx/conf.d/default.conf
ports:
- 4400:80
Пересоздадим контейнер nginx
docker-compose up -d
И вновь наблюдаем по в браузере по адресу http://localhost:4400
стандартное
приветствие nginx.
Внесем изменения
docker-compose.yml
image: nginx
volumes:
- ./nginx/conf/custom.conf:/etc/nginx/conf.d/default.conf
+ - ./www:/www
ports:
- 4400:80
nginx/conf/custom.conf
#access_log /var/log/nginx/host.access.log main;
location / {
- root /usr/share/nginx/html;
+ root /www/public;
index index.html index.htm;
}
Теперь по адресу http://localhost:4400
отображается 'Hello World!' из файла
www/public/index.html
.
Прорывом это назвать сложно, но мы определенно двигаемся в нужном направлении.
Настройка php
Начнем с создания папок для хранения файлов настроек контейнера.
mkdir -pv php/conf
Далее создадим php/Dockerfile
FROM php:7-fpm
RUN apt-get -qq update && apt-get -qq install curl > /dev/null
ENV PHPREDIS_VERSION 3.0.0
RUN mkdir -p /usr/src/php/ext/redis && curl -L https://github.com/phpredis/phpredis/archive/$PHPREDIS_VERSION.tar.gz | tar xvz -C /usr/src/php/ext/redis --strip 1 && echo 'redis' >> /usr/src/php-available-exts && docker-php-ext-install redis
И внесем изменения в docker-compose.yml
- ./www:/www
ports:
- 4400:80
+ php:
+ build: ./php
+ volumes:
+ - ./www:/www
+ environment:
+ - REDIS_PASSWORD=${REDIS_PASSWORD}
Также нам нужно внести изменения в настройки nginx, чтобы файлы с расширением
.php
обрабатывались php-fpm
.
Изменим файл nginx/conf/custom.conf
следующим образом
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
-#location ~ \.php$ {
-# root html;
-# fastcgi_pass 127.0.0.1:9000;
-# fastcgi_index index.php;
-# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
-# include fastcgi_params;
-#}
+location ~ \.php$ {
+ root /www;
+ fastcgi_pass php:9000;
+ fastcgi_index index.php;
+ fastcgi_param REQUEST_METHOD $request_method;
+ fastcgi_param CONTENT_TYPE $content_type;
+ fastcgi_param CONTENT_LENGTH $content_length;
+ fastcgi_param SCRIPT_FILENAME /www/public/$fastcgi_script_name;
+ include fastcgi_params;
+}
Осталось создать файл www/public/info.php
со следующим кодом
<?php
phpinfo();
Перезапустим наш контейнер
docker-compose restart
И теперь по адресу http://localhost:4400/info.php
отображается информация о
настройках php.
Еще немного поэкспериментируем и создадим файл www/Test.php
<?php
class Test
{
public function prn()
{
echo 'Success';
}
}
А содержимое файла www/public/info.php
заменим на следующее:
<?php
require_once(
implode(
DIRECTORY_SEPARATOR,
[
dirname(__DIR__),
'Test.php'
]
)
);
$test = new Test();
$test->prn();
Теперь по адресу http://localhost:4400/info.php
отображается 'success', а это
означает, что php-fpm
доступны скрипты расположенные в папке www
, а через
браузер они недоступны. Т.е. мы продолжаем двигаться в нужном направлении.
Настройка redis
Это, пожалуй, самая короткая часть.
Redis в этом проекте не будет доступен из внешней сети, но защиту паролем
настроим.
Для этого создадим файл .env
REDIS_PASSWORD=eustatos
Внесем изменения в docker-compose.yml
build: ./php
volumes:
- ./www:/www
+ redis:
+ image: redis
+ command: ["sh", "-c", "exec redis-server --requirepass \"${REDIS_PASSWORD}\""]
Чтобы протестировать подключение к redis изменим файл www/public/info.php
<?php
$redis = new Redis();
// подключаемся к серверу redis
$redis->connect(
'redis',
6379
);
// авторизуемся. 'eustatos' - пароль, который мы задали в файле `.env`
$redis->auth($_ENV['REDIS_PASSWORD']);
// публикуем сообщение в канале 'eustatos'
$redis->publish(
'eustatos',
json_encode([
'test' => 'success'
])
);
// закрываем соединение
$redis->close();
Рестартуем контейнер
docker-compose restart
Теперь подключимся к серверу redis
docker-compose exec redis bash
Перейдем к командной строке. 'eustatos' — пароль, который мы ранее задали в
файле .env
# redis-cli -a eustatos
Подпишемся на канал 'eustatos' (название произвольное, чтобы все работало,
долно совпадать с названием канала, которое мы определили в файле
www/public/info.php
)
> subscribe eustatos
После всех этих приготовлений, переходим в браузере по адресу
http://localhost:4400/info.php
и наблюдаем, как в терминале, где мы
подключались к redis появляются примерно следующие строки:
1) "message"
2) "eustatos"
3) "{\"test\":\"success\"}"
Значит мы стали еще ближе к нашей цели.
Настройка socket.io
Созадим папку, где будут лежать файлы нашего socket.io сервера
mkdir socket
Внесем изменения в docker-compose.yml
redis:
image: redis
command: ["sh", "-c", "exec redis-server --requirepass \"${REDIS_PASSWORD}\""]
+ socket:
+ image: node
+ user: "node"
+ volumes:
+ - ./socket:/home/node/app
+ ports:
+ - 5000:5000
+ working_dir: /home/node/app
+ command: "npm start"
Перейдем в папку socket
cd socket
Установим необходимые пакеты
npm init -y
npm i -S socket.io redis express
После этого добавим в файл socket/package.json
строки
{
"name": "socket-php-example",
"version": "1.0.0",
"main": "index.js",
"author": "eustatos <astashkinav@gmail.com>",
"license": "MIT",
+ "scripts": {
+ "start": "node index.js"
+ },
"dependencies": {
"express": "^4.16.3",
"redis": "^2.8.0",
"socket.io": "^2.1.0"
}
}
Создадим файл socket/index.js
const express = require('express');
const app = express();
const http = require('http').Server(app);
const port = process.env.PORT || 5000;
app.get(
'/',
function(req, res, next) {
res.send('success');
}
);
http.listen(
port,
function() {
console.log('Listen at ' + port);
}
);
Перезапустим наш контейнер
docker-compose restart
После этого в браузере по адресу http://localhost:5000
отображается "success".
Значит мы еще чуть ближе к нашей цели. Осталось совсем немного.
Изменим файл socket/index.js
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
// подключаемся к redis
const subscriber = require('redis').createClient({
host: 'redis',
port: 6379,
password: 'eustatos'
});
// подписываемся на изменения в каналах redis
subscriber.on('message', function(channel, message) {
// пересылаем сообщение из канала redis в комнату socket.io
io.emit('eustatosRoom', message);
});
// открываем соединение socket.io
io.on('connection', function(socket){
// подписываемся на канал redis 'eustatos' в callback
subscriber.subscribe('eustatos');
});
const port = process.env.PORT || 5000;
http.listen(
port,
function() {
console.log('Listen at ' + port);
}
);
На этом настройка контейнера socket.io завершена.
Настройка клиентского приложения
Клиентское приложение можно развернуть в любом из наших контейнеров, но
для чистоты эксперимента развернем его в отдельном контейнере.
Файлы клиентского приложения разместим в папке client
mkdir client
Создадим файл client/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.3/socket.io.min.js"></script>
<script>
const socket = io(
window.location.protocol + '//' + window.location.hostname + ':5000'
);
socket.on(
'eustatosRoom',
function(message) {
console.log(JSON.parse(message));
}
);
</script>
</body>
</html>
Изменим docker-compose.yml
ports:
- 5000:5000
command: "npm start"
+ client:
+ image: nginx
+ volumes:
+ - ./client:/usr/share/nginx/html
+ ports:
+ - 8000:80
Перезапустим наш контейнер
docker-compose restart
Откроем сначала в браузере http://localhost:8000
. Для демонстрации результата
наших трудов нужно открыть панель разработчика.
Пока ничего не отображается.
Откроем в другой вкладке или окне адрес http://localhost:4400/info.php
и посмотри на консоль панели разработчика нашего клиента. Мы должны увидеть:
{test: "success"}
А это значит, что наш сервер благополучно передал клиентскому приложению данные.
Комментарии (24)
xakepmega
10.04.2018 22:52зачем здесь пхп?
eustatos Автор
10.04.2018 22:59Существует бэкенд на php, есть задача уведомить клиентское приложение о наступлении некоего события. Можно делать много запросов из клиента на бэк. А можно сделать, как в данном примере. Это не образец передового архитектурного решения, это выход из сложившейся ситуации
Fortop
11.04.2018 00:30Обычный Ratchet не устраивает?
Ну или уж https://github.com/walkor/phpsocket.io
Зачем создавать винегрет?
Fesor
11.04.2018 16:09Действительно, надо было взять centrifugo и не париться.
p.s. Ratchet еще живой?
apapacy
10.04.2018 23:10По конфигам есть два существенных замечания. 1) Порты все кроме nginx не следует открывать. Внутри контейнеров Вы можете обращаться к ним напрямую напр. redis:port… 2) В документации nginx есть несколько примеров для fastcgi и там рекомендуется стандартные параметры включать из файла
include fastcgi.conf;
Если только не требуется что-то нестандартное.eustatos Автор
10.04.2018 23:16Спасибо за замечание. Порты наружу открыл в демонстрационных целях. Собственно, сервер и клиент открыты через nginx. Socket.io тоже получается нужно открывать наружу, иначе клиентский браузер к нему не достучится.
Про fastcgi согласен. Конфигурация у nginx далеко не образцовая. Цель была продемонстрировать возможностьapapacy
10.04.2018 23:31По портам в основном касалось redis. Но раз Вы упомянули о socket.io то он как правило слушает тот же порт что и приложение основное (в Вашем случае php) поэтому полюбому придется как-то делать проксирование через nginx то есть socket.io тоже наружу не придетс открывать.
ZurgInq
11.04.2018 07:33Зачем здесь node.js? На php существует уже больше одной реализации event loop (event driven), позволяющий запускать долгоживущие процессы php.
eustatos Автор
11.04.2018 08:49Возможно, мы по разному трактуем термины. Было бы проще, если были бы конкретные примеры этих решений. Эти решения можно использовать в связке с SPA, например на Angular?
ZurgInq
11.04.2018 11:19Хотя бы reactphp.org Выше дали ещё ссылку на phpsocket.io. А сокеты, SPA и angular друг другу ортогональны
VolCh
11.04.2018 07:40Слабенький пример докеризации. Большими жирными буквами надо написать "чисто для ознакомительных целей, не делать так в продакшене".
Навскидку:
- конфиги типа nginx если и монтировать, то в только дев-режиме, вообще они должны быть включены в образ.
- зависимости npm должны устанавливаться при билде образа
- исходные файлы (php, js, html) должны копироваться в образ
eustatos Автор
11.04.2018 08:34Тестовость данного проекта очевидна. Все контейнеры сложены в один только в демонстрационных целях. Чтобы можно было одной командой развернуть и попробовать.
А с замечанием полностью согласен.VolCh
11.04.2018 12:20Это тем очевидно, кто начинал по таким туториалам внедрять докер в продакшен, ничего о нём не зная, как я )
L0NGMAN
12.04.2018 00:20Почему исходные файлы должны копироватся в образ?
VolCh
12.04.2018 13:47+1Чтобы образ можно было запустить на любой машине, например на продакшен сервере.
Sadykh
12.04.2018 20:20Не считаю истинно правильным копировать исходные файлы (php, html, js, etc) в образ. Часто использую docker контейнер для одинакового окружения (настроенная версия php, nginx, mysql), а в отдельном репозитории с git'ом хранить команды для запуска сборка образа (как правило docker-compose) и весь исходный код. Все доступы в .env файле, которые не выкладываются нигде.
Тут почему никто не говорит, что если вы сохраните данные в образе, то при выполнении команды down для контейнеров, образ вернется на стадию "упакования".
В общем, по моему скромному мнению:
- в docker хранится образ окружения (несколько контейнеров, которые выполняют свою роль)
- исходный код (ваши php файлы, html, css, js) хранятся в отдельном репозитории и подключаются монтированием к образу (подробнее прочитайте про volume)
- база данных (к примеру) — подключается также монтированием папки с базой
Я не понимаю, зачем вам хранить данные в образе. Если вы запустите образ с данными на другой машине, то там данные будут не актуальны. Куда правильнее использовать репликацию и так далее, но это уже совсем другая история. Разве что видел интересный кейс c копированием верстки на react/angular в образ, но там смысл есть.
VolCh
13.04.2018 10:53+1Тут почему никто не говорит, что если вы сохраните данные в образе, то при выполнении команды down для контейнеров, образ вернется на стадию "упакования".
Это подразумевается по умолчанию, предполагается, что люди читали документацию по докеру: "When a container is removed, any changes to its state that are not stored in persistent storage disappear."
Я не понимаю, зачем вам хранить данные в образе.
Кто говорит про данные? Речь про код и конфиги. Данные, состояние хранится либо в volume, либо вообще вне контейнера или даже вне докера.
Как вы будете деплоить свое приложение на продакшен сервер? Особенно, если всё, что на нём есть — это ядро ОС и сам docker. Вот реально больше ничего, ни ssh, ни ftp, ни даже хоть какой-то консоли физической. Докер не умеет заливать файлы на удаленную машину, только в контейнер. Да, вы можете в образе или при удаленном запуске контейнера сделать volume (хотя не обязательно), а потом с локальной машины или деплой-сервера через docker cp скопировать файлы приложения на удаленный сервер. Можете даже разделить запуск на создание и старт: создать конейнер, скопировать в него код приложения, запустить, чтобы не получить ситуацию когда сервер работает, но кода нет.
Можете, но зачем, если их можно сразу включить в образ, сразу одной командой docker run с любой машины, включая те, где исходников нет, хоть с телефона, получить работающий сервер на удаленной машине, где ничего кроме ядра и докера нет.
Докер предназначен для упаковки и запуска приложений в контейнерах. Ваш сценарий — это сценарий упаковки и запуска приложений nginx, php-fpml и т. д., которые как данные получают ваш код. Но обычно цель не запустить эти приложения, скормив им какие-то данные с локальной машины, а запустить ваше приложение, которое где-то под капотом будет использовать эти приложения.
В целом volume, примонтированный на локальный каталог с исходниками или результатами билда — это хак, для упрощения локальной разработки, чтобы не билдить образ на каждый чих и не перезапускать. Ни докер в целом, ни его фича volume для этого не создавались. Докер создавался, чтобы всё приложение упаковать в образ, который можно без проблем запускать на разных машинах, в том числе удаленно, лишь бы там был запущен сервер докера. Функциональность volume создавалась, чтобы разделять данные (не код) между контейнерами, прежде всего между контейнерами из одного образа. Монтирование volume на конкретный каталог в локальной ФС — в целом противоречит идеологии докера, хотя без него в некоторых ситуациях сложно обойтись.
eustatos Автор
12.04.2018 16:30Если данные не скопировать в образ и попытаться запустить его например на AWS, то некрасиво получится, так как они (данные) в этом случае будут недоступны.
Dreyk
пароль от редиски надо пробрасывать во все контейнеры через ENV
eustatos Автор
Спасибо за предложение.
Внес изменения
VolCh
В сокете остался.
Fesor
а еще лучше через secret.