вторник, 2 августа 2016 г.

Решение проблемы подключения к старой версии MySQL из новой версии PHP
(old insecure authentication)

На новом сервере CentOS7 я использую PHP 5.6.
За данными необходимо обращаться к старому серверу с базой данных MySQL 5.1
При попытке установить соединение с сервером получаем ошибку:
mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication. 
Please use an administration tool to reset your password with the command SET PASSWORD = PASSWORD('your_existing_password'). 
This will store a new, and more secure, hash value in mysql.user. 
If this user is used in other scripts executed by PHP 5.2 or earlier you might need to remove the old-passwords flag from your my.cnf file

Для решения проблемы на сервере MySQL со старой версией авторизации нужно сделать следующее:
1) Зайти на сервер со старой версией MySQL через phpmyadmin
2) Убедиться, что кеш пароля в этой базе имеет всего 16 бит. Для этого выполнить команду:
SELECT LENGTH(PASSWORD('youpasswd'));
В ответ должны получить 16.
3) Зайти в базу mysql, в таблицу user.
Найти здесь нашего пользователя.
4)Заменить кеш пароля пользователя (поле Password)
Стандартный пароль этой старой базы к примеру имеет вид:
0a622cbe0ef52f17
Новый кеш пароль с большим кешем должен иметь например такой вид:
*0FDBA6287D54E65C05D50D6D3E792A1B8FEBD9DB
Новый длинный кеш пароля можно узнать создав нового пользователя в новой версии MySQL 5.5 на другом сервере.
А потом скопировав этот кеш с нового сервера в старый.
5) Выполнить команду SQL
flush privileges;

четверг, 28 июля 2016 г.

Использование Event Socket Library в Freeswitch.
Различные практики.

Общие сведения.

Модуль mod_event_socket представляет собой интерфейс управления FreeSWITCH
По умолчанию прослушивается локальный интерфейс 127.0.0.1, порт 8021.
Пароль для доступа по умолчанию "ClueCon".
Модуль работает в двух режимах inbound и outbound.
В inbound режиме мы подключаемся к Freeswitch с помощью скриптов и выдаем команды / получаем события. В outbound режиме мы должны иметь скрипт-демон, к которому будет подключаться Freeswitch при обработке вызовов.

Включаем возможность работы с сокетами.

# nano /usr/local/freeswitch/conf/autoload_configs/modules.conf.xml
Следующая строчка должна быть раскомментирована:
<load module="mod_event_socket"/>
Затем открываем файл непосредственного конфигурации модуля mod_event_socket:
# nano  /usr/local/freeswitch/conf/autoload_configs/event_socket.conf.xml
Конфиг для приема всех соединений с любого интерфейса на порт 8021 с паролем ClueCon выглядит так:
<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="nat-map" value="false"/>
    <param name="listen-ip" value="0.0.0.0"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="ClueCon"/>
    <!--<param name="apply-inbound-acl" value="loopback.auto"/>-->
    <!--<param name="stop-on-bind-error" value="true"/>-->
  </settings>
</configuration>
Рестартуем систему:
# systemctl restart freeswitch.service
Убеждаемся, что порт 8021 слушается системой:
# netstat -ltupn | grep freesw
tcp      0    0 0.0.0.0:8021       0.0.0.0:* LISTEN  9543/freeswitch
tcp      0    0 10.200.16.214:5060 0.0.0.0:* LISTEN  9543/freeswitch
udp      0    0 10.200.16.214:5060 0.0.0.0:*         9543/freeswitch
Не забываем в Iptables прописать дополнительные ip адреса для доступа к сокетам Freeswitch по сети.

Проверяем работу сокетов в режиме inbound с использованием скриптов на PHP.

Для проверки, что сокеты работают, создадим скрипт и запустим его:
# nano /var/www/html/callyshek.php
Содержание файла:
<?php
date_default_timezone_set('Etc/GMT-3');

 $password = "ClueCon";
 $port = "8021";
 $host = "127.0.0.1";

 function event_socket_create($host, $port, $password) {
     $fp = fsockopen($host, $port, $errno, $errdesc) 
       or die("Connection to $host failed");
     socket_set_blocking($fp,false);
     
     if ($fp) {
         while (!feof($fp)) {
            $buffer = fgets($fp, 1024);
            usleep(100); //allow time for reponse
            if (trim($buffer) == "Content-Type: auth/request") {
               fputs($fp, "auth $password\n\n");
               break;
            }
         }
         return $fp;
     }
     else {
         return false;
     }           
 }


 function event_socket_request($fp, $cmd) {
    
     if ($fp) {    
         fputs($fp, $cmd."\n\n");    
         usleep(100); //allow time for reponse
         
         $response = "";
         $i = 0;
         $contentlength = 0;
         while (!feof($fp)) {
            $buffer = fgets($fp, 4096);
            if ($contentlength > 0) {
               $response .= $buffer;
            }
            
            if ($contentlength == 0) { //if contentlenght is already don't process again
                if (strlen(trim($buffer)) > 0) { //run only if buffer has content
                    $temparray = split(":", trim($buffer));
                    if ($temparray[0] == "Content-Length") {
                       $contentlength = trim($temparray[1]);
                    }
                }
            }
            
            usleep(100); //allow time for reponse
            
            //optional because of script timeout //don't let while loop become endless
            if ($i > 10000) { break; } 
            
            if ($contentlength > 0) { //is contentlength set
                //stop reading if all content has been read.
                if (strlen($response) >= $contentlength) {  
                   break;
                }
            }
            $i++;
         }
         
         return $response;
     }
     else {
       echo "no handle";
     }
 }

 $fp = event_socket_create($host, $port, $password);
 $cmd = "api help";
  //$cmd = "show channels";
 $response = event_socket_request($fp, $cmd);
 echo $response; 
 fclose($fp); 
