Введение в веб-сокеты
Содержание:
- Обработка разрывов соединения
- История массового перехода на https
- Connection close
- Реализуем часть протокола
- Простой клиент веб-сокетов
- High-level API¶
- A simple example
- Basic example¶
- FAQ
- Специальные требования к серверу
- Browser-based example¶
- Summary
- データ転送
- 2021: WSS Docs РД3
- Или getting started with WebSocket PHP без phpDaemon
Обработка разрывов соединения
Если соединение разрывается, оно автоматически восстанавливается браузером. Сервер может отправить таймаут для повторного завершения или закрытия соединения. В таком случае браузер попытается подключиться после завершения таймаута или не будет ничего делать, если соединение завершено.
Реализация образца сервера
Если клиент такой простой, возможно, сложной окажется реализация сервера? Обработчик сервера для SSE может выглядеть следующим образом:
function handler(response) { // настраиваем заголовки для ответа с целью получить постоянное HTTP-соединение response.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // составляем сообщение response.write('id: UniqueIDn'); response.write("data: " + data + 'nn'); // каждый раз, когда мы вводим два символа новой строки, сообщение отправляется автоматически }
Определяем функцию, которая будет обрабатывать ответ:
- Устанавливать заголовки;
- Создавать сообщение;
- Отправлять.
Обратите внимание, что здесь нет вызов метода send() или метода push(). Стандарт определяет: сообщение будет отправлено, как только в него будет добавлено два символа n n, как например: response.write(«data: » + data + ‘nn’);
В результате сообщение будет немедленно отправлено клиенту.
Составление сообщений
Сообщение может содержать несколько свойств:
1. ID
Если значение этого поля не содержит U + 0000 NULL, устанавливаем для буфера последнего идентификатора события значение поля. Иначе игнорируем поле.
2. Data
Добавляем значение поля в буфер, затем добавляем в буфер один символ U + 000A LINE FEED (LF).
3. Event
Устанавливаем для буфера тип события и значение поля. Это приводит к тому, что для event.type задается пользовательское имя события.
4. Retry
Если значение поля состоит только из цифр ASCII, тогда интерпретируем значение поля как целое число в десятичной системе исчисления. А также устанавливаем для времени повторного соединения потока событий это целое число. В противном случае игнорируем поле.
Все остальное будет проигнорировано. Мы не можем вводить собственные поля.
Пример с добавленным event:
response.write('id: UniqueIDn'); response.write('event: addn'); response.write('retry: 10000n'); response.write("data: " + data + 'nn');
В клиенте это обрабатывается с помощью addEventListener следующим образом:
source.addEventListener("add", function(event) { // выполняем действия с данными event.data; });
Вы можете отправлять несколько сообщений, разделенных символом новой строки, а также использовав для них разные идентификаторы.
... id: 54 event: add data: "" id: 55 event: remove data: JSON.stringify(some_data) id: 56 event: remove data: { data: "msg" : "JSON data"n data: "field": "value"n data: "field2": "value2"n data: }nn
Это значительно упрощает то, что мы можем сделать с нашими данными.
История массового перехода на https
С развитием сети Интернет со временем компании стали задумываться о безопасности данных пользователей передаваемых по сети. Времена HTTP/0.9 и HTTP/1.0 когда данные передавались в незашифрованном виде давно прошли. В середине нулевых с появлением большого количества веб-сайтов требующих авторизации и содержащих большое количество данных пользователей начало набирать популярность расширение протокола HTTP HTTPS (HTTP Sercure), которое обеспечивало ассиметричное шифрование тела HTTP запроса передаваемого по сети. А в середине десятых, в эру Facebook, Google и регуляторов Интернета из Евросоюза, запрос общества на полную защищенность передаваемых данных в сети Интернет сформировался в требование поддержки HTTPS для большинства сайтов. И это требование не то, что можно игнорировать – ведь сайты без поддержки HTTPS стали понижаться в поисковой выдаче, браузеры стали предупреждать о нежелательности посещения сайтов без поддержки HTTPS, а также отключать возможность смешанного использования HTTP и HTTPS в рамках одного веб-сайта. Нужно понимать, что это не прихоть, а к такому стремлению использовать везде HTTPS подтолкнуло развитие технологий анализа трафика (снифферов) позволяющих перехватывать все данные пользователей вплоть до паролей передаваемых по незашифрованному HTTP протоколу.
Connection close
Normally, when a party wants to close the connection (both browser and server have equal rights), they send a “connection close frame” with a numeric code and a textual reason.
The method for that is:
- is a special WebSocket closing code (optional)
- is a string that describes the reason of closing (optional)
Then the other party in event handler gets the code and the reason, e.g.:
Most common code values:
- – the default, normal closure (used if no supplied),
- – no way to set such code manually, indicates that the connection was lost (no close frame).
There are other codes like:
- – the party is going away, e.g. server is shutting down, or a browser leaves the page,
- – the message is too big to process,
- – unexpected error on server,
- …and so on.
The full list can be found in .
WebSocket codes are somewhat like HTTP codes, but different. In particular, any codes less than are reserved, there’ll be an error if we try to set such a code.
Реализуем часть протокола
Что бы в реализации сервера была какая то цель, нужно эту цель придумать. Целью кода данной статьи будет написание WebSocket сервера, который реализует часть протокола сокетов и позволяет переписываться нескольким клиентам из консоли браузера. Для начала нужно реализовать функционал опроса клиента с помощью управляющих фреймов Ping. Нам нужно знать, что клиент еще жив и готов принимать данные с сервера. Фрейм Ping, управляющий фрейм, но он так же может содержать данные. Когда клиент получит такое сообщение по сокету, он должен отправить на сервер фрейм Pong с теми данными, которые были во фрейме Ping. До реализации этого функционала, давайте пропишем в класс сервера необходимые константы
Далее реализуем наш метод по формированию фрейма Ping
По большому счету, в данном случае, это не требуется. Нам совершенно не обязательно пересылать какие-то данные клиенту вместе с управляющим фреймом Ping. Поэтому этот метод можно удалить, а вместо него в класс добавить еще одну константу. Также для того, чтобы реализовать функционал чата, нам потребуется хранить объекты подключений. Заведем под это отдельную коллекцию в классе сервера.
Модицифируем конструктор, добавим отправку фрейма Ping подключившимся клиентам с интервалом в 5 секунд, а также добавляем новых клиентов в коллекцию.
Теперь мы можем принимать соединения по сокетам и поддерживать его с помощью пингов. Осталось научить наш сервер маршрутизировать сообщения от клиентов. В спецификации к протоколу написано, что клиенты всегда должны отправлять сообщения на сервер в маскированном виде, а сообщения сервера всегда без маски. Из этого следует, что нам нужно раскодировать сообщение, а для этого нужно понять, что за сообщение пришло на сервер, получить маску, длину сообщения и сами данные. Напишем для этого метод
- В этой строке нам нужно получить длину данных внутри фрейма. Мы делаем это с помощью операции XOR и констранты, которая представляет число 128 в двоичном виде, которое выглядит как 10000000. В данном случае мы это делаем, исходя из того, что данные от клиента всегда приходят в маскированном виде, а значит первый бит этого байта всегда будет 1.
- Согласно спецификации для фреймов с длиной 126, длина сообщения передаётся в двух следующих байтах
- Согласно спецификации для фреймов с длиной 127, длина сообщения передаётся в восьми следующих байтах
С помощью этой функции мы можем получать всю необходимую информацию для обмена сообщениями между клиентами. Напишем метод, который будет демаскировать данные
Демаскирование происходит путем применения функции XOR к каждому байту данных и соответствующему ему байту маски. Длина маски указана в спецификации и составляет 4 байта. Теперь можно написать метод для отправки коротких сообщений по сокету клиенту.
Нам осталось финализировать конструктор класса. Добавим туда рассылку полученных сообщений от клиента всем активным клиентам, а также добавим отправку всем клиентам сообщения при подключении нового клиента.
Теперь можно запустить сервер. Для проверки работоспособности можно открыть две вкладки браузера и в консоли каждой вклдаки написать следующий код.
Затем отправить сообщение в одной из вкладок
Простой клиент веб-сокетов
С точки зрения веб-страницы функциональность веб-сокетов легко понять и использовать. Первый шаг — это создать объект WebSocket и передать ему URL. Код для этого подобен следующему:
Строка URL начинается с текста ws://, который идентифицирует подключение типа веб-сокет. Этот URL указывает файл веб-приложения на сервере (в данном случае это сценарий socketServer.php).
Стандарт веб-сокетов также поддерживает URL, которые начинаются с текста wss://, что указывает на требование использовать безопасное, зашифрованное подключение (точно так же, как и при запросе веб-страницы указывается URL, начинающийся с https:// вместо http://).
Веб-сокеты могут подключаться не только к своему веб-серверу. Веб-страница может открыть подключение к серверу веб-сокетов, исполняющемуся на другом веб-сервере, не требуя для этого никаких дополнительных усилий.
Само обстоятельство создания объекта WebSocket понуждает страницу пытаться подключиться к серверу. Дальше надо использовать одно из четырех событий объекта WebSocket: onOpen (при установлении подключения), onError (когда возникает ошибка), onClose (при закрытии подключения) и onMessage (когда страница получает сообщение от сервера):
Например, в случае успешного подключения неплохо бы отправить соответствующее подтверждающее сообщение. Такое сообщение доставляется с помощью метода send() объекта WebSocket, которому в качестве параметра передается обычный текст. Далее приведена функция, которая обрабатывает событие onopen и отправляет сообщение:
Предположительно, веб-сервер получит это сообщение и даст на него ответ.
События onError и onClose можно использовать для отправки извещений посетителю веб-страницы. Но безоговорочно самым важным является событие onMessage, которое срабатывает при получении новых данных от сервера. Опять же, код JavaScript для обработки этого события не представляет никаких сложностей — мы просто извлекаем текст сообщения из свойства data:
Если веб-страница решит, что вся ее работа выполнена, она может закрыть подключение, используя метод disconnect():
Из этого обзора веб-сокетов можно видеть, что использование сервера веб-сокетов стороннего разработчика не представляет никаких трудностей — нам нужно лишь знать, какие сообщения отправлять, а какие — ожидать.
Чтобы заставить подключение веб-сокетов работать, выполняется большой объем работы за кулисами. Прежде всего, веб-страница устанавливает связь по обычному стандарту HTTP. Потом это подключение нужно повысить до подключения веб-сокетов, позволяющего свободную двустороннюю связь. На этом этапе возможны проблемы, если между компьютером клиента и веб-сервером находится прокси-сервер (как, например, в типичной корпоративной сети). Прокси-сервер может отказаться сотрудничать и разорвет подключение. Эту проблему можно решить, обнаруживая неудачное подключение (посредством события onError объекта WebSocket) и применяя один из заполнителей (polyfills) для сокетов, описанных на веб-сайте GitHub. Эти заполнители применяют метод опроса, чтобы эмулировать подключение веб-сокетов.
High-level API¶
Server
The module defines a simple WebSocket server API.
- (ws_handler, host=None, port=None, *, klass=WebSocketServerProtocol, origins=None, **kwds)
-
This coroutine creates a WebSocket server.
It’s a thin wrapper around the event loop’s create_server method.
host, port as well as extra keyword arguments are passed to
create_server.ws_handler is the WebSocket handler. It must be a coroutine accepting
two arguments: a and
the request URI. If provided, origin is a list of acceptable Origin HTTP
headers. Include if the lack of an origin is acceptable.It returns a Server object with a close method to stop the server.
Whenever a client connects, the server accepts the connection, creates a
, performs the opening
handshake, and delegates to the WebSocket handler. Once the handler
completes, the server performs the closing handshake and closes the
connection.Since there’s no useful way to propagate exceptions triggered in handlers,
they’re sent to the websockets.server logger instead. Debugging is much
easier if you configure logging to print them:import logging logger = logging.getLogger('websockets.server') logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler())
- class (ws_handler, *, origins=None, host=None, port=None, secure=None, timeout=10, max_size=2 ** 20, loop=None)
-
Complete WebSocket server implementation as an asyncio protocol.
This class inherits most of its methods from
.For the sake of simplicity, this protocol doesn’t inherit a proper HTTP
implementation. Its support for HTTP responses is very limited.- (origins=None)
-
Perform the server side of the opening handshake.
If provided, is a list of acceptable HTTP Origin values.
Include if the lack of an origin is acceptable.Return the URI of the request.
Client
The module defines a simple WebSocket client API.
- (uri, *, klass=WebSocketClientProtocol, origin=None, **kwds)
-
This coroutine connects to a WebSocket server.
It accepts an keyword argument to set the Origin HTTP header.
It’s a thin wrapper around the event loop’s create_connection method.
Extra keyword arguments are passed to create_server.It returns a which can
then be used to send and receive messages.It raises if uri is invalid and
if the handshake fails.Clients shouldn’t close the WebSocket connection. Instead, they should
wait until the server performs the closing handshake by yielding from the
protocol’s attribute.implements the sequence called “Establish a WebSocket
Connection” in RFC 6455, except for the requirement that “there MUST be no
more than one connection in a CONNECTING state.”
- class (*, host=None, port=None, secure=None, timeout=10, max_size=2 ** 20, loop=None)
-
Complete WebSocket client implementation as an asyncio protocol.
This class inherits most of its methods from
.- (wsuri, origin=None)
-
Perform the client side of the opening handshake.
If provided, sets the HTTP Origin header.
A simple example
To open a websocket connection, we need to create using the special protocol in the url:
There’s also encrypted protocol. It’s like HTTPS for websockets.
Always prefer
The protocol is not only encrypted, but also more reliable.
That’s because data is not encrypted, visible for any intermediary. Old proxy servers do not know about WebSocket, they may see “strange” headers and abort the connection.
On the other hand, is WebSocket over TLS, (same as HTTPS is HTTP over TLS), the transport security layer encrypts the data at sender and decrypts at the receiver. So data packets are passed encrypted through proxies. They can’t see what’s inside and let them through.
Once the socket is created, we should listen to events on it. There are totally 4 events:
- – connection established,
- – data received,
- – websocket error,
- – connection closed.
…And if we’d like to send something, then will do that.
Here’s an example:
For demo purposes, there’s a small server server.js written in Node.js, for the example above, running. It responds with “Hello from server, John”, then waits 5 seconds and closes the connection.
So you’ll see events → → .
That’s actually it, we can talk WebSocket already. Quite simple, isn’t it?
Now let’s talk more in-depth.
Basic example¶
Here’s a WebSocket server example.
It reads a name from the client, sends a greeting, and closes the connection.
#!/usr/bin/env python # WS server example import asyncio import websockets async def hello(websocket, path): name = await websocket.recv() print(f"< {name}") greeting = f"Hello {name}!" await websocket.send(greeting) print(f"> {greeting}") start_server = websockets.serve(hello, "localhost", 8765) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
On the server side, executes the handler coroutine
once for each WebSocket connection. It closes the connection when the handler
coroutine returns.
Here’s a corresponding WebSocket client example.
#!/usr/bin/env python # WS client example import asyncio import websockets async def hello(): uri = "ws://localhost:8765" async with websockets.connect(uri) as websocket name = input("What's your name? ") await websocket.send(name) print(f"> {name}") greeting = await websocket.recv() print(f"< {greeting}") asyncio.get_event_loop().run_until_complete(hello())
FAQ
How to get the IP address of the client?
The remote IP address can be obtained from the raw socket.
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws, req) { const ip = req.socket.remoteAddress; });
When the server runs behind a proxy like NGINX, the de-facto standard is to use
the header.
wss.on('connection', function connection(ws, req) { const ip = req.headers'x-forwarded-for'.split(\s*,\s*); });
How to detect and close broken connections?
Sometimes the link between the server and the client can be interrupted in a way
that keeps both the server and the client unaware of the broken state of the
connection (e.g. when pulling the cord).
In these cases ping messages can be used as a means to verify that the remote
endpoint is still responsive.
const WebSocket = require('ws'); function noop() {} function heartbeat() { this.isAlive = true; } const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { ws.isAlive = true; ws.on('pong', heartbeat); }); const interval = setInterval(function ping() { wss.clients.forEach(function each(ws) { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; ws.ping(noop); }); }, 30000); wss.on('close', function close() { clearInterval(interval); });
Pong messages are automatically sent in response to ping messages as required by
the spec.
Just like the server example above your clients might as well lose connection
without knowing it. You might want to add a ping listener on your clients to
prevent that. A simple implementation would be:
const WebSocket = require('ws'); function heartbeat() { clearTimeout(this.pingTimeout); // Use `WebSocket#terminate()`, which immediately destroys the connection, // instead of `WebSocket#close()`, which waits for the close timer. // Delay should be equal to the interval at which your server // sends out pings plus a conservative assumption of the latency. this.pingTimeout = setTimeout(() => { this.terminate(); }, 30000 + 1000); } const client = new WebSocket('wss://echo.websocket.org/'); client.on('open', heartbeat); client.on('ping', heartbeat); client.on('close', function clear() { clearTimeout(this.pingTimeout); });
Специальные требования к серверу
В нашем случае лучше всего использовать сервер на основе цикла событий. Например, NodeJS, Kestrel или Twisted. Идея состоит в том, что при использовании потокового решения будет один поток на соединение. То есть, 1000 соединений = 1000 потоков. В решении на основе цикла событий у нас будет один поток для 1000 соединений.
- Вы можете принимать запросы EventSource только в том случае, если HTTP-запрос говорит, что он может принимать MIME-тип event-stream;
- Необходимо вести список всех подключенных пользователей, чтобы запускать новые события;
- Вы должны прослушивать сброшенные соединения и удалять их из списка подключенных пользователей;
- Вы должны поддерживать историю сообщений, чтобы при повторном подключении клиентов можно было отправить им пропущенные сообщения.
Мы получили все, чтобы приложение работало эффективно. Но столкнулись с некоторыми проблемами:
- Устаревшие прокси-серверы в некоторых случаях удаляют HTTP-соединения после короткого таймаута. Чтобы защитить соединения, авторы могут включать строку комментариев (начинающуюся с символа «:») каждые 15 секунд или около того.
- Авторы, желающие связать соединения источника событий друг с другом или с определенными ранее документами, могут обнаружить, что использование IP-адресов не работает. Отдельные клиенты могут иметь несколько IP-адресов (из-за наличия нескольких прокси-серверов) и отдельные IP-адреса могут иметь несколько клиентов (из-за совместного использования прокси-сервера). Лучше включать в документ уникальный идентификатор и передавать его как часть URL-адреса при установлении соединения.
- Использование chunked transfer encoding может уменьшить надежность HTTP протокола, если блокирование выполняется другим слоем, не подозревающим о требованиях к синхронизации. Если эта проблема возникнет, блокирование может быть отключено для обслуживания потоков событий.
- Клиенты, которые поддерживают ограничение на подключение к серверу через протокол HTTP, могут столкнуться с трудностями при открытии нескольких страниц сайта, если на каждой из этих страниц есть источник событий, расположенный в том же домене. Можно избежать этого, применяя механизм уникальных доменных имен для каждого соединения и разрешая пользователям включать функции EventSource для каждой страницы.
- Поддержка браузера и полифиллы: Microsoft Edge не поддерживает эту реализацию. Но существует полифиллы, которые позволяют решить данную проблему. Тем не менее, самый важный сегмент для SSE — это мобильные устройства, где браузеры IE / Edge распространены незначительно.
Некоторые из доступных полифиллов:
· Yaffle.
· amvtek.
· remy.
Бесплатное подключение и другие функции
Пользовательские агенты, работающие в контролируемых средах, могут отключить управление соединением с прокси-сервером в сети. В такой ситуации считается, что пользовательский агент включает как программное обеспечение мобильного устройства, так и сетевой прокси-сервер.
Например, браузер на мобильном устройстве, установив соединение, может обнаружить, что он находится в поддерживаемой сети, и попросить прокси-сервер сети взять на себя управление созданным соединением. Последовательность действий в такой ситуации может быть следующей:
- Браузер подключается к удаленному HTTP-серверу и запрашивает ресурс, указанный автором в конструкторе EventSource.
- Сервер отправляет случайные сообщения.
- В промежутке между двумя сообщениями браузер обнаруживает, что он неактивен, за исключением активности сети, связанной с поддержанием TCP- соединения, и решает переключиться в спящий режим для экономии энергии.
- Браузер отключается от сервера.
- Браузер связывается с сервисом в сети и просит, чтобы служба «push proxy» поддерживала соединение.
- Служба «push proxy» связывается с удаленным HTTP-сервером и запрашивает ресурс, указанный в конструкторе EventSource (возможно, включая HTTP-заголовок последнего события и т. д.).
- Браузер позволяет мобильному устройству перейти в спящий режим.
- Сервер отправляет другое сообщение.
- Служба «push proxy» использует технологию OMA push для передачи события на мобильное устройство, которое выходит из спящего режима на время, достаточное для обработки события. Затем возвращается в спящий режим.
Подобный подход может снизить объем передаваемых данных и привести к значительной экономии энергии.
Помимо реализации существующего API и формата передаваемых данных ext/event-stream также могут поддерживаться форматы фреймворка событий, определенные другими спецификациями.
Browser-based example¶
Here’s an example of how to run a WebSocket server and connect from a browser.
Run this script in a console:
#!/usr/bin/env python # WS server that sends messages at random intervals import asyncio import datetime import random import websockets async def time(websocket, path): while True now = datetime.datetime.utcnow().isoformat() + "Z" await websocket.send(now) await asyncio.sleep(random.random() * 3) start_server = websockets.serve(time, "127.0.0.1", 5678) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
Then open this HTML file in a browser.
Summary
WebSocket is a modern way to have persistent browser-server connections.
- WebSockets don’t have cross-origin limitations.
- They are well-supported in browsers.
- Can send/receive strings and binary data.
The API is simple.
Methods:
- ,
- .
Events:
- ,
- ,
- ,
- .
WebSocket by itself does not include reconnection, authentication and many other high-level mechanisms. So there are client/server libraries for that, and it’s also possible to implement these capabilities manually.
Sometimes, to integrate WebSocket into existing project, people run WebSocket server in parallel with the main HTTP-server, and they share a single database. Requests to WebSocket use , a subdomain that leads to WebSocket server, while goes to the main HTTP-server.
Surely, other ways of integration are also possible.
データ転送
WebSocket 通信は “フレーム” (データフラグメント、どちら側からでも送信でき、いくつかの種類があります)で構成されます。 :
- “text frames” – 関係者が互いに送信するテキストデータを含んでいます。
- “binary data frames” – 関係者が互いに送信するバイナリデータを含んでいます。
- “ping/pong frames” は接続確認に使用されます。サーバから送信され、ブラウザは自動でそれらに応答します。
- “connection close frame” やその他いくつかのサービスフレームもあります。
ブラウザでは、テキストフレームまたはバイナリフレームのみを直接扱います。
WebSocket メソッドはテキストまたはバイナリデータを送信できます。
呼び出しは、文字列または や などを含むバイナリ形式の が許可されます。設定は必要ありません。任意のフォーマットで送信するだけでOKです。
データを受信したとき、テキストは常に文字列として来ます。また、バイナリデータの場合は 、 形式のいずれかを選択することができます。
これは プロパティで設定されます。デフォルトは なので、バイナリデータは オブジェクトで来ます。
Blob は高レベルのバイナリオブジェクトで、, 等といったタグと直接統合されます。そのため、これは妥当なデフォルト値です。ただし、バリナリ処理の場合に個々のバイトデータにアクセスする必要があれば、 に変更することができます。
2021: WSS Docs РД3
1 марта 2021 года компания WSS-Consulting объявила о выходе релиза системы электронного документооборота WSS Docs РД3. В обновленной версии продукта были оптимизированы системные показатели, а также добавлены возможности, нацеленные на удобство работы с системой.
По информации компании, оптимизирована производительность системы, облегчена база оперативных документов за счет разрезания данных на оперативные и архивные документы, изменена архитектура доступа к объектам системы, исключены ошибки искажения данных при сохранении документов или принятии решений за счет создания единого сохранения карточки.
Обновленный функционал включает следующее:
На странице превью (в карточке и картотеке) появилась возможность выбора номера страницы для быстрого перехода к указанной странице.
Возможность сортировать карточки документов
- При отправке документа на повторное согласование система проверяет поля согласующих и выдает предупреждение, если нет дополнительных согласующих или решения текущих согласующих не были очищены.
- Карточки документов теперь можно сортировать по нескольким столбцам для удобства работы с большим потоком документов. Для этого нужно сделать сортировку по первому столбцу и с зажатой клавишей ctrl отсортировать документы по другим столбцам. Чтобы снять множественную сортировку, требуется отпустить клавишу ctrl и сделать сортировку по любому из столбцов.
Поручения к документу
- При создании поручений из файла добавлена возможность автоматической простановки времени для поручений, в которых указана только дата. Функциональность помогает при планировании задач в сжатые сроки.
- Изменилось окно рассылки документа. В предыдущей версии WSS Docs адресаты рассылки отображались в порядке рассылки документа. сортировка осуществляется в алфавитном порядке по возрастанию для упрощения поиска сотрудников. Также все поля окна рассылки в релизе системы имеют ограничение по высоте. Это позволит пользователям удобно работать в рамках небольшого окна. А в случае выбора большого числа адресатов, групп или адресов Email, появится скролл.
Окно рассылки документа
Добавлено информационное окно при обработке карточки, открытой из оповещения.Ранее при сохранении карточки, открытой из оповещения, отображалась пустая вкладка.
В рамках оптимизации производительности системы была снижена нагрузка на сервера, оптимизирован поиск и отображение документов в папках интерфейсов, включен полнотекстовый поиск в папках интерфейсов, в окнах выбора документов и контекстном поиске в полях, оптимизирована скорость открытия и сохранения карточки документа.
При наличии мобильного приложения WSS Docs доступна функциональность автоматического входа в приложение через систему администрирования AppCenter. В данной системе администраторы подтверждают вход пользователя, блокируют устройства в случае утери сотрудником компании и смотрят историю действий.
В релизе системы AppCenter самостоятельно проверяет в СЭД принадлежность пользователя к компании. Если проверка прошла успешно, то устройство будет автоматически зарегистрировано в AppCenter без подтверждения администратора.
Или getting started with WebSocket PHP без phpDaemon
Здравствуйте! Простите за столь длинный заголовок, но, надеюсь, что новичкам вроде меня будет легче найти эту статью, ведь мне ничего подобного найти не удалось. Несколько недель назад я принял решение переработать игровой клиент и сервер своей игры Growing Crystals с AJAX, на WebSocket, но всё оказалось не просто непросто, а очень сложно. Поэтому я и решил написать статью, которая бы помогла самым что ни на есть начинающим разработчикам на WebSocket + PHP сэкономить несколько дней времени, максимально подробно объясняя каждый свой шаг по настройке и запуску первого WebSocket скрипта на PHP.
Что у меня есть: Денвер на локальной машине, на нём я веду разработку проекта и дешевый PHP хостинг, на котором я публикую свой проект для того, чтобы получить обратную связь от Интернет-пользователей.
Что я хочу: Без установки phpDaemon (phpd), NodeJS и прочих вещей на локальную машину и хостинг, продолжить разработку своего проекта, но теперь с WebSocket, в этой статье разберем простой WebSocket эхо сервер.
Чего я не хочу: Говоря о NodeJS, не хочется переписывать серерную логику с PHP на другой язык, тем более устанавливать NodeJS, хотя и люблю JavaScript больше чем PHP.
Важно: не путайте демона написанного на php, с фреймворком асинхронных приложений phpDaemon. Который, конечно же, обязательно потребуется в случае развития проекта и многократного роста нагрузки на хостинг
Но для начала работы с WebSocket на дешевом хостинге можно обойтись и без него.