Как объединить веб-сокеты и http для создания REST API, который поддерживает актуальность данных?

Я думаю о создании REST API как с веб-сокетами, так и с http, где я использую веб-сокеты, чтобы сообщить клиенту, что новые данные доступны, или напрямую предоставить новые данные клиенту.

Вот несколько вариантов того, как это может работать:
ws = websocket

Идея А:

  1. Дэвид получает всех пользователей с GET /users
  2. Джейкоб добавить пользователя с POST /users
  3. Всем клиентам отправляется сообщение ws с информацией о существовании нового пользователя.
  4. Дэвид получает сообщение от ws и звонит GET /users

Идея Б:

  1. Дэвид получает всех пользователей с GET /users
  2. Дэвид зарегистрируется, чтобы получать обновления ws, когда вносятся изменения в /users
  3. Джейкоб добавить пользователя с POST /users
  4. Новый пользователь отправляется Дэвиду с помощью ws

Идея С:

  1. Дэвид получает всех пользователей с GET /users
  2. Дэвид зарегистрируется, чтобы получать обновления ws, когда вносятся изменения в /users
  3. Джейкоб добавляет пользователя с POST /users и получает идентификатор 4
  4. Дэвид получает идентификатор 4 нового пользователя с помощью ws
  5. Дэвид получает нового пользователя с GET /users/4

Я умер:

  1. Дэвид получает всех пользователей с GET /users
  2. Дэвид регистрируется, чтобы получать обновления ws, когда изменения вносятся в /users.
  3. Джейкоб добавить пользователя с POST /users
  4. Дэвид получает сообщение ws о внесении изменений в /users
  5. Давид получает только дельту по телефону GET /users?lastcall='time of step one'

Какая альтернатива лучше, и каковы ее плюсы и минусы?
Является ли она еще лучшей «Идеей E»?
Нужно ли нам вообще использовать REST или ws достаточно для всех данных?

Изменить
Чтобы решить проблемы с рассинхронизацией данных, мы могли бы предоставить заголовок
"If-Unmodified-Since"
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since
или "E-Tag"
https://developer.mozilla.org/en-US/docs/Web/HTTP/Заголовки/ETag
или оба с запросами PUT.


person David Berg    schedule 29.10.2015    source источник
comment
@Robocide, можешь подробнее объяснить свою награду? Я не уверен, что ты имеешь в виду.   -  person Glen Pierce    schedule 28.05.2017


Ответы (9)


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

Нужно ли вообще использовать REST или ws достаточно для всех данных?

Пожалуйста, проверьте: WebSocket/REST: клиентские подключения?

person vtortola    schedule 29.10.2015
comment
Спасибо за Ваш ответ! Я также думаю, что предпочитаю идею B, но у меня появилась новая мысль о посте. Было бы неплохо также использовать ws для сохранения сущностей? - person David Berg; 30.10.2015
comment
Это то, что я пытаюсь ответить в той ссылке, которую я включил. WS являются двунаправленными, так что технически это возможно. Однако, когда вам нужно масштабировать свое решение, вы захотите отделить запись от чтения, и если вы делаете все через один и тот же веб-сокет, это будет невозможно. - person vtortola; 30.10.2015

Я не знаю Java, но я работал с Ruby и C над этими проектами...

Как ни странно, я думаю, что самым простым решением является использование JSON, где REST API просто добавляет данные method (то есть method: "POST") в JSON и перенаправляет запрос тому же обработчику, который использует Websocket.

Ответ базового API (ответ от API, обрабатывающего запросы JSON) может быть переведен в любой нужный вам формат, например, в HTML-рендеринг... хотя я бы рассмотрел возможность простого возврата JSON для большинства случаев использования.

Это помогает инкапсулировать код и сохранить его СУХИМ при доступе к одному и тому же API с использованием как REST, так и веб-сокетов.

