Работа с сокетами в СУБД Caché. Пример реализации серверной части протокола WebSocket

в 4:44, , рубрики: cache, dbms, intersystems cache, nosql, socket, tcp-ip, web-разработка, Блог компании InterSystems, Веб-разработка, сокеты, субд Caché, метки: , , , , , , , ,

Работа с сокетами в СУБД Caché. Пример реализации серверной части протокола WebSocket
СУБД Caché для взаимодействия через TCP/IP с удалёнными процессами посредством сокетов предоставляет низкоуровневые команды, что может представлять собой сложность для новичков.

А есть ли возможность использовать сокеты «по-другому», не теряя при этом в гибкости, скорости и удобстве разработки?


Конечно, есть: достаточно написать объектную обёртку вокруг существующих команд open, use, close и т.д. В терминах ООП — это инкапсуляция.

К счастью такой класс, даже классы — один для серверной части и один для клиентской — уже написаны и поставляются по крайней мере с версии Caché 5.2, а именно:

  • %IO.ServerSocket — для работы на стороне сервера;
  • %IO.Socket — для работы на стороне клиента;

Рассмотрим два простых примера с их использованием.

Пример №1

Код серверной части

  #dim sock As %IO.ServerSocket = ##class(%IO.ServerSocket).%New()
  
set sock.TranslationTable="UTF8"

  write #,"Запуск сервера...", !

  #; параметры: pPort, pTimeout, pSC
  
