Поинтегрируем: WebSocket’ы в платформе 1С. Часть 1
Прошло уже более полугода после того момента, как я написал первую часть о новой для 1С технологии WebSocket. За это время фирма 1С подлатала баги по данной технологии, и на просторах Инфостарт появилась ещё одна замечательная статья, которую почему-то многие не заметили.
В комментариях к первой части спрашивали про это, и давайте я сделаю вот так:
Использование WebSockets для работы с RabbitMQ посредством протокола WebSTOMP
В первой части мы с вами узнали:
• В чём разница между http-сервисами и WebSocket’ами
• Посмотрели на WebSocket’ы в СУБД
• Далее мы развернули node.js и создали серверную часть WebSocket
• Подключились к своему серверу, используя Postman и 1С. Тогда мы создавали WebSocket-клиент в самой конфигурации
Во второй части:
• Узнаем, что было до появления WebSocket
• Разберёмся, как отправлять сообщения конкретному клиенту
• Сделаем серверную часть на JS
• Сделаем клиентскую часть на HTML\JS
• Создадим конфигурацию с обработкой для тестирования WebSocket’ов
• Посмотрим некоторые новые инструменты для тестирования
Поехали!
На самом деле существует технология, которая до сих пор используется вместо WebSocket в некоторых ситуациях. Чаще всего это связано с ограничениями браузера или требованиями безопасности.
Называется она Long Polling (длинные опросы). Кстати, она применяется, например, в Telegram.
Принцип работы длинных опросов:
Клиент отправляет запрос с длительным таймаутом, например, на 30 секунд.
Сервер удерживает это соединение и, если появляются данные — отдает их. Клиент принимает данные и тут же посылает новый запрос с большим таймаутом.
Если проводить аналогию, то это похоже на рыбалку:
Закинули удочку и ждёте. Начинает клевать — достали, обновили наживку и снова закинули.
Через 30 секунд клева нет — обновили наживку и закинули заново.
Еще хочется рассказать про Socket.IO, где, кстати, принцип длинных опросов используется как резервный канал.
Пару слов о Socket.IO
Помните, в предыдущей части мы в Postman подключались к WebSocket, а рядом была возможность подключиться к Socket.IO.
По большому счету в мире JS чаще всего для WebSocket используют две библиотеки: WS и Socket.IO.
• WS — это минималистичная, легковесная и производительная библиотека, которая реализует чистый протокол WebSocket (RFC 6455). Подходит для проектов, где нужен простой и быстрый транспорт, и где разработчик готов самостоятельно управлять переподключениями и обработкой ошибок.
• Socket.IO — это более мощная и комплексная библиотека, которая поверх WebSocket поддерживает и fallback-методы (например, long polling) для случаев, когда WebSocket недоступен. Она обеспечивает удобный событийно-ориентированный API, автоматическое переподключение, работу с комнатами и пространствами имён, а также упрощает разработку более надежных и масштабируемых приложений.
Зайдем в документацию по Socket.IO -> https://socket.io/docs/v4/how-it-works/
То есть Socket.IO это надстройка над WebSocket и если по какой-то причине WebSocket недоступен, то работа будет осуществляться через длительные опросы.
Сама по себе Socket.IO упрощает большинство вещей, которые придется программировать на WS.
Есть хорошая статья по этому сравнению WS и Socket.IO: Разница между веб-сокетами и Socket.IO
Насколько я понимаю, использовать Socket.IO мы сможем только через «Поле HTML документа», поэтому я не вижу смысла говорить про Socket.IO, но пример сервера и клиентской html\js части приложу к репозиторию данной статьи.
Сервер.
В первой части мы создали с вами серверную часть, которая отправляет сообщения всем подключённым клиентам и хранит все сообщения с момента запуска сервера.
Это замечательно, но в жизни у нас бывает не так.
Как быть, если сообщение нужно отправлять не всем?
Очевидно, что нужно каким-то образом хранить клиентов и их данные, чтобы понимать, кому отправлять сообщения. И нужен какой-то тип сообщений, чтобы понимать, является ли это сообщением или уведомлением о подключении нового клиента.
// Подключаем библиотеку ws
const WebSocket = require('ws');
// Запускаем WebSocket сервер локально на порту 3001
const server = new WebSocket.Server({ port: 3001 });
// Словарь для сопоставления userID и WebSocket
const userSockets = {};
// Режим с выводом в консоль логов
const debuglog = true;
// Делаем обработчик для подключения
server.on('connection', (ws) => {
let userID;
// обработчик сообщений
ws.on('message', (message) => {
const data = JSON.parse(message);
// проверяем сообщение с типом регистрация
if (data.type === 'register') {
// При регистрации сохраняем соответствие data.id и ws
userSockets[data.id] = ws;
if (debuglog) {
console.log(`User ${data.id} registered`);
}
// Проверяем сообщение с типом сообщение
} else if (data.type === 'message') {
// если кому незаполнен, тогда отправляем всем кроме автора
if (!data.to) {
// Отправляем сообщение всем
for (const id in userSockets) {
// Проверяем, что получатель не совпадает с отправителем
if (id !== data.from) {
sendToUser(id, data.from, data.message);
}
}
} else {
// Отправляем сообщение конкретному пользователю
sendToUser(data.to, data.from, data.message);
if (debuglog) {
console.log(`Message received from ${data.to}: ${data.message}`);
}
}
}
});
ws.on('close', () => {
// Удаляем соединение при отключении
delete userSockets[userID];
if (debuglog) {
console.log(`Connection closed for ${userID}`);
console.log(userSockets);
}
});
});
// Функция для отправки сообщения определенному пользователю
function sendToUser(id, idFrom, message) {
const ws = userSockets[id];
// проверяем что соединение есть и оно активно
if (ws && ws.readyState === WebSocket.OPEN) {
const textMessage = `${idFrom}: ${message}`;
ws.send(textMessage);
if (debuglog) {
console.log(`Message sent to ${textMessage}`);
}
} else {
// Если отправить сообщение неудалось, тогда сообщаем отправителю
const textERR = `User ${id} not found or not connected`;
if (debuglog) {
console.log(textERR);
}
// Если нужно сообщение, что юзер не подключен или не найден. Получит отправитель.
//if (id !== idFrom) {
// const senderWs = userSockets[idFrom];
// if (senderWs && senderWs.readyState === WebSocket.OPEN) {
// senderWs.send(textERR);
// }
//}
}
}
Пояснение:
userSockets – объект, который будет хранить в себе имя клиента и сессию. Очень похож на соответствие в 1С.
Также на сервере есть debuglog = true, если его выставить в false, тогда логи не будут мозолить глаза.
Серверная часть будет работать по такой логике:
Клиент подключается, а затем отправляет первое сообщение, содержащее JSON:
{ "type": "register", "id": "ИмяКлиента" }
type говорит нам, что это регистрация, то есть новое подключение.
id - это имя подключившегося клиента
Сервер принимает сообщение и записывает в userSockets нового клиента и его сессию.
Далее клиент отправляет сообщения с другим JSON:
{ "type": "message", "from": "ИмяКлиентаОтправителя", "to": ИмяКлиентаПолучателя, "message": "Сообщение" }
Тут type говорит нам, что это сообщение, которое надо передать.
from содержит имя клиента, который отправил сообщение.
to содержит имя получателя, null или пустую строку. Если равно null или пустая строка, тогда отправляется всем, кроме отправителя. Сообщение отправителю создаём на клиенте сами.
message содержит текст сообщения.
Важно! Чтобы клиенту было отправлено сообщение, его сессия должна быть открыта, в противном случае в логах будет ошибка, что клиент не найден или отключён. Кстати, в конце есть закомментированный код, который позволяет отправить это сообщение на клиент.
Клиент.
В первой части я не стал делать клиент под HTML/JS, а сразу сделал конфигурацию на 1С… Вы знаете, когда человек на больничном, у него больше свободного времени 😉
В этой части я скидал клиента, тем более большую часть работы за меня сделал ИИ — примерно 80% — и выдал нерабочий вариант, но починить и доработать труда не составило.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>WebSocket чат</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 30px auto; }
#messages {
border: 1px solid #ccc;
height: 300px;
padding: 10px;
overflow-y: auto;
margin-bottom: 10px;
background: #f9f9f9;
}
#status {
margin-bottom: 10px;
font-weight: bold;
}
input[type="text"] {
width: calc(100% - 12px);
padding: 5px;
margin-bottom: 10px;
}
button {
padding: 7px 15px;
}
</style>
</head>
<body>
<h1>WebSocket чат</h1>
<div id="status">Статус: Не подключено</div>
<input type="text" id="username" placeholder="Введите ваше имя" />
<button id="registryButton">Войти</button>
<input type="text" id="usernameto" placeholder="Введите имя получателя" />
<input type="text" id="messageInput" placeholder="Введите сообщение" />
<button id="sendButton">Отправить</button>
<div id="messages"></div>
<script>
const statusEl = document.getElementById('status');
const messagesEl = document.getElementById('messages');
const usernameEl = document.getElementById('username');
const usernametoEl = document.getElementById('usernameto');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const registryButton = document.getElementById('registryButton');
sendButton.disabled = true; // Блокируем отправку сообщений до регистрации
messageInput.disabled = true;
// Подключение к WebSocket серверу — замените адрес на ваш
const socket = new WebSocket('ws://localhost:3001');
socket.onopen = () => {
statusEl.textContent = 'Статус: Подключено';
};
socket.onmessage = (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch {
data = null;
}
const msg = document.createElement('div');
if (data && data.type === 'message') {
msg.textContent = `${data.from} U94; ${data.to || 'all'}: ${data.message}`;
} else if (data && data.type === 'system') {
msg.textContent = 'Системное сообщение: ' + data.message;
msg.style.fontStyle = 'italic';
msg.style.color = 'gray';
} else {
// Если сообщение не в формате JSON
msg.textContent = event.data;
}
messagesEl.appendChild(msg);
messagesEl.scrollTop = messagesEl.scrollHeight;
};
socket.onclose = () => {
statusEl.textContent = 'Статус: Отключено';
sendButton.disabled = true;
messageInput.disabled = true;
};
socket.onerror = (error) => {
statusEl.textContent = 'Ошибка: ' + error.message;
};
registryButton.onclick = () => {
const username = usernameEl.value.trim();
if (!username) {
alert('Введите имя пользователя');
return;
}
if (socket.readyState !== WebSocket.OPEN) {
alert('Соединение не установлено. Попробуйте позже.');
return;
}
// Отправляем серверу сообщение о регистрации
const fullMessage = JSON.stringify({ type: 'register', id: username });
socket.send(fullMessage);
// Скрываем поле с именем и кнопку регистрации
registryButton.style.display = 'none';
usernameEl.style.display = 'none';
// Разблокируем поле ввода сообщения и кнопку отправки
sendButton.disabled = false;
messageInput.disabled = false;
statusEl.textContent = `Статус: Подключено как ${username}`;
};
sendButton.onclick = () => {
const username = usernameEl.value.trim();
const usernameto = usernametoEl.value.trim();
const message = messageInput.value.trim();
if (!message) {
alert('Введите сообщение');
return;
}
if (registryButton.style.display !== 'none') {
alert('Сначала войдите в систему');
return;
}
if (socket.readyState !== WebSocket.OPEN) {
alert('Соединение не установлено. Сообщение не отправлено.');
return;
}
const fullMessage = JSON.stringify({
type: 'message',
from: username,
to: usernameto || null,
message: message
});
// Добавляем сообщение в окно чата (от пользователя)
const msg = document.createElement('div');
msg.textContent = `you U94; ${usernameto || 'all'}: ${message}`;
messagesEl.appendChild(msg);
messagesEl.scrollTop = messagesEl.scrollHeight;
socket.send(fullMessage);
messageInput.value = '';
};
// Отправлять сообщение по Enter в поле ввода сообщения
messageInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
sendButton.click();
}
});
</script>
</body>
</html>
Пояснение:
status - блок для вывода статуса
username – поле ввода для фиксации имени отправителя
registryButton – кнопка для первичной регистрации нового клиента
usernameto – поле ввода для фиксации получателя
messageInput – отправляемое сообщение
sendButton – кнопка отправления сообщения
messages – блок для вывода сообщений
Клиентская часть будет работать по такой логике:
При запуске происходит коннект к WebSocket.
const socket = new WebSocket('ws://localhost:3001');
Далее блокируются все части, связанные с отправкой сообщения, пока не будет введено имя отправителя и не нажата кнопка регистрации «Войти».
При нажатии «Войти» будет отправлен JSON на сервер, и будут разблокированы элементы формы, отвечающие за отправку сообщений, а элементы, отвечающие за регистрацию, пропадут.
При отправке сообщения будет проверяться получатель, и если он не заполнен, тогда сообщение на форме будет содержать «you → all:», в противном случае — «you → ИмяПолучателя:».
Посмотрим, как работает все это на JS:
В первой части я уже говорил, что понадобится, и я рассчитываю, что у вас установлены: VSC, npm, NodeJS, 1C 8.3.27 или 8.5. Если нет, то посмотрите в первой части, где взять и как установить.
1 Помещаем index.js и index.html в отдельную папку и открываем эту папку через VSC
2 Установим библиотеку для websocket:
npm I ws
После выполнения в папке появятся файлы с описанием библиотек и папка с библиотекой ws.
3 Запускается серверная часть как обычно:
node index.js
Сервер будет запущен по адресу: ws://localhost:3001
Запустим index.html
Статус подключено говорит о том, что мы подключились к websocket.
Введем имя «Василий»
Нажимаем «Войти»
Посмотрите, что пишет терминал, где вы запустили Websocket
Ну, что же, первый клиент есть.
Давайте еще запусти парочку раз index.html и введем «Петя» и «Лена»
Теперь от Лены напишем «Всем привет!»
Мы увидим вот такую картину:
Обратите внимание, что ушло только два сообщения. Как я уже писал, в логике написано так, что автор сообщение получает не через WebSocket, а непосредственно на клиентской части.
Давайте теперь Петя напишет Лене «Ооо Ленка!»
Как видите, все работает.
Давайте теперь прикрутим 1С и еще какой-нибудь клиент.
Конфигурация лежит тут: https://github.com/dsdred/WebSocketIn1C/releases/download/2.0.0/1Cv8.cf
В конфигурации два модуля ВебСокетыСервер и ВебСокетыКлиент, в основном для логирования в регистре сведений СобытияВебСокеты.
Самое главное, что там есть это обработка КлиентДля2Части.
&НаКлиенте
Процедура ПриОткрытии(Отказ)
ОбработчикиСокета = Новый ОбработчикиWebSocketКлиентСоединения;
ОбработчикиСокета.Модуль = ЭтотОбъект;
ОбработчикиСокета.ОбработчикОткрытияСоединения = "ВебСокет_ОбработчикОткрытия";
ОбработчикиСокета.ОбработчикЗакрытияСоединения= "ВебСокет_ОбработчикЗакрытия";
ОбработчикиСокета.ОбработчикПолученияСообщения = "ВебСокет_ОбработчикПолученияСообщения";
ОбработчикиСокета.ОбработчикОшибки = "ВебСокет_ОбработчикОшибки";
WebSocketКлиентСоединения.ОткрытьСоединение("ws_localhost_3001", "ws://localhost:3001", ОбработчикиСокета,,"Администратор");
КонецПроцедуры
&НаКлиенте
Процедура ВебСокет_ОбработчикОткрытия(Соединение) Экспорт
СтруктураСоединение = ВебСокетыКлиент.ПараметрыСоединения(Соединение);
СоединениеJSON = ВебСокетыСервер.ЗаписатьДанныеВJSON(,СтруктураСоединение).Результат;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("Соединение", СоединениеJSON);
ВебСокетыСервер.ПриОткрытииСоединения(Соединение.Ключ, ПараметрыЗаписи);
КонецПроцедуры
&НаКлиенте
Процедура ВебСокет_ОбработчикПолученияСообщения(Соединение, Сообщение) Экспорт
НоваяСтрока = Общение.Добавить();
НоваяСтрока.ТекстСообщения = Сообщение;
СтруктураСоединение = ВебСокетыКлиент.ПараметрыСоединения(Соединение);
СоединениеJSON = ВебСокетыСервер.ЗаписатьДанныеВJSON(,СтруктураСоединение).Результат;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("Соединение", СоединениеJSON);
ПараметрыЗаписи.Вставить("Сообщение", Сообщение);
ВебСокетыСервер.ПриПолученииСообщения(Соединение.Ключ, ПараметрыЗаписи);
КонецПроцедуры
&НаКлиенте
Процедура ВебСокет_ОбработчикОшибки(Соединение, КодОшибки, Описание) Экспорт
СтруктураСоединение = ВебСокетыКлиент.ПараметрыСоединения(Соединение);
СоединениеJSON = ВебСокетыСервер.ЗаписатьДанныеВJSON(,СтруктураСоединение).Результат;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("Соединение", СоединениеJSON);
ПараметрыЗаписи.Вставить("Код",КодОшибки);
ПараметрыЗаписи.Вставить("Описание",Описание);
ВебСокетыСервер.ПриОшибке(Соединение.Ключ, ПараметрыЗаписи);
КонецПроцедуры
&НаКлиенте
Процедура ВебСокет_ОбработчикЗакрытия(Соединение, КодЗакрытия) Экспорт
СтруктураСоединение = ВебСокетыКлиент.ПараметрыСоединения(Соединение);
СоединениеJSON = ВебСокетыСервер.ЗаписатьДанныеВJSON(,СтруктураСоединение).Результат;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("Соединение", СоединениеJSON);
ПараметрыЗаписи.Вставить("Код",КодЗакрытия);
ВебСокетыСервер.ПриЗакрытииСоединения(Соединение, ПараметрыЗаписи);
ВидимостьДоступностьЭлементов(Ложь);
КонецПроцедуры
&НаКлиенте
Процедура Зарегистрироваться(Команда)
ВебСокет = WebSocketКлиентСоединения.ПолучитьСоединение("ws_localhost_3001");
СтруктураРегистрации = Новый Структура("type,id","register",ИмяКлиента);
ТекстСообщения = ВебСокетыСервер.ЗаписатьДанныеВJSON(,СтруктураРегистрации).Результат;
СостояниеСоединения = ВебСокет.ПолучитьСостояние();
СостояниеОткрыто = (СостояниеСоединения = СостояниеWebSocketСоединения.Открыто);
Если СостояниеОткрыто Тогда
// отправка данных в WebSocket
ВебСокет.ОтправитьСообщение(ТекстСообщения);
КонецЕсли;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("Сообщение", ТекстСообщения);
ВебСокетыСервер.ПриЗакрытииСоединения("ws_localhost_3001", ПараметрыЗаписи);
ВидимостьДоступностьЭлементов(СостояниеОткрыто);
КонецПроцедуры
&НаКлиенте
Процедура ОтправитьСообщение(Команда)
ВебСокет = WebSocketКлиентСоединения.ПолучитьСоединение("ws_localhost_3001");
СтруктураСообщения = Новый Структура("type,from,to,message","message",ИмяКлиента,ИмяПолучателя,ТекстОтправки);
ТекстСообщения = ВебСокетыСервер.ЗаписатьДанныеВJSON(,СтруктураСообщения).Результат;
СостояниеСоединения = ВебСокет.ПолучитьСостояние();
СостояниеОткрыто = (СостояниеСоединения = СостояниеWebSocketСоединения.Открыто);
Если СостояниеОткрыто Тогда
// отправка данных в WebSocket
ВебСокет.ОтправитьСообщение(ТекстСообщения);
НоваяСтрока = Общение.Добавить();
НоваяСтрока.ТекстСообщения = ?(ПустаяСтрока(ИмяПолучателя),"you U94; all: ", "you U94; "+ИмяПолучателя+": ") + ТекстОтправки;
КонецЕсли;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("Сообщение", ТекстСообщения);
ВебСокетыСервер.ПриЗакрытииСоединения("ws_localhost_3001", ПараметрыЗаписи);
ВидимостьДоступностьЭлементов(СостояниеОткрыто);
КонецПроцедуры
Процедура ВидимостьДоступностьЭлементов(СостояниеОткрыто)
Элементы.Группа1.Видимость = Не СостояниеОткрыто;
Элементы.ИмяПолучателя.Видимость = СостояниеОткрыто;
Элементы.ТекстОтправки.Видимость = СостояниеОткрыто;
Элементы.ОтправитьСообщение.Видимость = СостояниеОткрыто;
Элементы.Общение.Видимость = СостояниеОткрыто;
КонецПроцедуры
Пояснение:
ИмяКлиента — Отправитель.
ИмяПолучателя — Получатель.
Общение — таблица значений для хранения сообщений.
ТекстОтправки — отправляемое сообщение.
Зарегистрироваться — команда регистрации.
ОтправитьСообщение — команда отправки сообщения.
Логика работы 1С:
Подключение к WebSocket происходит в процедуре ПриОткрытии, там же подключены обработчики событий, указано, что обработчики находятся в модуле формы.
В подключении указан ключ подключения «ws_localhost_3001» и адрес подключения «ws://localhost:3001».
Точно так же присутствуют две кнопки. Одна отвечает за регистрацию, другая — за отправку сообщений. Но я не сделал при регистрации проверку, что «ИмяКлиента» заполнено.
При любом событии происходит запись в регистр сведений «СобытияВебСокеты».
Посмотрим, как работает все это на 1С:
Запускаем обработку через Сервис
Давайте введем имя «1Сник» и нажмем «Войти»
Давайте отправим сообщение «Привет, коллеги!»
Давайте от Пети напишем 1Снику «Ты наконец то освоил WS?»
Как видите, все работает.
Чем можно еще тестировать?
Postman или любым подобным сервисом.
Нужно подключиться к websocket.
Отправить первым сообщением JSON:
{"type": "register", "id": "postman"}
Где id может быть другой, это имя нового клиента.
А дальше слать JSON с сообщением:
{"type": "message", "from": "postman", "to": null, "message": "Привет из postman всем"}
Или
{"type": "message", "from": "postman", "to": "1Сник", "message": "Напиши уже обработку для просмотра сессий вебсокетов!"}
Postman - https://www.postman.com/
Один из первых на рынке и сильно распространен, но в последнее время появилось много конкурентов.
insomnia - https://docs.insomnia.rest/
Очередной заменитель Postman
PieSocket WebSocket Tester - https://chromewebstore.google.com/detail/piesocket-websocket-teste/oilioclnckkoijghdniegedkbocfpnip
Плагин для Chrome
Wireshark - https://www.wireshark.org/
Это уже другого класса программа, она весь сетевой трафик смотрит.
Сервисы, которые не пробовал:
Apidog - https://apidog.com/
Еще один заменитьель Postman
k6 - https://k6.io/
Позволяет проводить нагрузочное тестирование.
На этом завершаю вторую часть.
Будет ли третья? Посмотрим, может быть. Все зависит от вашего интереса ;)
Всем удачи и интересных проектов!
Все материалы выложены в GitHub:
Вступайте в нашу телеграмм-группу Инфостарт