Новое в СУБД Caché 2013.1: встроенная поддержка WebSockets

в 4:46, , рубрики: cache, dbms cache, intersystems cache, WebSocket, Блог компании InterSystems, Веб-разработка, Программирование, субд Caché, метки: , , , ,

В одной из предыдущих статей уже рассматривалась работа с WebSocket на примере собственной серверной реализации этого протокола поверх обычных сокетов.

В СУБД Caché 2013.1 CSP-Шлюз теперь включает поддержку спецификации HTML 5 для WebSocket-соединений между веб-сервером и HTML 5 совместимым браузером. Эта функция доступна для Apache 2.2 и выше, и для IIS 8.0, который является частью Windows Server 2012.

Поскольку в Caché 2013.1 уже встроен Apache 2.4, мы будем наши примеры запускать именно на нём.
Для реализации клиентской части использовался фреймворк ZEN, но вы можете переделать примеры и на технологию CSP или любую другую.

Итак приступим.

Теория

Как уже было сказано выше, теперь всю внутреннюю логику по поддержке WebSocket на стороне сервера СУБД Caché берёт на себя. От программиста требуется лишь создать свой класс, унаследовав его от класса %CSP.WebSocket, и переопределить в нём несколько методов.
Объект класса %CSP.WebSocket служит в качестве обработчика событий для связи между клиентом и сервером с использованием протокола WebSocket. Все WebSocket-сервера наследуются от %CSP.WebSocket.
Подробное описание методов и свойств класса %CSP.WebSocket можно найти в справочнике классов.
Здесь отмечу кратко лишь некоторые из них:

Метод/Свойство Описание
Read() получить данные от клиента
Write() отправить данные клиенту
OnPreServer() обработчик события PreServer: вызывается перед стартом WebSocket-сервера
OnPostServer() обработчик события PostServer: вызывается после останова WebSocket-сервера
Server() собственно сам WebSocket-сервер
EndServer() остановить WebSocket-сервер
AtEnd свойство принимает значение true (1), когда во время чтения, WebSocket-сервер достигает конца текущего кадра данных
SharedConnection свойство определяет, будет ли обмен информацией между клиентом и WebSocket-сервером происходить по выделенному соединению CSP-Шлюза или через пул разделяемых соединений (пока не используется)

Практика

Давайте рассмотрим простой пример, как всё это можно использовать на практике.
Для начала создадим в Студии в области USER ZEN-страницу — класс demo.WebSocket — и унаследуем её от класса %CSP.WebSocket:

Class demo.WebSocket Extends (%ZEN.Component.page%CSP.WebSocket)
{

XData Contents [ XMLNamespace "www.intersystems.com/zen" ]
{
<
page xmlns="www.intersystems.com/zen" title="">
</
page>
}

}

Подключим файл zenCSLM.js для поддержки работы с JSON:

Parameter JSINCLUDES = "zenCSLM.js";

Реализуем методы для подключения клиента по WebSocket и обработки соответствующих событий:

/// This client event, if present, is fired when the page is loaded.
ClientMethod 
onloadHandler() [ Language = javascript ]
{
  ws
=null;

  url=((window.location.protocol == 'https:''wss:' 'ws:''//' window.location.host window.location.pathname;

  wsCtor window['MozWebSocket'MozWebSocket window['WebSocket'WebSocket null;
  
  
if (zenIsMissing(wsCtor)) zenAlert('WebSocket НЕ поддерживается вашим браузером!');
}

ClientMethod start() [ Language = javascript ]
{
  
if (!zenIsMissing(wsCtor)) {
    
if (zenIsMissing(ws)) {
      ws 
new wsCtor(url);
      
      ws.onopen 
function() {
        zenAlert(
'onopennnreadyState: ',ws.readyState,'nbinaryType: ',ws.binaryType,'nbufferedAmount: ',ws.bufferedAmount);
      };
      
      ws.onmessage 
function(e) {
        zenAlert(e.data);
      };
      
      ws.onclose 
function(e) {
        zenAlert(
'onclosennwasClean: ',e.wasClean,'ncode: ',e.code,'nreason: ',e.reason);
        ws
=null;
      };
      
      ws.onerror 
function(e) {
        zenAlert(
'onerror');
      };
    }
  }
}

Поскольку реализация WebSocket-сервера находится на этой же странице, строка подключения будет идентичной за исключением протокола. То есть, если url нашей ZEN-странички имеет вид
http://localhost:57772/csp/user/demo.WebSocket.cls
, то для WebSocket-сервера это будет
ws://localhost:57772/csp/user/demo.WebSocket.cls

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

XData Contents [ XMLNamespace "www.intersystems.com/zen" ]
{
<
page xmlns="www.intersystems.com/zen" title="">
<
text id="txt" label="Текст для отправки" value="Мир"/>
<
button caption="1.Подключиться" onclick="zenPage.start();"/>
<
button caption="2.Отправить текст" onclick="if (!zenIsMissing(ws)) ws.send(zenGetProp('txt','value'));"/>
<
button caption="3.Отправить длинную строку" onclick="zenPage.sendLongStr(100000);"/>
<
button caption="4.Отправить JSON" onclick="zenPage.sendJSON();"/>
<
button caption="5.Отключиться" onclick="if (!zenIsMissing(ws)) ws.close();"/>
</
page>
}

Код метода sendLongStr следующий:

ClientMethod sendLongStr(N) [ Language = javascript ]
{
  
if (zenIsMissing(ws)) return;
  
  
var s='a';
  
for(var i=1;i<N;i++) s+='a';
  ws.send(s);
}

Код метода sendJSON:

ClientMethod sendJSON() [ Language = javascript ]
{
  
if (zenIsMissing(ws)) return;
  
  
var obj={
   
"firstName""Иван",
   
"lastName""Иванов",
   
"address": {
       
"streetAddress""Московское ш., 101, кв.101",
       
"city""Ленинград",
       
"postalCode"101101
   
},
   
"phoneNumbers": [
       
"812 123-1234",
       
"916 123-4567"
   
]
  };
  
  ws.send(ZLM.jsonStringify(obj));
}

Пришло время реализации собственно нашего WebSocket-сервера. Для этого переопределим методы OnPreServer, OnPostServer и Server, как показано ниже:

Method OnPreServer() As %Status
{
  
Do $system.Process.Undefined(2)

  Set ^tmp($Increment(^tmp),"OnPreServer")=""
  
Quit $$$OK
}

Method OnPostServer() As %Status
{
  
Set ^tmp($Increment(^tmp),"OnPostServer")=""
  
Quit $$$OK
}

Method Server() As %Status
{
  
For  {
    
    
Set len=32656
    
Set data=$ZConvert(..Read(.len,.status),"I","UTF8")
    
    
If $$$ISOK(status{
      
      
If data="Мир" {
        
Do ..Write($ZConvert("Привет, "_data_"!","O","UTF8"))
      
}ElseIf data="bye" {
        
        
; принудительно завершаем работу WebSocket-сервера и выходим из бесконечного цикла
        ; на клиенте при этом сработает onclose
        
Do ..EndServer()
        
Quit
        
      
}Else{
        
        
#Dim obj As %RegisteredObject=$$$NULLOREF
        
        Set 
^tmp=$Increment(^tmp)

        ; преобразовываем строку в объект
        
If $$$ISOK(##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(data,,.obj)) {
          
          
; если нет ошибки, сохраняем значения свойств
          
Set ^tmp(^tmp,"Server","firstName")=obj.firstName
          Set 
^tmp(^tmp,"Server","lastName")=obj.lastName
          Set 
^tmp(^tmp,"Server","address.streetAddress")=obj.address.streetAddress
          Set 
^tmp(^tmp,"Server","address.city")=obj.address.city
          Set 
^tmp(^tmp,"Server","address.postalCode")=obj.address.postalCode
          Set 
^tmp(^tmp,"Server","phoneNumbers.1")=obj.phoneNumbers.GetAt(1)
          
Set ^tmp(^tmp,"Server","phoneNumbers.2")=obj.phoneNumbers.GetAt(2)
          
          
; Меняем фамилию
          
Set obj.lastName="Сидоров"
          
          ; Добавляем ещё один телефон
          
Do obj.phoneNumbers.Insert("111 111-1111")
          
          
; Добавляем к объекту ещё два новых свойства
          
Set obj.name="Вася"
          
Set obj.street="ул. Мира 17"
          
          ; Конвертируем изменённый объект в строку и отсылаем клиенту обратно
          
Do ..Write(..Write2Str(.obj))
          
        
}Else{
          
          
; сохраняем данные при получении длинной строки
          
Set ^tmp(^tmp,"Server","longStr")=..AtEnd_":"_$Length(data)_":"_len
        
}
      }
    }
Else{
      
Quit:($$$GETERRORCODE(status)=$$$CSPWebSocketClosed)
    
}
  }
  
Quit $$$OK
}

Служебный метод Write2Str служит для сборки в строку, выводимых командой Write, данных:

ClassMethod Write2Str(ByRef objAs %String Private ]
{
  
Try{
    
Set tIO=$IO,tXDEV="|XDEV|"_+$Job
    Do 
{
      
      
// For $$$IsUnicode use UTF-8
      
Open tXDEV:($ZF(-6,$$$XSLTLibrary,12):"":"S":/HOSTNAME="XSLT":/IOT=$Select($$$IsUnicode:"UTF8",1:"RAW"):/IBU=16384:/OBU=16384)
      
Use tXDEV
      
      
Quit:$$$ISERR(obj.%ToJSON(,"aeloiwu"))
      
      
// Flush any remaining output
      
Write *-3
      
      
// Now read back a string (up to the maximum possible length, 32k or ~4MB for long strings)
      
Set ""
      
While (1) {
        
#Dim tChunk As %String
        
Read tChunk:0
        
Quit:'$Length(tChunk)
        
Set tChunk
      
}
      
    } 
While (0)
  
}Catch{}

  Close tXDEV
  
Use tIO
  
Quit s
}

Осталось скомпилировать наш класс (Ctrl+F7) и открыть его для просмотра в браузере (F5).
После последовательного нажатия на кнопки содержимое глобала ^tmp будет следующим:

^tmp=7
^tmp(1,"OnPreServer")=""
^tmp(2,"Server","longStr")="0:32656:32656"
^tmp(3,"Server","longStr")="0:32656:32656"
^tmp(4,"Server","longStr")="0:32656:32656"
^tmp(5,"Server","longStr")="1:2032:2032"
^tmp(6,"Server","address.city")="Ленинград"
^tmp(6,"Server","address.postalCode")=101101
^tmp(6,"Server","address.streetAddress")="Московское ш., 101, кв.101"
^tmp(6,"Server","firstName")="Иван"
^tmp(6,"Server","lastName")="Иванов"
^tmp(6,"Server","phoneNumbers.1")="812 123-1234"
^tmp(6,"Server","phoneNumbers.2")="916 123-4567"
^tmp(7,"OnPostServer")=""

Скачать исходник класса demo.WebSocket.

Автор: servitRM

Источник

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


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