do sock.Open(7001, 1, .sc)

  if $$$ISOK(sc) {

    do sock.Listen()

    do{
      
#; параметры: pMaxReadLen, pTimeout
      
set s=sock.ReadAny(250, 5)
      
write s,!

      #; параметры: pData, pFlush
      
do sock.Write($$$FormatText("Сообщение с сервера %1",$$$quote(s)), $$$YES)
    
}while(s'="bye!")

    do sock.Close()

    write "Останов сервера...", !
  
}

Код клиентской части

  #dim sock As %IO.ServerSocket = ##class(%IO.Socket).%New()
  
set sock.TranslationTable="UTF8"

  write #,"Запуск клиента...", !

  #; параметры: pHost, pPort, pTimeout, pSC
  
do sock.Open("127.0.0.1","7001",1,.sc)

  if $$$ISOK(sc) {

    #; параметры: pData, pFlush
    
do sock.Write("Команда с клиента", $$$YES)

    #; параметры: pMaxReadLen, pTimeout
    
write sock.ReadAny(250, 5),!

    hang 2
    
    
do sock.Write("bye!", $$$YES)
    
write sock.ReadAny(250, 5),!

    do sock.Close()

    write "Останов клиента...", !
  
}

  • Результаты на сервере:
    Запуск сервера...
    Команда с клиента
    bye!
    Останов сервера...
  • Результаты на клиенте:
    Запуск клиента...
    Сообщение с сервера "Команда с клиента"
    Сообщение с сервера "bye!"
    Останов клиента...

Останавливаться подробно на примере не имеет смысла, так как всё достаточно прозрачно.
Замечу лишь, что при получении от клиентской части стоп-строки "bye!" серверная часть завершает свою работу.

Пример №2

Код:

Class demo.socket Extends %RegisteredObject
{

ClassMethod Server2()
{
  
#; do ##class(demo.socket).Server2()
  
  
#dim sock As %IO.ServerSocket = ##class(%IO.ServerSocket).%New()

  write #,"Запуск сервера...", !

  do sock.Open(10081, 1, .sc)

  if $$$ISOK(sc) {

    do sock.Listen(-1, .sc)

    do{
      
set s=sock.ReadAny($$$MaxLocalLength,, .sc)
      
write s,!
      
do sock.Write(s, $$$YES, .sc)
    
}while(s'="bye!")
    
    
do sock.Close()

    write "Останов сервера...", !
  
}
}

ClassMethod Client2(end As %Boolean = {$$$NO})
{
  
#; do ##class(demo.socket).Client2(1)

  #dim sock As %IO.ServerSocket = ##class(%IO.Socket).%New()

  write #,"Запуск клиента...", !

  do sock.Open("127.0.0.1","10081",1,.sc)

  if $$$ISOK(sc) {

    set time=$zhorolog

    for i=1:1:10 {
      
do sock.Write("1234567890", $$$YES, .sc)
      
set s=sock.ReadAny($$$MaxLocalLength,, .sc)
      
write s,!
    
}

    write "Время= ",$zhorolog-time," сек.",!

    do:end sock.Write("bye!", $$$YES, .sc)

    do sock.Close()

    write "Останов клиента...", !
  
}else{
    
write $system.Status.GetErrorText(sc,"ru"),!
  
}
}

}

Пример реализации серверной части для поддержки протокола WebSocket

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

Как быть, если нам необходимо обрабатывать одновременно десятки/сотни клиентских подключений?
Для этого следует воспользоваться методом %IO.ServerSocket:ListenJob()

Давайте напишем пример посложнее, например, добавим в СУБД Caché поддержку протокола WebSocket.

Это будет особенно актуально, учитывая что СУБД Caché предоставляет технологию создания веб-приложений (CSP), а также имеет готовый фреймворк с богатым набором визуальных, MVC и других компонент (ZEN).

А вспомнив о том, что СУБД Caché помимо поддержки ООП и SQL, может выступать ещё и как NoSQL хранилище…

О самом протоколе WebSocket в интернете написано немало. На его основе пишут чаты, онлайн-игры и всё то, где требуется быстрый обмен данными с сервером (базой данных).
Отмечу, что наш демонстрационный пример будет поддерживать лишь две версии протокола: 76 и 7.

Для тестов нам понадобится один из современных браузеров, имеющих поддержку WebSocket.
Для старых браузеров, например IE 8, можно воспользоваться специальным плагином, который работает поверх Flash.

Технические подробности реализации протокола интересующиеся могут найти в исходниках в конце статьи.

Здесь же мы рассмотрим лишь небольшой пример, реализующий простейший веб-чат.

Примечание: Исходный код реализации протокола WebSocket на языке Caché ObjectScript служит только для ознакомительных целей.

Итак:

  1. в Студии импортируйте проект в область с настроенной поддержкой веб-приложений, например «SAMPLES»;
  2. создайте свой класс обработчик событий на стороне сервера, наследуясь от класса net.WebSocketEvents и переопределите в нём соответствующие методы.

    Например:

    /// Демонстрационный класс обработки серверных событий.
    Class demo.Server Extends net.WebSocketEvents
    {

    /// обработчик события onbeforeconnect.
    /// <br> Здесь мы можем отклонить подключение для некоторых пользователей.
    Method
    onbeforeconnect() As %Boolean
    {
      
    set ^tmp($i(^tmp),"onbeforeconnect")=$lb(..WebSocketGET,..WebSocketHost,..WebSocketOrigin,..WebSocketVer)
      
    q $$$YES
    }

    /// обработчик события onconnect.
    Method
    onconnect()
    {
      
    set ^tmp($i(^tmp),"onconnect")=""
    }

    /// обработчик события onmessage.
    /// <br> Параметры:
    /// <br><var>msg</var> - данные, пришедшие от клиента.
    Method
    onmessage(msg As %String)
    {
      
    set ^tmp($i(^tmp),"onmessage")=msg

      #; анализируем данные от клиента и в зависимости от этого выполняем те или иные действия на сервере.

      /*

      do ..send("Cachéйцуasd"_$random(1000)):msg="get",
         ..send($replace($j("",32000)," ","é")):msg="getBig",
         ..sendBroadcast("from Caché to All"):msg="toAll",
         ..sendBroadcast($$$FormatText("Подключился пользователь: %1",$$$quote($p(msg,"^",2)))):$e(msg)="^"

      */

      if msg="get" {
        
    do ..send("Cachéйцуasd"_$random(1000))
      
    }elseif msg="getBig" {
        
    do ..send($replace($j("",32000)," ","é"))
      
    }elseif msg="toAll" {
        
    do ..sendBroadcast("from Caché to All")
      
    }elseif $e(msg)="^" {
        
    do ..sendBroadcast($$$FormatText("Подключился пользователь: %1",$$$quote($p(msg,"^",2))))
      
    }
    }

    /// обработчик события onclose.
    Method
    onclose()
    {
      
    set ^tmp($i(^tmp),"onclose")=""
    }

    }

  3. если вы используете плагин, использующий Flash (см. выше), то предварительно установите последнюю версию Flash Player, который можно скачать отсюда.

    Примечание: Не забудьте включить WebSocket в браузерах, в которых они по умолчанию отключены, например в Opera.

  4. запустите на сервере демон обработки WebSocket-подключений:
    SAMPLES>do ##class(net.WebSocketServer).Start("demo.Server")
  5. запустите в браузере, например FireFox, страницу (класс) demo.webclient, исходный код которой следующий:

    Class demo.webclient Extends %ZEN.Component.page
    {

    /// If true, then attempt to refresh this page when its session timeout period has expired.
    /// This will cause a login page to display if the current session has ended
    /// and security is set to require login.
    Parameter 
    AUTOLOGOUT As BOOLEAN = 0;

    /// Comma-separated list of additional JS include files for the page.
    Parameter 
    JSINCLUDES As STRING = "websocket/swfobject.js,websocket/web_socket.js";

    XData Contents [ XMLNamespace "www.intersystems.com/zen" ]
    {
    <
    page xmlns="www.intersystems.com/zen" title="">
      <
    vgroup labelPosition="left">
        <
    label id="lb" label="Статус подключения:"/>
        <
    text id="usr" label="Логин:"/>
        <
    button caption="Старт" onclick="zenPage.start();"/>
        <
    button caption="Команда 1" label="Hello123xcf789фйбоз" onclick="ws.send('Hello123xcf789фйбоз');"/>
        <
    button caption="Команда 2" label="toAll" onclick="ws.send('toAll');"/>
        <
    button caption="Команда 3" label="get" onclick="ws.send('get');"/>
        <
    button caption="Команда 4" label="getBig" onclick="ws.send('getBig');"/>
        <
    button caption="Останов" onclick="ws.close();"/>
      </
    vgroup>
    </
    page>
    }

    ClientMethod start() [ Language = javascript ]
    {
      ws 
    new WebSocket("ws://127.0.0.1:10081/asd s HTTP/1.1///");
      ws.onopen 
    function() {
        zenSetProp(
    'lb','value','open');
        ws.send(
    '^'+zenGetProp('usr','value'));
      };
      ws.onmessage 
    function(e) {
        zenAlert(
    'onmessage',' length=',e.data.length,' data=',e.data);
      };
      ws.onclose 
    function() {
        zenSetProp(
    'lb','value','close');
      };
      ws.onerror 
    function() {
        zenAlert(
    'onerror');
      };
    }

    /// This client event, if present, is fired when the page is loaded.
    ClientMethod 
    onloadHandler() [ Language = javascript ]
    {
      
    // Let the library know where WebSocketMain.swf is:
      
    WEB_SOCKET_SWF_LOCATION "websocket/WebSocketMain.swf";
      WEB_SOCKET_DEBUG 
    false;
    }

    }

  6. нажмите сперва кнопку «Старт». Если соединение браузера с СУБД Caché прошло успешно, то вы увидите статус «open».
    Если соединения нет, то проверьте раздел Troubleshooting. На локальной машине всё должно работать без проблем.
  7. когда соединение успешно установлено, вы можете пробовать различные тесты — в примере их четыре — в любой последовательности.
  8. в конце тестирования не забудьте нажать кнопку «Останов».
  9. попробуйте запустить несколько браузеров и повторите шаги 6-8.

Давайте посмотрим содержимое журнала на сервере:

  • FireFox 12
    USER>zw ^tmp
    ^tmp=8
    ^tmp(1,"onbeforeconnect")=$lb("/asd%20s%20HTTP/1.1///","127.0.0.1:10081","","hybi-10")
    ^tmp(2,"onconnect")=""
    ^tmp(3,"onmessage")="^sdsdf"
    ^tmp(4,"onmessage")="Hello123xcf789фйбоз"
    ^tmp(5,"onmessage")="toAll"
    ^tmp(6,"onmessage")="get"
    ^tmp(7,"onmessage")="getBig"
    ^tmp(8,"onclose")=""
  • IE 9
    USER>zw ^tmp
    ^tmp=8
    ^tmp(1,"onbeforeconnect")=$lb("/asd s HTTP/1.1///","127.0.0.1:10081","","hybi-10")
    ^tmp(2,"onconnect")=""
    ^tmp(3,"onmessage")="^dfg"
    ^tmp(4,"onmessage")="Hello123xcf789фйбоз"
    ^tmp(5,"onmessage")="toAll"
    ^tmp(6,"onmessage")="get"
    ^tmp(7,"onmessage")="getBig"
    ^tmp(8,"onclose")=""

Использование защищённого протокола wss вместо ws

Для этого вам понадобится:

  1. настроить в Портале серверную SSL-конфигурацию с именем, например, WebSocketSSL и установить "Peer certificate verification level" в "None".

    Примечание: Детали по настройке SSL в СУБД Caché можно посмотреть в одной из предыдущих статей данного блога.

  2. при запуске WebSocket-демона нужно указать нашу SSL-конфигурацию:
    SAMPLES>do ##class(net.WebSocketServer).Start("demo.Server",,"WebSocketSSL")
  3. если вы используете плагин, использующий Flash (см. выше), то предварительно запустите на порту 843 демон обработки файла политики:
    SAMPLES>do ##class(net.WebSocketServer).StartPolicy()
  4. перекомпилируйте класс demo.webclient предварительно поменяв строку
    "ws://127.0.0.1:10081 ..."

    на

    "wss://127.0.0.1:10081 ..."
  5. далее см. выше.

В итоге мы имеем веб-приложение, серверную часть и базу данных, реализованные исключительно в рамках СУБД Caché.

Исходный проект

Автор: servitRM

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js