Как вы можете догадаться, такой дизайн упрощает тестирование, поскольку базовый API, обрабатывающий JSON, можно тестировать локально без необходимости эмуляции сервера.

Удачи!

P.S. (Pub/Sub)

Что касается Pub/Sub, я считаю, что лучше всего иметь «ловушку» для любых вызовов API обновления (обратный вызов) и отдельный модуль Pub/Sub, который обрабатывает эти вещи.

Я также считаю, что более экономично записывать все данные в службу Pub/Sub (вариант B), а не только ссылочный номер (вариант C) или сообщение «доступно обновление» (варианты A и D).

В общем, я также считаю, что отправка всего списка пользователей неэффективна для больших систем. Если у вас нет 10-15 пользователей, вызов базы данных может оказаться неудачным. Рассмотрим администратора Amazon, запрашивающего список всех пользователей... Бррр....

Вместо этого я бы рассмотрел возможность разделения на страницы, скажем, 10-50 пользователей на странице. Эти таблицы могут быть заполнены с помощью нескольких запросов (Websocket/REST, не имеет значения) и легко обновлены с помощью живых сообщений Pub/Sub или перезагружены, если соединение было потеряно и восстановлено.

ИЗМЕНИТЬ (REST и веб-сокеты)

Что касается REST и веб-сокетов... Я считаю, что вопрос о необходимости в основном является подмножеством вопроса "кто клиент?"...

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

Я должен отметить, что веб-сокеты часто имеют небольшое преимущество, когда дело доходит до аутентификации (учетные данные обмениваются один раз за соединение, а не один раз за запрос). Не знаю, беспокоит ли это.

По той же причине (как и по другим причинам) веб-сокеты обычно имеют преимущество в отношении производительности... насколько большое преимущество над REST зависит от транспортного уровня REST (HTTP/1.1, HTTP/2 и т. д.).

Обычно этими вещами можно пренебречь, когда приходит время предлагать публичную точку доступа к API, и я считаю, что реализация обоих — это, вероятно, путь на данный момент.

person Myst    schedule 22.05.2017

Другой вариант — использовать Firebase Cloud Messaging:

Используя FCM, вы можете уведомить клиентское приложение о том, что новая электронная почта или другие данные доступны для синхронизации.

Как это работает?

Реализация FCM включает два основных компонента для отправки и получения:

  • Надежная среда, такая как облачные функции для Firebase или сервер приложений, на котором можно создавать, настраивать и отправлять сообщения.
  • Клиентское приложение iOS, Android или веб-приложение (JavaScript), которое получает сообщения.

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

person Justinas Jakavonis    schedule 16.05.2017
comment
Я думаю, что Firebase использует веб-сокеты для своего общения, по крайней мере, для веб-страниц. Другой аналогичный продукт, который я тестировал, — это Deepstream, который является хорошей альтернативой, если вы хотите использовать полное решение для веб-сокетов. (deepstream.io) - person David Berg; 16.05.2017

Как правило, вы можете взглянуть на современные веб-фреймворки «реального времени», такие как MeteorJS, которые решают именно эту проблему.

Meteor в конкретном случае работает более или менее так же, как ваш пример D, с подписками на определенные данные и дельты, отправляемые после изменений только затронутым клиентам. Используемый ими протокол называется DDP, который дополнительно отправляет дельты не в виде HTML, подверженного накладным расходам, а в виде необработанных данных.

Если веб-сокеты недоступны, запасные варианты, такие как длинный опрос или события, отправленные сервером можно использовать.

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

person Maximilian Körner    schedule 16.05.2017

Ответ зависит от вашего варианта использования. По большей части, хотя я обнаружил, что вы можете реализовать все, что вам нужно, с помощью сокетов. Пока вы пытаетесь получить доступ к своему серверу только с клиентами, которые могут поддерживать сокеты. Кроме того, масштаб может быть проблемой, когда вы используете только сокеты. Вот несколько примеров того, как вы можете использовать только сокеты.