?> 
Запускаем файл так:
# php -f /var/www/html/callyshek.php 
На выход должны получить справочную информацию описания api Freeswitch
Valid Commands:
...,,Shutdown,mod_commands
acl,<ip> <list_name>,Compare an ip to an acl list,mod_commands
alias,[add|stickyadd] <alias> <command> | del [<alias>|*],Alias,mod_commands
banner,,Return the system banner,mod_commands
bg_system,<command>,Execute a system command in the background,mod_commands
bgapi,<command>[ <arg>],Execute an api command in a thread,mod_commands
break,<uuid> [all],uuid_break,mod_commands
cdr_csv,parameters,cdr_csv controls,mod_cdr_csv
chat,<proto>|<from>|<to>|<message>|[<content-type>],chat,mod_dptools
coalesce,[^^<delim>]<value1>,<value2>,...,Return first nonempty parameter,mod_commands
complete,add <word>|del [<word>|*],Complete,mod_commands
cond,<expr> ? <true val> : <false val>,Evaluate a conditional,mod_commands
… <………> …
xml_locate,[root | <section> <tag> <tag_attr_name> <tag_attr_val>],Find some xml,mod_commands
xml_wrap,<command> <args>,Wrap another api command in xml,mod_commands

Библиотека Event Socket Library (ESL)

Библиотека Event Socket Library (ESL) предназначена для использования взаимодействия с Freeswitch в различных языках программирования.
Сборка базовой библиотеки Event Socket Library (ESL):
# yum install libxml2-devel pcre-devel bzip2-devel curl-devel gmp-devel aspell-devel libtermcap-devel gdbm-devel db4-devel 
# cd /usr/local/src/freeswitch/libs/esl/
# make
Затем для включения модулей для конкретных языков программирования выполняем:

Для языка php:
# yum install php-devel
# make phpmod-install
ПРИМЕЧАНИЕ:
Если во время установки возникнет следующая ошибка
esl_wrap.cpp:730:18: фатальная ошибка: zend.h: Нет такого файла или каталога
 #include "zend.h"
                  ^
