Что такое подписка GraphQL?


Подписки GraphQL позволяют клиенту подписываться на изменения. Вместо того, чтобы запрашивать изменения, клиент может получать обновления в режиме реального времени. Вот простой пример из нашей [демонстрации GraphQL Federation Demo] (https://github.com/wundergraph/wundergraph-demo):


```график


подписка {


updatedPrice: federated_updatedPrice {


УПЦ


название


цена


отзывы {


я бы


тело


автор {


я бы


название


Это основано на Федерации Аполлона. Как только микросервис «продукт» получает обновление цены, WunderGraph объединяет данные с отзывами из микросервиса «обзор», выполняет дополнительное объединение с некоторой пользовательской информацией из микросервиса «пользователь» и отправляет данные обратно клиенту. Клиент получает это как поток данных. Таким образом, пользовательский интерфейс может обновляться в режиме реального времени.


Традиционные способы реализации подписок GraphQL


Наиболее широко распространенный способ реализации подписок GraphQL — использование WebSockets.


WebSocket API — это стандарт HTTP 1.1, который обычно поддерживается всеми современными браузерами. (Согласно caniuse.com, 94,22% всех браузеров поддерживают WebSockets API)


Сначала клиент отправляет HTTP-запрос на обновление, прося сервер обновить соединение до WebSocket. Как только сервер обновляет соединение, и клиент, и сервер могут отправлять и получать данные, передавая сообщения через WebSocket. Давайте теперь обсудим проблемы с WebSockets


API WebSocket является стандартом HTTP 1.1.


В настоящее время большинство веб-сайтов используют HTTP/2 или даже HTTP/3 для ускорения работы в Интернете.


HTTP/2 позволяет мультиплексировать несколько запросов по одному TCP-соединению.


Это означает, что клиент может отправлять несколько запросов одновременно.


HTTP/3 улучшает это еще больше, но не в этом суть этого поста.


Проблема заключается в том, что если ваш веб-сайт смешивает HTTP/1.1 и HTTP/2, клиенту придется открывать несколько TCP-соединений с сервером. Клиенты могут легко мультиплексировать до 100 запросов HTTP/2 по одному TCP-соединению, тогда как с WebSockets вы вынуждены открывать новое TCP-соединение для каждого WebSocket. Если пользователь открывает несколько вкладок на вашем веб-сайте, каждая вкладка открывает новое TCP-соединение с сервером. Используя HTTP/2, несколько вкладок могут использовать одно и то же соединение TCP. Итак, первая проблема с WebSockets заключается в том, что они используют устаревший и неподдерживаемый протокол, который вызывает дополнительные TCP-соединения.


WebSockets сохраняют состояние


Другая проблема с WebSockets заключается в том, что клиент и сервер должны отслеживать состояние соединения. Если мы посмотрим на принципы REST, один из них гласит, что [запросы должны быть без состояния] (https://restfulapi.net/stateless/). Без сохранения состояния в этом контексте означает, что каждый запрос должен содержать всю необходимую информацию, чтобы его можно было обработать. Давайте рассмотрим несколько сценариев использования подписок GraphQL с WebSockets:


1. Отправьте заголовок авторизации вместе с запросом на обновление


Как мы узнали выше, каждое соединение WebSocket начинается с HTTP-запроса на обновление. Что, если мы отправим заголовок авторизации вместе с запросом на обновление? Это возможно, но это также означает, что когда мы «подписываемся» с помощью сообщения WebSocket, эта «подписка» больше не является не имеющей состояния, поскольку она зависит от заголовка авторизации, который мы ранее отправили. Что, если пользователь тем временем вышел из системы, но мы забыли закрыть соединение WebSocket?


Другая проблема с этим подходом заключается в том, что API браузера WebSocket не позволяет нам устанавливать заголовки в запросе на обновление. Это возможно только при использовании пользовательских клиентов WebSocket.


Так что на самом деле такой способ реализации подписок GraphQL не очень практичен.


2. Отправьте токен аутентификации с сообщением WebSocket «connection_init»


Другой подход заключается в отправке токена аутентификации с сообщением WebSocket «connection_init». Так это делает Reddit. Если вы перейдете на reddit.com, откройте Chrome DevTools, щелкните вкладку сети и отфильтруйте по «ws». Вы увидите соединение WebSocket, когда клиент отправляет токен Bearer с сообщением «connection_init». Этот подход также является состоянием. Вы можете скопировать этот токен и использовать любой другой клиент WebSocket для подписки на подписку GraphQL. Затем вы можете выйти из системы на веб-сайте без закрытия соединения WebSocket. Последующие сообщения подписки также будут зависеть от контекста, который был установлен первоначальным сообщением «connection_init», просто чтобы подчеркнуть тот факт, что оно все еще сохраняет состояние. Тем не менее, есть гораздо большая проблема с этим подходом.


Как вы видели, клиент отправил токен Bearer с сообщением «connection_init». Это означает, что в какой-то момент времени у клиента был доступ к указанному токену. Таким образом, JavaScript, работающий в браузере, имеет доступ к токену. В прошлом у нас было множество проблем, когда широко используемые пакеты npm были заражены вредоносным кодом. Предоставление части JavaScript вашего веб-приложения доступа к токену Bearer может привести к проблеме безопасности. Лучшее решение — всегда хранить такие токены в безопасном месте, мы вернемся к этому позже.


3. Отправьте токен аутентификации с сообщением WebSocket «подписаться»


Другим подходом может быть отправка токена аутентификации с сообщением WebSocket «подписаться». Это снова сделает нашу подписку GraphQL без состояния, поскольку вся информация для обработки запроса содержится в сообщении «подписаться». Однако такой подход создает кучу других проблем.


Во-первых, это будет означать, что мы должны позволить клиентам анонимно открывать соединения WebSocket, не проверяя, кто они. Поскольку мы хотим, чтобы наша подписка GraphQL не сохраняла состояние, первый раз, когда мы отправляем токен авторизации, мы отправляем сообщение «подписаться».


Что произойдет, если миллионы клиентов откроют соединения WebSocket с вашим сервером GraphQL, даже не отправив сообщение «подписаться»?


Обновление соединений WebSocket может быть довольно дорогим, и вам также необходимо иметь ЦП и память, чтобы поддерживать соединения. Когда вы должны отключить «вредоносное» соединение WebSocket? Что делать, если у вас есть ложные срабатывания? Другая проблема с этим подходом заключается в том, что вы более или менее заново изобретаете HTTP через WebSockets. Если вы отправляете «Метаданные авторизации» с сообщением «подписаться», вы, по сути, повторно реализуете заголовки HTTP. Почему бы просто не использовать вместо этого HTTP? Мы обсудим лучший подход (SSE/Fetch) позже.


WebSockets обеспечивают двустороннюю связь


Следующая проблема с WebSockets заключается в том, что они обеспечивают двустороннюю связь. Клиенты могут отправлять произвольные сообщения на сервер. Если мы вернемся к спецификации GraphQL, то увидим, что для реализации подписок не требуется двунаправленная связь. Клиенты подписываются один раз.


После этого только сервер отправляет сообщения клиенту. Если вы используете протокол (WebSockets), который позволяет клиентам отправлять произвольные сообщения на сервер, вам нужно каким-то образом ограничить количество сообщений, которые может отправить клиент. Что делать, если вредоносный клиент отправляет на сервер много сообщений? Сервер обычно тратит процессорное время и память на анализ и отклонение сообщений. Не лучше ли использовать протокол, запрещающий клиентам отправлять произвольные сообщения на сервер?


WebSockets не идеальны для SSR (рендеринга на стороне сервера)


Еще одна проблема, с которой мы столкнулись, — это удобство использования WebSockets при выполнении SSR (рендеринга на стороне сервера).


Одна из проблем, которую мы недавно решили, — разрешить «Универсальный рендеринг» (SSR) с подписками GraphQL. Мы искали удобный способ отображать подписку GraphQL как на сервере, так и в браузере. Почему вы хотите это сделать? Представьте, вы создаете веб-сайт, который всегда должен показывать последнюю цену акции или товара. Вы определенно хотите, чтобы веб-сайт работал (почти) в режиме реального времени, но вы также хотите отображать контент на сервере из соображений SEO и удобства использования.


Вот пример из нашей [демонстрации GraphQL Federation] (https://github.com/wundergraph/wundergraph-demo):


```tsx


const UniversalSubscriptions = () => {


const priceUpdate = useSubscription.PriceUpdates();


возврат (


<дел>


Обновление цен


<ул>


{priceUpdate.map(цена => (


<ключ li={price.id}>


{цена.продукт} - {цена.цена}