Сторона сервера:

socket.on('getUsers', () => {
    // Get users from db or data model (save as user_list).
    socket.emit('users', user_list );
})
socket.on('createUser', (user_info) => {
    // Create user in db or data model (save created user as user_data).
    io.sockets.emit('newUser', user_data);
})

Сторона клиента:

socket.on('newUser', () => {
    // Get users from db or data model (save as user_list).
    socket.emit('getUsers');
})
socket.on('users', (users) => {       
    // Do something with users
})

Это использует socket.io для node. Я не уверен, каков ваш точный сценарий, но это сработает для этого случая. Если вам нужно включить конечные точки REST, это тоже будет хорошо.

person Brian Baker    schedule 16.05.2017

Со всей отличной информацией, которую все великие люди добавили до меня.

Я обнаружил, что в конечном итоге нет правильного или неправильного, все просто сводится к тому, что соответствует вашим потребностям:

давайте возьмем CRUD в этом сценарии:

Подход только WS:

Create/Read/Update/Deleted information goes all through the websocket.    
--> e.g If you have critical performance considerations ,that is not 
acceptable that the web client will do successive REST request to fetch 
information,or if you know that you want the whole data to be seen in 
the client no matter what was the event ,  so just send the CRUD events 
AND DATA inside the websocket.

WS ДЛЯ ОТПРАВКИ ИНФОРМАЦИИ О СОБЫТИИ + REST ДЛЯ ПОТРЕБЛЕНИЯ САМИХ ДАННЫХ

Create/Read/Update/Deleted , Event information is sent in the Websocket,
giving the web client information that is necessary to send the proper 
REST request to fetch exactly the thing the CRUD that happend in server.