компиляция прервана.
make[1]: *** [esl_wrap.o] Ошибка 1
make[1]: Выход из каталога `/usr/local/src/freeswitch/libs/esl/php'
make: *** [phpmod] Ошибка 2
Необходимо выполнить следующее:
# cd /usr/local/src/freeswitch
# ./configure
Затем повторяем:
# cd /usr/local/src/freeswitch/libs/esl/
# make phpmod-install
Теперь сборка происходит без ошибок.
Теперь после инсталляции ESL для PHP
Конфигурация библиотеки здесь - /etc/php.d/esl.ini
Модуль ESL прописался в /usr/lib64/php/modules и /usr/share/pear
Файл библиотеки здесь  - /usr/share/pear/ESL.php
Его можно использовать в разработке программ ESL.php

Для языка python:
# yum install python-devel
# make pymod-install
Модуль PYTHON прописался в /usr/lib64/python2.7/site-packages/ESL.py

В заключении рестартуем apache:
# systemctl restart httpd.service

Проверяем работу сокетов в режиме inbound с использованием скриптов на PHP и библиотеки ESL.

Для проверки работы библиотеки ESL в языке php создаем файл /var/www/html/callyshek2.php
# touch /var/www/html/callyshek2.php
Его содержимое будет такое:
<?php
require_once('ESL.php');

if ($argc > 1) {
array_shift($argv);
$command = sprintf('%s', implode(' ', $argv));
printf("Command to run is: %s\n", $command);
$sock = new ESLconnection('127.0.0.1', '8021', 'ClueCon');
$res = $sock->api($command);
printf("%s\n", $res->getBody());
} else {
printf("ERROR: You Need To Pass A Command\nUsage:\n\t%s <command>", $argv[0]);
}?>
Запуск для проверки:
# php -f /var/www/html/callyshek2.php status
На выходе должны получить что-то такое:
Command to run is: status
UP 0 years, 0 days, 2 hours, 13 minutes, 46 seconds, 695 milliseconds, 497 microseconds
FreeSWITCH (Version 1.6.9 git d574870 2016-06-13 18:10:44Z 64bit) is ready
0 session(s) since startup
0 session(s) - peak 0, last 5min 0
0 session(s) per Sec out of max 30, peak 0, last 5min 0
1000 session(s) max
min idle cpu 0.00/99.80
Current Stack Size/Max 240K/8192K

Проверка работы Freeswitch в режиме outbound.

В качестве сокет-сервера для проверки будет выступать утилита netcat.

Для того что бы передать управление внешнему серверу и использовать режим outbound прописываем в плане набора:
  <extension name="call_in">
    <condition field="destination_number" expression="^(8459938103)$|^(9938103)$">
        <action application="socket" data="10.200.16.214:8022 async full"/>
    </condition>
  </extension>
Порт 8021, указанный в конфигурации мы использовать не можем, так как этот порт уже занят самим Freeswitch и не может выступать в качестве порта сокет-сервера. Используем здесь порт 8022.
Затем проводим рестарт:
# systemctl restart freeswitch

Запускаем сервер сокет на порту 8022 и ждем:
# nc -k -v -l 10.200.16.214 8022
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on 10.200.16.214:8022
При вызове на номер 8459938103 Freeswitch передаст управление сокет-серверу:
Ncat: Connection from 10.200.16.214.
Ncat: Connection from 10.200.16.214:39806.
Вводим в консоли команду connect и два раза ENTER:
# connect\n\n
Получаем ответ от сервера с данными о поступившем вызове:
Event-Name: CHANNEL_DATA
Core-UUID: 77af8caf-af88-42ab-81ed-b7287794de01
FreeSWITCH-Hostname: callpeg.svttk.ru
FreeSWITCH-Switchname: callpeg.svttk.ru
FreeSWITCH-IPv4: 10.200.16.214
FreeSWITCH-IPv6: %3A%3A1
Event-Date-Local: 2016-07-13%2017%3A11%3A07
Event-Date-GMT: Wed,%2013%20Jul%202016%2014%3A11%3A07%20GMT
Event-Date-Timestamp: 1468419067842618
Event-Calling-File: mod_event_socket.c
Event-Calling-Function: parse_command
Event-Calling-Line-Number: 1996
….<….>….
variable_endpoint_disposition: DELAYED%20NEGOTIATION
variable_DP_MATCH: ARRAY%3A%3A8469938103%7C%3A8469938103
variable_call_uuid: ae37202f-c4ab-41dd-b109-8fde03c56223
variable_current_application_data: 10.200.16.214%3A8022%20async%20full
variable_current_application: socket
variable_socket_host: 10.200.16.214
Content-Type: command/reply
Reply-Text: %2BOK%0A
Socket-Mode: async
Control: full
Значит все работает.

Проверка работы Freeswitch в режиме outbound с использованием сокет-сервера на PYTHON.

Создадим файл servcall.py на основе примера, имеющегося в дистрибутиве ESL:
/usr/local/src/freeswitch/libs/esl/python/server.py
Содержимое файла:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import SocketServer
from ESL import *

class ESLRequestHandler(SocketServer.BaseRequestHandler ):
def setup(self):
print self.client_address, 'connected!'

fd = self.request.fileno()
print fd

con = ESLconnection(fd)
print "Connected: ", con.connected()
if con.connected():

info = con.getInfo()

uuid = info.getHeader("unique-id")
print uuid
con.execute("answer", "", uuid)
con.execute("playback", "ttk/fon.wav", uuid);

#server host is a tuple ('host', port)
server = SocketServer.ThreadingTCPServer(('10.200.16.214', 8022), ESLRequestHandler)
server.serve_forever()
В этом файле описывается логика работы сервера. Сервер запускается и прослушивает Ip 10.200.16.214 и порт 8022. При установлении соединения, запрашивается информация о соединении и серверу Freeswitch дается команда проигрывать абоненту звуковой файл ttk/fon.wav.
Файл должен быть помещен в папку: /usr/local/freeswitch/sounds/en/us/callie/ttk/ (папка ttk должна быть создана).
Для работоспособности схемы, необходимо запустить сервер командой:
# ./servcall.py

Вывод событий Freeswitch в stdout linux, используя ESL и PYTHON и подключаясь к Freeswitch в режиме inbound.

В дистрибутиве ESL Freeswitch существует скрипт-сервер, написанный на PYTHON, который подключается к серверу, подписывается на события и выводит все поступившие события в консоль. Скрипт лежит здесь - /usr/local/src/freeswitch/libs/esl/python/events.py
На его основе создадим свой файл-скрипт-сервер events2.py.
Содержимое файла:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import string
import sys

from ESL import *

con = ESLconnection("127.0.0.1","8021","ClueCon")
#are we connected?

if con.connected:

  con.events("plain", "all");

  while 1:
  #my $e = $con->recvEventTimed(100);
    e = con.recvEvent()

    if e:
      print e.serialize()
При запуске этого скрипта все, произойдет подключение к Freeswitch на ip 127.0.0.1 и порт 8021. Устанавливается режим получения сообщений – plain. Параметр all говорит о том, что мы хотим получать все сообщения. Затем все полученные события будут выводиться в консоль.
Пример:
#./events2.py
При поступлении вызова мы увидим множество сообщений:
Event-Name: HEARTBEAT

Event-Name: RE_SCHEDULE

Event-Name: CHANNEL_STATE

Event-Name: CHANNEL_CALLSTATE

Event-Name: PRESENCE_IN

Event-Name: CHANNEL_EXECUTE

Event-Name: API

Event-Name: CHANNEL_OUTGOING

Event-Name: CHANNEL_CREATE

Event-Name: CHANNEL_ORIGINATE

Event-Name: CHANNEL_PROGRESS

Event-Name: CHANNEL_HANGUP

Event-Name: CHANNEL_EXECUTE_COMPLETE

Event-Name: CHANNEL_HANGUP_COMPLETE

Event-Name: CHANNEL_DESTROY


вторник, 26 июля 2016 г.

Сокет-сервера в Linux. (CentOS7).

1. NETCAT

В качестве сокет-сервера может выступать утилита netcat.
Сначала убедимся, что сервер на основе netcat работает и ему ничего не мешает. Запускаем на каком-нибудь сервере Linux сокет-сервер таким образом:
# nc -v -l 10.10.50.250 8021
Ip 10.10.50.250 – это Ip адрес нашего сервера.
8021 – это порт, который будет слушать сервер.
C другой машины подключаемся к созданному сокету так:
# nc 10.10.50.250 8021

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

С клиентской машины послать в порт данные можно так
# echo "12345" | nc 10.200.16.214 8021
Правда и в этот раз после приема данных сервер закроет соединение.
Для того что бы сервер каждый раз открывал соединение заново при закрытии сессии запускаем сервер так(с ключиком -k):
# nc -k -v -l 10.200.16.214 8021

2. PYTHON SOCKETSERVER

Ссылка на библиотеку сокет-сервера PYTHON:
https://docs.python.org/2/library/socketserver.html

Существует 4 класса SocketServer на PYTHON. Нас интересует класс TCPServer. 
Создание сервера требует нескольких шагов. Во-первых, необходимо создать класс обработчик - подкласс класса BaseRequestHandler и переопределить в обработчике метод handle (); Этот метод будет обрабатывать входящие запросы. Во-вторых, нужно создать экземпляр класса TCPServer, передавая ему адрес сервера и класса обработчика.
Затем вызвать handle_request() или serve_forever().
Для закрытия сокета вызывается server_close ()

Файл сервера servcall.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import SocketServer

class MyTCPHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print "{} wrote:".format(self.client_address[0])
        print self.data
        self.request.sendall(self.data.upper())


server = SocketServer.TCPServer(("localhost", 8022), MyTCPHandler)
# Активация сервера до ввода Ctrl-C
server.serve_forever()
Сервер делает только одно: переводит все буквы в верхний регистр и отвечает тем же текстом клиенту.
Запуск сервера выполняется так:
# ./servcall.py

В соседней консоли создаем клиента.
Файл клиента clientsocketserv.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import sys

HOST, PORT = "10.200.16.214", 8022
data = " ".join(sys.argv[1:])

# Create a socket (SOCK_STREAM means a TCP socket)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    # Connect to server and send data
    sock.connect((HOST, PORT))
    sock.sendall(data + "\n")

    # Receive data from the server and shut down
    received = sock.recv(1024)
finally:
    sock.close()

print "Sent:     {}".format(data)
print "Received: {}".format(received)
Клиент принимает все аргументы команды запуска и отправляет их серверу

Запуск клиента:
# ./clientsocketserv.py 12345 privet drug
При этом мы увидим:
Sent:     12345 privet drug
Received: 12345 PRIVET DRUG
В консоли сервера будет отображено:
127.0.0.1 wrote:
12345 privet drug


понедельник, 25 июля 2016 г.

Использование APE (Ajax Push Engine) для построения web-приложений реального времени в CentOS7.

Ссылки:
http://ape-project.org/



Общие сведения:
Схема работы APE в рассматриваемом примере такова:


Имеется APE сервер, работающий на серверной стороне, принимающий данные на порту 6969, преобразующий эти данные и рассылающий подписчикам. PYTHON-скрипт – является поставщиком данных для сервер.
Пользователь, просматривая страницу в браузере, загружает ее с WEB-сервера. Затем обновление данных на странице происходит с использованием JS-фреймворка APE. Обновление данных между APE сервером и APE фреймворком происходит по специальному протоколу по концепции COMET.

Установка серверной части.
Переходим в каталог, куда будем загружать дистрибутив сервера.
# cd /usr/src/
Копируем ссылку на исходные коды сервера (Source Code .zip) со страницы http://ape-project.org/download/ и качаем
# wget http://ape-project.org/download/stable/APE_Server-1.1.2.zip
Распаковываем
# unzip APE_Server-1.1.2.zip
# cd APE_Server-1.1.2
Устанавливаем необходимые пакеты
# yum install gcc
# yum install zip
# yum install autoconf
# yum groupinstall 'Development Tools'
Устанавливаем APE сервер:
# ./build.sh
# make
# make install
Установка сервера происходит в папку /usr/share/APE_Server

Настройка сервера:
Файл конфига находиться здесь - /usr/share/APE_Server/bin/ape.conf
В файл необходимо внести немного правок:
# nano /usr/share/APE_Server/bin/ape.conf
Раздел Server определяет, какие ip адреса и какой порт будет прослушивать сервер APE. Здесь же необходимо прописать домен, в котором находиться сервер. Пример:
Server {
        port = 6969
        daemon = yes
        ip_listen = 0.0.0.0
        ip_local = 127.0.0.1
        domain = callpeg.svttk.ru
        rlimit_nofile = 10000
        pid_file = /var/run/aped.pid
}
В разделе Config необходимо указать полные пути, где располагаются модули и их конфигурации сервера APE. Если этого не сделать, сервер можно будет запустить только из директории, куда он установился.
Config {
#relative to ape.conf
#       modules = ../modules/lib/
#       modules_conf = ../modules/conf/
        modules = /usr/share/APE_Server/modules/lib/
        modules_conf = /usr/share/APE_Server/modules/conf/

}
Теперь необходимо указать полный путь к расположению скриптов:
# nano /usr/share/APE_Server/modules/conf/javascript.conf
Содержимое файла приводим к виду:
#scripts_path = ../scripts/
scripts_path = /usr/share/APE_Server/scripts/

Обеспечиваем работу серверной части.
Что бы APE корректно работал, добавим путь /usr/share/APE_Server/bin в $PATH
# nano ~/.bashrc
Добавляем в конец:
PATH=$PATH:/usr/share/APE_Server/bin
export PATH
Открываем порт 6969 в iptables
# nano /etc/sysconfig/iptables
Добавляем в начало правил строку:
-A INPUT -p tcp --dport 6969 -j ACCEPT
Рестартуем службу iptables
#systemctl restart iptables
Серверу для корректной работы необходимо выделить собственный поддомен.
Мое имя сервера: callpeg.svttk.ru
Выделяем поддомен «ape»: ape.callpeg.svttk.ru
В DNS прописываем:
ape.callpeg IN A 10.200.16.214
*.ape.callpeg IN A 10.200.16.214
Я для верности прописал настройки в файл hosts:
# nano /etc/hosts
Добавляем в конец файла:
127.0.0.1 callpeg.svttk.ru
127.0.0.1 ape.callpeg.svttk.ru
127.0.0.1 0.ape.callpeg.svttk.ru
127.0.0.1 1.ape.callpeg.svttk.ru
127.0.0.1 2.ape.callpeg.svttk.ru
127.0.0.1 3.ape.callpeg.svttk.ru
127.0.0.1 4.ape.callpeg.svttk.ru
127.0.0.1 5.ape.callpeg.svttk.ru
127.0.0.1 6.ape.callpeg.svttk.ru
127.0.0.1 7.ape.callpeg.svttk.ru
127.0.0.1 8.ape.callpeg.svttk.ru
127.0.0.1 9.ape.callpeg.svttk.ru

Запуск сервера APE.
Запускается сервер так:
# /usr/share/APE_Server/bin/aped --cfg /usr/share/APE_Server/bin/ape.conf
При правильном старте мы должны увидеть:
   _   ___ ___
  /_\ | _ \ __|
 / _ \|  _/ _|
/_/ \_\_| |___|
AJAX Push Engine

Bind on port 6969

Version : 1.1.2
Build   : Jun 27 2016 17:25:02
Author  : Weelya (contact@weelya.com)

[Module] [spidermonkey] Loading module : Javascript embeded (0.01) - Anthony Catel
[JS] Loading script ../scripts/framework/mootools.js...
[JS] Loading script ../scripts/framework/Http.js...
[JS] Loading script ../scripts/framework/userslist.js...
[JS] Loading script ../scripts/utils/utils.js...
[JS] Loading script ../scripts/commands/proxy.js...
[JS] Loading script ../scripts/commands/inlinepush.js...
[JS] Loading script ../scripts/examples/nickname.js...
[JS] Loading script ../scripts/examples/move.js...
[JS] Loading script ../scripts/utils/checkTool.js...

Клиентский фреймворк.
Для работы с сервером нужен клиент Ajax Push Engine, доступный по адресу https://github.com/APE-Project/APE_JSF
Скачиваем этот фреймфорк в папку web сервера apache:
# cd /var/www/html
# git clone https://github.com/APE-Project/APE_JSF.git
В каталоге появится папка APE_JSF.
Вносим изменения в файл – библиотеки клиента:
# nano APE_JSF/Build/closureCompiler/apeClientJS.js
Ишем группу строк:
APE.Config.baseUrl = 'http://local.ape-project.org/APE_JSF'; //APE JSF
APE.Config.domain = 'ape-project.org';
APE.Config.server = 'ape.local.ape-project.org:6969'; //APE server URL
APE.Config.baseUrl = 'http://ape.home.efyx.io/APE_JSF'; //APE JSF
APE.Config.domain = 'efyx.io';
APE.Config.server = 'ape.home.efyx.io:6969'; //APE server URL
APE.Config.transport = 6;
Приводим их к виду, определяя настройки домена:
APE.Config.baseUrl = 'http://callpeg.svttk.ru/APE_JSF/'; //APE JSF
APE.Config.domain = 'callpeg.svttk.ru';
APE.Config.server = 'ape.callpeg.svttk.ru:6969'; //APE server URL
APE.Config.transport = 6;
Для того что бы работали DEMO-примеры делаем тоже самое в следующем файле 
# nano /var/www/html/APE_JSF/Demos/config.js
APE.Config.baseUrl = 'http://callpeg.svttk.ru/APE_JSF'; //APE JSF
APE.Config.domain = 'callpeg.svttk.ru';
APE.Config.server = 'ape.callpeg.svttk.ru:6969'; //APE server URL

Проверка установки.
Проверка может быть проведена на странице http://callpeg.svttk.ru/APE_JSF/Tools/Check/
Правильный результат теста:


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





Тестирование сервера APE, используя приложение inlinepush, идущее в комплекте с сервером.
Приложение  inlinepush подключено к выполнению в основном конфигурационном файле приложений сервера - /usr/share/APE_Server/scripts/main.ape.js. Используется директива:
include("commands/inlinepush.js");
Само приложение расположено здесь: /usr/share/APE_Server/scripts/commands/inlinepush.js
Это приложение выполняет следующее: принимает данные в определенный канал и рассылает всем получателям эти данные. Для отправки данных требуется параметр password.
Пароль для использования приложения inlinepush, задается в файле конфигурации.
Изменим пароль по умолчанию:
# nano /usr/share/APE_Server/modules/conf/inlinepush.conf
Пусть пароль будет такой:
password = testpasswd3

Создадим источник данных – скрипт PYTHON:
# touch /usr/share/APE_Server/testservape.py
# chmod +x /usr/share/APE_Server/testservape.py
# nano /usr/share/APE_Server/testservape.py
Содержимое файла:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import urllib2
import json

server = 'http://127.0.0.1:6969/0/?'

cmd = [{'cmd': 'inlinepush',
'params': {
'password': 'testpasswd3',
'raw': 'DATA',
'channel': 'testchannel',
'data': {
'msg': 'Привет, детка!'
}
}
}]

url = server + urllib2.quote(json.dumps(cmd))
#print url
response = urllib2.urlopen(url)
print response.read()
При запуске скрипта произойдет отправка данных на url http://127.0.0.1:6969/0/?. В URL будет передана команда Inlinepush, указан пароль testpasswd3, задан канал отправки – testchannel и приведено сообщение для передачи – “Привет, детка!”
Если при запущенном APE сервере выполнить этот скрипт, то мы должны получить примерно следующий вывод:
# /usr/share/APE_Server/testservape.py
[{"time":"1469424768","raw":"ERR","data":{"code":"401","value":"UNKNOWN_CHANNEL"}}]
Сообщение ERR говорит о том, что канал testchannel еще никем не используется.

Помещаем на сервер apache следующую web-страницу:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" dir="ltr" lang="en">
  <head> 
    <!-- Загрузка клиента APE. Указывается путь к вашему JSF. -->
    <script type="text/javaScript" src="APE_JSF/Build/closureCompiler/apeClientJS.js"></script>
  </head>
  <body>
    <?php
        echo "APE сервер.";
    ?>
    <script type="text/javaScript">
      var client = new APE.Client(); 
  // -- небольшая функция, которая будет генерировать случайные имена для каждого, кто входит на сайт    
      function randomString(length) {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'.split('');          
        var str = '';
        for (var i = 0; i < length; i++) {str += chars[Math.floor(Math.random() * chars.length)];}
        return str;
      }
  // -- Для вывода полученных данных
      function createDiv(vartext) {
    var _body = document.getElementsByTagName('body') [0];
      var _div = document.createElement('div');
        var _text = document.createTextNode(vartext)
        _div.appendChild(_text);
        _body.appendChild(_div);
      }
      client.load();
      client.addEvent('load', function() {
  // -- Стартуем клиент, используя случайный name на 15 симвлолв.
        client.core.start({"name":randomString(15)});
      });
      client.addEvent('ready', function() {
    createDiv("Connected");
        client.core.join('testchannel');
        client.addEvent('multiPipeCreate', function(pipe, options) {
        });
  // -- Собственно обработчик. Вывод данных на экран.
        client.onRaw('data', function(raw, pipe) {
        createDiv('Receiving : ' + unescape(raw.data.msg));
        });
      });
    </script>

  </body>
</html>
Здесь происходит подключение файла-фреймворка, затем браузер подключается к каналу 'testchannel' (при этом выводиться сообщение “Connected”) и ожидает поступления информации. При поступлении из канала информации, создается новый тег div с этой информацией.

Запускаем сервер APE. Открываем браузер и вводим url - http://callpeg.svttk.ru/testape.php 
Должно появится следующее:


В соседней консоли сервер запускаем скрипт PYTHON:
# /usr/share/APE_Server/testservape.py
[{"time":"1469425386","raw":"pushed","data":{"value":"ok"}}]
В браузере появляется сообщение:


Значит все работает.

Запуск сервера APE в режиме демона.
В конфигурационный файл сервера APE в раздел Sever вносим изменения:
# nano /usr/share/APE_Server/bin/ape.conf
Меняем директиву daemon c no на yes:
Server {
        port = 6969
        daemon = yes
        ip_listen = 0.0.0.0
        ip_local = 127.0.0.1
        domain = callpeg.svttk.ru
        rlimit_nofile = 10000
        pid_file = /var/run/aped.pid
}
Теперь при запуске сервера APE
/usr/share/APE_Server/bin/aped --cfg /usr/share/APE_Server/bin/ape.conf
сервер не будет привязан к консоли.
Что бы сервер APE стартовал при загрузке необходимо создать Unit system:
# touch /etc/systemd/system/multi-user.target.wants/apeserver.service
# chmod 664 /etc/systemd/system/multi-user.target.wants/apeserver.service
# nano /etc/systemd/system/multi-user.target.wants/apeserver.service
Содержимое файла:
[Unit]
Description=APE SERVER START
After=network.target httpd.target

[Service]
Type=forking
User=root
Group=root
ExecStart=/usr/share/APE_Server/bin/aped --cfg /usr/share/APE_Server/bin/ape.conf

[Install]
WantedBy=multi-user.target
Рестартуем демон systemd:
# systemctl daemon-reload
Теперь запустить демон phpdaemon можно и так:
# systemctl start apeserver.service

вторник, 12 июля 2016 г.

Freeswitch и прослушивание нескольких портов и интерфейсов

Freeswitch по умолчанию с одним профилем слушает только один IP и один порт.
Что бы заставить Freeswitch слушать дополнительный IP адрес необходимо создать дополнительный профиль в папке /usr/local/freeswitch/conf/sip_profiles, где в следующих переменных прописать нужный IP адрес:
    <param name="sip-ip" value="10.200.15.124"/>
    <param name="rtp-ip" value="10.200.15.124"/>
    <param name="ext-rtp-ip" value="10.10.49.215"/>
    <param name="ext-sip-ip" value="10.10.49.215"/>
Профиль создать можно путем простого копирования исходного профиля.
Никакие другие переменные (в том числе название профиля) можно не менять.

понедельник, 4 июля 2016 г.

Использование PHPDAEMON в CentOS7

Подготовка к установке

До установки демона необходимо обновиться до php 5.6
Обновление PHP будет производиться из репозитория Remi, поэтому произведем его подключение.
# rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
Редактируем файл репозитария /etc/yum.repos.d/remi.repo
# nano /etc/yum.repos.d/remi.repo
В секции
[remi-php56]
ставим
enabled=1
Затем устанавливаем новую версию php:
# yum install php -y
# systemctl restart httpd
Проверить версию php можно так:
# php -v
PHP 5.6.23 (cli) (built: Jun 22 2016 08:56:52)

Далее нужно поставить свежую версию библиотеки libevent 2
# cd /usr/src/
# yum install gcc kernel-devel make ncurses-devel
# curl http://sourceforge.net/projects/levent/files/latest/download?source=files -L -o libevent2.tar.gz -w 'Last URL was: %{url_effective}'
# curl http://sourceforge.net/projects/tmux/files/latest/download?source=files -L -o tmux.tar.gz -w 'Last URL was: %{url_effective}'
# tar zxvf libevent2.tar.gz
# cd libevent-2.0.22-stable
# ./configure --prefix=/usr/local
Библиотеку устанавливаем в папку/usr/local
# make
# make install

Ставим необходимые в дальнейшем пакеты и модули php:
# yum install -y git gcc openssl-devel
# yum install php-pear
# yum install php-devel
# pecl install eio inotify
# pecl install http://pecl.php.net/get/event
На вопросы, задаваемые инсталлятором отвечаем так:
libevent installation prefix [/usr] : /usr/local
Include libevent's pthreads library and enable thread safety support in Event [no] : yes
На остальные вопросы можно ответить значением по умолчанию просто нажав Enter.

Для корректной работы модулей php event и eio необходим модуль sockets.
В системах RedHat/CentOS файлы конфигурации подгружаются в алфавитном порядке, поэтому назовем их z-event.ini и z-eio.ini соответственно и проведем подключение:
# echo "extension=event.so" > /etc/php.d/z-event.ini
# echo "extension=eio.so" > /etc/php.d/z-eio.ini

Установка phpdaemon

Готовим директорию, где будет работать демон phpdaemon и устанавливаемся:
#  mkdir /opt/phpdaemon
# cd /opt/phpdaemon
# git clone https://github.com/kakserpom/phpdaemon.git ./
Копируем конфигурационный файл из примера.  
# cp /opt/phpdaemon/conf/phpd.conf.example /opt/phpdaemon/conf/phpd.conf
Создаем символьную ссылку
# ln -s /opt/phpdaemon/bin/phpd /usr/bin/phpd

Устанавливаем временную зону в файле php.ini
Открываем файл:
# nano  /etc/php.ini
Прописываем в переменную date.timezone  - Московское время
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
date.timezone = Etc/GMT-3

Запуск phpdaemon

Запускаем демон таким образом:
# phpd start --verbose-tty=1
Опция --verbose-tty=1 указыват демону выводить журнал в консоль.
Нормальный вывод сейчас должен выглядеть так:
# phpd start --verbose-tty=1
[PHPD] Loaded config file: 'conf/conf.d/ExampleJabberBot.conf'
[PHPD] Loaded config file: 'conf/conf.d/FastCGI.conf'
[PHPD] Loaded config file: 'conf/conf.d/FlashpolicyServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/HTTPServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/IdentServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/SSL-sample.conf'
[PHPD] Loaded config file: 'conf/conf.d/WebSocketServer.conf'
[PHPD] Loaded config file: '/opt/phpdaemon/conf/phpd.conf'
#

В дальнейшем работаем с демоном так:
# phpd start Запуск демона
# phpd stop Остановка демона
# phpd reload, phpd restart Перезапуск демона
# phpd status Статус демона
# phpd fullstatus Статус демона подробный

Логи работы сервера будут складываться по умолчанию в файл /var/log/phpdaemon.log.

Добавляем phpdaemon в автозапуск. Для этого создаем сервис systemd.
# touch /etc/systemd/system/multi-user.target.wants/phpdaemonstart.service
# chmod 664 /etc/systemd/system/multi-user.target.wants/phpdaemonstart.service
# nano /etc/systemd/system/multi-user.target.wants/phpdaemonstart.service
Содержание файла:
[Unit]
Description=PHP DAEMON START
After=network.target httpd.target

[Service]
Type=forking
User=root
Group=root
ExecStart=/usr/bin/php -f /opt/phpdaemon/bin/phpd start &

[Install]
WantedBy=multi-user.target 
Рестартуем демон systemd:
# systemctl daemon-reload
Теперь запустить демон phpdaemon можно и так:
# systemctl start phpdaemonstart.service

Phpdaemon и серверы HTTP и WebSocket

Включаем websocket сервер phpdaemon
# nano /opt/phpdaemon/conf/conf.d/WebSocketServer.conf
Содержимое файла приводим к виду:
Pool:WebSocketServer {
        # you can redefine default settings here
        privileged;
        listen 'tcp://0.0.0.0';
        port 8021;
}

Включаем web-сервер
# nano /opt/phpdaemon/conf/conf.d/HTTPServer.conf
Содержимое файла приводим к виду:
Pool:HTTPServer {
        #privileged;
        # you can redefine default settings here
        enable 1;
        listen '0.0.0.0';
        port 8080;
        expose 1;
}

Перезапускаем демон:
# phpd stop
# phpd start --verbose-tty=1
Вывод загрузки должен быть такой:
# phpd start --verbose-tty=1
[PHPD] Loaded config file: 'conf/conf.d/ExampleJabberBot.conf'
[PHPD] Loaded config file: 'conf/conf.d/FastCGI.conf'
[PHPD] Loaded config file: 'conf/conf.d/FlashpolicyServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/HTTPServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/IdentServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/SSL-sample.conf'
[PHPD] Loaded config file: 'conf/conf.d/WebSocketServer.conf'
[PHPD] Loaded config file: '/opt/phpdaemon/conf/phpd.conf'
[PHPD] M#23145 \PHPDaemon\Core\Pool:HTTPServer up.
[PHPD] ClassFinder: 'HTTPServer' -> '\PHPDaemon\Servers\HTTP\Pool', you should change your code.
[PHPD] M#23145 \PHPDaemon\Core\Pool:WebSocketServer up.
[PHPD] ClassFinder: 'WebSocketServer' -> '\PHPDaemon\Servers\WebSocket\Pool', you should change your code.
[root@callpeg ~]# [PHPD] M#23145 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\WebSocket\Pool up.
[PHPD] W#23149 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23152 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23148 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23150 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23155 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23147 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23151 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23156 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
Проверяем, что демон стартовал:
# ps ax | grep phpd
23307 ?        SNs    0:00 phpd: master process
23308 ?        SN     0:00 phpd: IPC process
23309 ?        SNl    0:00 phpd: worker process
23310 ?        SNl    0:00 phpd: worker process
23311 ?        SNl    0:00 phpd: worker process
23312 ?        SNl    0:00 phpd: worker process
23313 ?        SNl    0:00 phpd: worker process
23315 ?        SNl    0:00 phpd: worker process
23317 ?        SNl    0:00 phpd: worker process
23318 ?        SNl    0:00 phpd: worker process
23326 pts/1    S+     0:00 grep --color=auto phpd
Проверяем, слушаются ли порты 8021 (websoket-сервер от phpdaemon) и 8080 (web-сервер от phpdaemon)
# netstat -ltupn | grep phpd
tcp        0      0 0.0.0.0:8080   0.0.0.0:*  LISTEN   19297/phpd: master
tcp        0      0 0.0.0.0:8021   0.0.0.0:*  LISTEN   19297/phpd: master
Для возможности прослушивания портов по сети, открываем эти порты в iptables
# nano /etc/sysconfig/iptables
-A INPUT -p tcp --dport 8021 -j ACCEPT
-A INPUT -p tcp --dport 8080 -j ACCEPT
# systemctl restart  iptables

Включим в конфигурационном файле phpd.conf работу двух приложений, идущих в комплекте с пакетом phpdaemon,
# nano /opt/phpdaemon/conf/phpd.conf
Приводим файл к виду
## Config file

user root;
group root;

#Логирорвание
logging false;
log-storage '/var/log/phpdaemon.log';

max-workers     8;
min-workers     8;
start-workers   8;
max-idle        0;
#add-include-path '/path/to/your/folder';

# apps routing
path '/opt/phpdaemon/conf/AppResolver.php';

ExampleWebSocket {
}

ServerStatus {
}

include conf.d/*.conf;
Здесь указывается, что демон будет работать из-под пользователя root.
Файл описания маршрутизации запросов будет находиться здесь: /opt/phpdaemon/conf/AppResolver.php
Включены приложения ExampleWebSocket и ServerStatus
Логирование выключено.

Все приложения демона должны быть размещены в папке /opt/phpdaemon/PHPDaemon/Applications/.
Приложение ServerStatus в этой папке уже есть, а приложение ExampleWebSocket нужно скопировать туда из папки примеров:
# cp /opt/phpdaemon/Examples/ExampleWebSocket.php /opt/phpdaemon/PHPDaemon/Applications/ExampleWebSocket.php
Что бы этот пример заработал необходимо в файл внести небольшие изменения:
1. В самом начале файла строку
namespace PHPDaemon\Examples;
заменить на:
namespace PHPDaemon\Applications;
2. Строку
ws = new WebSocket('ws://' + document.domain + ':8047/exampleApp');
заменить на 
ws = new WebSocket('ws://' + document.domain + ':8021/exampleApp');

Для корректной отработки запросов к web-серверу на созданные приложения, необходимо внести изменения в конфигурационный файл маршрутизации AppResolver.php
# nano /opt/phpdaemon/conf/AppResolver.php
Функция getRequestRoute в этом файле описывает какие приложения будут отрабатывать при поступлении запросов к серверу из браузера клиента.
К имеющемуся условию:
if (preg_match('~^/(WebSocketOverCOMET|Example.*)/?~', $req->attrs->server['DOCUMENT_URI'], $m)) {
      return $m[1];
}
Добавляем условие:
if (preg_match('~^/(ServerStatus)/?~', $req->attrs->server['DOCUMENT_URI'], $m)) {
       return 'ServerStatus';
}

Перезапускаем демон:
# phpd stop
# phpd start --verbose-tty=1
Лог запуска теперь будет такой:
# phpd start --verbose-tty=1
[PHPD] Loaded config file: 'conf/conf.d/ExampleJabberBot.conf'
[PHPD] Loaded config file: 'conf/conf.d/FastCGI.conf'
[PHPD] Loaded config file: 'conf/conf.d/FlashpolicyServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/HTTPServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/IdentServer.conf'
[PHPD] Loaded config file: 'conf/conf.d/SSL-sample.conf'
[PHPD] Loaded config file: 'conf/conf.d/WebSocketServer.conf'
[PHPD] Loaded config file: '/opt/phpdaemon/conf/phpd.conf'
[PHPD] M#23255 \PHPDaemon\Core\Pool:HTTPServer up.
[PHPD] ClassFinder: 'HTTPServer' -> '\PHPDaemon\Servers\HTTP\Pool', you should change your code.
[PHPD] M#23255 \PHPDaemon\Core\Pool:WebSocketServer up.
[PHPD] ClassFinder: 'WebSocketServer' -> '\PHPDaemon\Servers\WebSocket\Pool', you should change your code.
[PHPD] M#23255 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\WebSocket\Pool up.
[root@callpeg ~]# [PHPD] W#23258 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23258 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23258 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23257 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23257 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23257 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23261 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23261 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23261 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23259 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23259 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23259 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23262 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23260 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23262 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23262 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23260 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23260 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23263 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23263 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23263 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.
[PHPD] W#23264 \PHPDaemon\Applications\ExampleWebSocket up.
[PHPD] W#23264 \PHPDaemon\Applications\ServerStatus up.
[PHPD] W#23264 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\HTTP\Pool up.

Теперь при обращении к серверу по адресу http://<ip-сервера>:8080/ServerStatus получим ответ о статусе сервера:


А при обращении к ресурсу http:// <ip-сервера>:8080/ExampleWebSocket  получим пример работы приложения на web-soket:


Первой кнопкой можно открыть сокет, второй кнопкой посылается запрос «ping» серверу, на который тот отвечает словом «pong». Третья кнопка служит для закрытия сокета.



В логах сервера phpdaemon, которые валять в консоль сервера мы увидим примерно следующее:
[PHPD] Notice: Undefined index: waitinit in /opt/phpdaemon/PHPDaemon/Applications/ServerStatusRequest.php:48
#1  PHPDaemon\Core\Daemon::errorHandler() called at [/opt/phpdaemon/PHPDaemon/Applications/ServerStatusRequest.php:48]
#2  PHPDaemon\Applications\ServerStatusRequest->run() called at [/opt/phpdaemon/PHPDaemon/Request/Generic.php:215]
#3  PHPDaemon\Request\Generic->eventCall()
#4  EventBase->dispatch() called at [/opt/phpdaemon/PHPDaemon/Core/EventLoop.php:149]
#5  PHPDaemon\Core\EventLoop->run() called at [/opt/phpdaemon/PHPDaemon/Thread/Worker.php:215]
#6  PHPDaemon\Thread\Worker->run() called at [/opt/phpdaemon/PHPDaemon/Thread/Generic.php:146]
#7  PHPDaemon\Thread\Generic->__invoke() called at [/opt/phpdaemon/bin/phpd:71]

[PHPD] ExampleWebSocket: 'pong' received by client.
[PHPD] ExampleWebSocket: 'pong' received by client.
[PHPD] ExampleWebSocket: 'pong' received by client.
[PHPD] ExampleWebSocket: 'pong' received by client.
[PHPD] ExampleWebSocket: 'pong' received by client.