например WS отправляет UsersListChangedEvent {"ListChangedTrigger: "ItemModified", "IdOfItem":"XXXX#3232", "UserExtrainformation":" Достаточно информации, чтобы позволить клиенту решить, имеет ли он значение для получения измененных данных"}

Я обнаружил, что использование WS [только для использования данных событий] и REST [для использования данных] лучше, потому что:

[1] Разделение между моделями чтения и записи. Представьте, что вы хотите добавить некоторую информацию о времени выполнения, когда ваши данные извлекаются при их чтении из REST, что теперь достигается, потому что вы не смешиваете модели записи и чтения, как в 1.

[2] Допустим, другая платформа, не обязательно веб-клиент, будет использовать эти данные. поэтому вы просто меняете триггер события с WS на новый способ и используете REST для потребления данных.

[3] Клиенту не нужно писать 2 способа чтения новых/измененных данных. обычно также есть код, который считывает данные при загрузке страницы, а не
через веб-сокет, этот код теперь можно использовать дважды, один раз при загрузке страницы и второй раз, когда WS инициировал определенное событие.

[4] Возможно, клиент не хочет получать нового пользователя, потому что в настоящее время он показывает только представление старых данных [например, users] , а новые изменения данных не в его интересах извлекать ?

person Robocide    schedule 14.06.2017

Я лично использовал Idea B в производстве и очень доволен результатами. Мы используем http://www.axonframework.org/, поэтому каждое изменение или создание объекта публикуется. как событие во всем приложении. Затем эти события используются для обновления нескольких моделей чтения, которые в основном представляют собой простые таблицы Mysql, поддерживающие один или несколько запросов. Я добавил несколько перехватчиков в обработчики событий, которые обновляют эти модели чтения, чтобы они публиковали события, которые они только что обработали, после фиксации данных в БД.

Публикация событий осуществляется через STOMP через веб-сокеты. Это делается очень просто, если вы используете поддержку Spring Web Socket (https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html). Вот как я это написал:

@Override
protected void dispatch(Object serializedEvent, String topic, Class eventClass) {
    Map<String, Object> headers = new HashMap<>();
    headers.put("eventType", eventClass.getName());
    messagingTemplate.convertAndSend("/topic" + topic, serializedEvent, headers);
}

Я написал небольшой конфигуратор, который использует API фабрики компонентов Springs, чтобы я мог аннотировать свои обработчики событий Axon следующим образом:

@PublishToTopics({
    @PublishToTopic(value = "/salary-table/{agreementId}/{salaryTableId}", eventClass = SalaryTableChanged.class),
    @PublishToTopic(
            value = "/salary-table-replacement/{agreementId}/{activatedTable}/{deactivatedTable}",
            eventClass = ActiveSalaryTableReplaced.class
    )
})

Конечно, это только один из способов сделать это. Подключение на стороне клиента может выглядеть примерно так:

var connectedClient = $.Deferred();

function initialize() {
    var basePath = ApplicationContext.cataDirectBaseUrl().replace(/^https/, 'wss');
    var accessToken = ApplicationContext.accessToken();
    var socket = new WebSocket(basePath + '/wss/query-events?access_token=' + accessToken);
    var stompClient = Stomp.over(socket);

    stompClient.connect({}, function () {
        connectedClient.resolve(stompClient);
    });
}


this.subscribe = function (topic, callBack) {
    connectedClient.then(function (stompClient) {
        stompClient.subscribe('/topic' + topic, function (frame) {
            callBack(frame.headers.eventType, JSON.parse(frame.body));
        });
    });
};

initialize();
person CrimsonCricket    schedule 28.05.2017

я предпочитаю A, он позволяет клиенту гибко обновлять существующие данные или нет.

также с этим методом реализация и контроль доступа становятся намного проще.

например, вы можете просто транслировать событие userUpdated всем пользователям, это экономит список клиентов для выполнения конкретных трансляций, а Access Controls and Authentications, применяемый для вашего REST-маршрута, не нужно менять на повторное применение, потому что клиент снова сделает запрос GET.

Многое зависит от того, какое приложение вы делаете.

person Mujtaba Kably    schedule 16.05.2017
comment
Что делает управление доступом альтернативы A проще, чем C или D, где GET также используется для выборки данных? - person David Berg; 16.05.2017

person    schedule
comment
Спасибо за ваш ответ. Я думаю, что ваше предложение для варианта E — это то, что я подумал с вариантом B. Мне жаль, если пример был недостаточно хорош. Также о вашей обеспокоенности тем, что данные будут не синхронизированы, это можно решить, предоставив заголовок If-Not-Modified-Since с почтовым вызовом. developer.mozilla.org/en-US/ документы/Интернет/HTTP/Заголовки/ - person David Berg; 15.05.2017
comment
Как убедиться, что время вашего сервера и время вашего пользователя совпадают? Если вы когда-нибудь начнете использовать микросервисы, как вы будете уверены, что время ваших серверов синхронизировано? Устранение ошибок в распределенных системах с несинхронизированными временными метками журналов — это не весело. - person Glen Pierce; 15.05.2017
comment
Вы можете сохранить дату последнего редактирования в ресурсах на сервере, которые вы предоставляете клиенту в запросах на получение, затем вы используете эту дату при отправке заголовка If-Unmodified-Since. - person David Berg; 15.05.2017
comment
Это, однако, не гарантирует синхронизацию времени серверов, но я думаю, что у вас должны быть инструменты в распределенной системе, чтобы убедиться в этом. - person David Berg; 15.05.2017
comment
Я прочитал вашу правку. Если в худшем сценарии много пользователей с медленным соединением, не лучше ли было бы отправлять как можно меньше и отправлять только дельту и, возможно, контрольную сумму для всего объекта. Таким образом, клиент может увидеть, не синхронизированы ли данные, и запросить полный набор только в том случае, если это произойдет. Чем больше я думаю об этом, тем больше я склоняюсь к тому, чтобы предпочесть какую-то версию альтернативы D или даже пойти на полную реализацию ws. - person David Berg; 15.05.2017