Как использовать Websocket на примере простого Express API?
Websocket — это протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени.
Для установления соединения WebSocket клиент и сервер используют протокол, похожий на HTTP. Клиент формирует особый HTTP-запрос, на который сервер отвечает определенным образом.
Несмотря на «похожесть» новых запросов и ответов на запросы и ответы протокола HTTP, они таковыми не являются. Например, в запросе есть тело, но в заголовках поле «Content-Length» отсутствует (что нарушает соглашения HTTP). Подробнее об этом можно прочитать в Википедии.
Одним из главных преимуществ технологии — это ее простота. На клиенте и сервере есть всего 4 события для обработки:
Почему Websocket?
Кроме ws существуют еще два способа непрерывной передачи данных: Server-Sent Events (SSE) и Long Polling.
Приведем сравнения механизмов непрерывной связи сервера и клиента, а также сделаем выводы, почему стоит (или не стоит) использовать вебсокет.
| Websocket | sse | long pooling | |
|---|---|---|---|
| протокол | websocket (ws, или wss) | HTTP(S) | HTTP(S) |
| скорость | высокая | низкая | низкая |
| направленность потоков данных | двунаправленная | однонаправленная | двунаправленная |
| дополнительно | передача бинарных данных, отсутствует поддержка некоторых старых браузеров |
автоматическое переподключение при обрыве соединения |
Одним из главных преимуществ технологии ws — это скорость передачи данных. SSE и LP используют протокол HTTP(S) и работают примерно так:
- Делаем запрос на изменения;
- Если изменения на сервере появились, то сервер их отправляет;
- Когда клиент получает изменения, клиент делает новый запрос.
- Вебсокет не требует от клиента постоянно запрашивать изменения и именно поэтому он быстрее.
- Вебсокет позволяет передавать бинарные данные, что не позволяет протокол HTTP(S).
- Вебсокет не нужно использовать, если проект требует совместимость со старыми версиями браузеров. Читать о совместимости с браузерами
Пример работы простейшего api.
const http = require("http"); const express = require( "express"); const WebSocket = require( "ws"); const app = express(); const server = http.createServer(app); const webSocketServer = new WebSocket.Server(< server >); webSocketServer.on('connection', ws => < ws.on('message', m => < webSocketServer.clients.forEach(client =>client.send(m)); >); ws.on("error", e => ws.send(e)); ws.send('Hi there, I am a WebSocket server'); >); server.listen(8999, () => console.log("Server started"))
Что здесь происходит?
Чтобы создать сервер поддерживающий ws, мы создаем обычный http сервер, а потом привязываем к нему при создании websocket сервер.
Функция “on” помогает управлять событиями websocket. Самым примечательным является событие message, так что рассмотрим его подробнее.
Здесь функция получает параметр m — сообщение, то есть то, что отправил пользователь. Таким образом мы можем отправить с клиента строку и обработать ее на сервере. В данном случае сервер просто пересылает это сообщение всем, кто подключен к серверу websocket. Массив clients объекта webSocketServer содержит все подключения к серверу. Объект ws в то же время хранит данные только об одном подключении.
Не стоит использовать такой подход в реальном приложении. Если описать api таким образом, то сервер не может отличить один запрос от другого. О том, как можно построить api на основе websocket будет написано далее.
Взаимодействие с сервером на клиенте будет выглядеть так:
export const wsConnection = new WebSocket("ws://localhost:8999"); wsConnection.onopen = function() < alert("Соединение установлено."); >; wsConnection.onclose = function(event) < if (event.wasClean) < alert('Соединение закрыто чисто'); >else < alert('Обрыв соединения'); // например, "убит" процесс сервера >alert('Код: ' + event.code + ' причина: ' + event.reason); >; wsConnection.onerror = function(error) < alert("Ошибка " + error.message); >; export const wsSend = function(data) < // readyState - true, если есть подключение if(!wsConnection.readyState)< setTimeout(function ()< wsSend(data); >,100); > else < wsConnection.send(data); >>;
API на основе Websocket
В отличие от REST API, где запросы распределены по разным url, Websocket API имеет только один url. Для того, чтобы построить полноценное API на основе вебсокетов, необходимо научить систему отличать один запрос от другого. Это можно реализовать следующим образом:
1) С клиента мы будем передавать запросы в виде строки-json, которую распарсим на сервере:
const sendMessage = (message) => conn.send(JSON.stringify(< event: "chat-message", payload: < userName, message >>));
2) На сервере мы распарсим строку и выделем в ней поле event — тип запроса. Пропишем для каждого типа соответствующий ответ:
const dispatchEvent = (message, ws) => < const json = JSON.parse(message); switch (json.event) < case "chat-message": webSocketServer.clients.forEach(client =>client.send(message)); default: ws.send((new Error("Wrong query")).message); > >
Таким образом мы можем отправлять разные запросы на сервер и обрабатывать ответ в зависимости от запроса.
Заключение
Если вам была дана задача сделать API и вы узнали, что поддержка старых браузеров заказчика не интересует, то API на основе WebSocket — отличный выбор. Для вашего удобства мы подготовили код клиентской и серверной части по ссылке.
WS-адресация
WS-Адресация (Web Services Addressing — адресация WEB служб) — это спецификация транспортно нейтрального механизма, позволяющего WEB службам обмениваться информацией об адресах. По существу, она состоит из двух частей: структуры, содержащей конечные точки служб и набора свойств сообщения, ассоциирующих информацию об адресе с конкретным сообщением.
Описание
WS-Адресация это стандартный способ включения информации о маршрутизации в SOAP заголовки. Вместо того, чтобы возвращать информацию о маршрутизации по сетевому уровню, сообщение, использующее WS-Адресацию, может содержать собственные метаданные в стандартном SOAP заголовке. В данном случае сетевой уровень отвечает только за доставку сообщения диспетчеру, способному читать метаданные такого сообщения. Когда это сообщение приходит к диспетчеру, тот определяет URI, работа сетевого уровня на этом заканчивается.
WS-Адресация поддерживает использование асинхронных операций. Для этого необходимо указать SOAP заголовок (wsa:ReplyTo), содержащий ссылку на конечную точку — endpoint reference (EPR), куда должен быть послан ответ. Провайдер службы отправляет ответное сообщение через отдельное соединение по адресу, который указан в заголовке wsa:ReplyTo. Это позволяет разделить время жизни SOAP запроса/ответа от времени жизни HTTP запроса/ответа, что позволяет организовывать длительно-текущие взаимодействия, длительность которых оказывается каждый раз разной.
Адрес конечной точки (Endpoint References)
Конечная точка это XML структура инкапсулирующая информацию полезную для адресации сообщения предназначенного для Web службы. Она включает в себя адрес назначения сообщения и любые дополнительные параметры, необходимые для маршрутизации сообщения к точке назначения, Конечная точка также включает в себя необязательные метаданные(такие как WSDL илиWS-Policy) о службе.
Свойства сообщения (Message Addressing Properties)
Свойства сообщения содержат адресную информацию, относящуюся к доставке сообщения на Web сервис
- Пункт назначения сообщения (Message destination) URI
- Конечная точка ресурса (Source endpoint) — конечная точка службы, которая отправляет сообщение
- Reply endpoint — конечная точка на которую будет отправлен ответ
- Fault endpoint — конечная точка на которую будет отправлено сообщение об ошибке
- Действие (Action) — параметр, отражающий смысл сообщения
- ID сообщения URI
- Взаимосвязь с предыдущим сообщением (A pair of URIs)
История
Стандарт WS-Адресации была создана при участии следующих компаний: Microsoft, IBM, BEA, Sun в W3C submitted.
Спецификация WS-Адресации в настоящее время разделена на три части:
- Core — спецификация структуры конечных точек и свойств сообщения.
- binding свойства SOAP.
- Metadata — Метаданные.
Ссылки
- Web Services Addressing Working Group
- WS-Addressing — specification (IBM)
- WS-Addressing — Submission Request to W3C
- Team Comment on the WS-Addressing Submission
- Интернет-сервисы
- Веб-стандарты
- Стандарты на основе XML
Wikimedia Foundation . 2010 .
Сообщение пробы
Сообщение пробы — это WS-Discovery сообщение, используемое клиентом для поиска служб в сети по типу службы. Дополнительные сведения о сообщениях пробы см. в разделе 5.2 спецификации WS-Discovery.
Сообщение пробы отправляется многоадресной рассылкой UDP на порт 3702. Сообщения одноадресной пробы не поддерживаются.
Клиенты DPWS отправляют сообщения пробы. В следующем списке показаны сценарии, в которых WSDAPI отправляет сообщение пробы.
- Клиенты обнаружения функций отправляют сообщения пробы.
- Клиенты WSDAPI, вызывающие IWSDiscoveryProvider::SearchByAddress , отправляют сообщения пробы.
- Клиенты WSDAPI, вызывающие IWSDiscoveryProvider::SearchByType, отправляют сообщения пробы.
- Приложения, использующие направленное обнаружение, отправляют сообщения пробы по протоколу HTTP или HTTPS.
В этом разделе показан пример сообщения DPWS, созданного клиентами и узлами WSDAPI. WSDAPI будет анализировать и принимать другие сообщения, совместимые с DPWS, которые не соответствуют этому примеру. Не используйте этот пример для проверки взаимодействия DPWS; вместо этого используйте WSDAPI Basic Interoperability Tool (WSDBIT).
В следующем сообщении SOAP показан пример сообщения пробы.
urn:schemas-xmlsoap-org:ws:2005:04:discovery https://schemas.xmlsoap.org/ws/2005/04/discovery/Probe urn:uuid:29cf10da-5c41-4d55-b184-5ee15e38ce23 wsdp:Device
Сообщение пробы имеет следующие точки фокуса.
https://schemas.xmlsoap.org/ws/2005/04/discovery/Probe
urn:uuid:29cf10da-5c41-4d55-b184-5ee15e38ce23
wsdp:Device
Что такое веб-сокеты и как они вообще работают

Коммуникация в режиме реального времени и практически мгновенная передача данных являются обязательными стандартами современного интернета. Чтобы удовлетворить эти стандарты, в 2011 году появился протокол связи WebSocket, который позволяет сайтам отправлять и получать данные без задержки. С помощью веб-сокетов можно создавать многопользовательские игры, мессенджеры, а также сервисы для совместной работы.
Что такое веб-сокеты
WebSocket — это технология, которая позволяет клиенту установить двухстороннюю ( «дуплексную» ) связь с сервером. Сразу поясним: клиент — это приложение на компьютере или смартфоне пользователя, а сервер — это удаленный компьютер, на котором хранится веб-сайт и связанные с ним данные.
Ключевое слово в этом определении — двусторонний: с помощью веб-сокетов клиент и сервер могут инициировать связь друг с другом, а также могут отправлять сообщения одновременно. Почему это так важно? Чтобы в полной мере оценить возможности WebSocket, сделаем шаг назад и рассмотрим несколько самых распространенных способов, с помощью которых компьютеры могут получать данные с сервера.
Node.js-разработчик — с нуля до трудоустройства за 10 месяцев
- Постоянная поддержка от наставника и учебного центра
- Помощь с трудоустройством
- Готовое портфолио к концу обучения
- Практика с первого урока
Вы получите именно те инструменты и навыки, которые позволят вам найти работу
Как технически устроен обмен данными в интернете
Ответ на запрос
В традиционном HTTP, который сегодня использует большинство сайтов, веб-сервер предназначен для приема запросов от клиентов, а также для ответа на них. При этом коммуникация может быть инициирована только в одном направлении: от клиента к серверу. Код сервера определяет, какой тип запросов он должен ожидать и как реагировать на каждый из них.
Этот вид коммуникации можно сравнить с работой кухни в ресторане:
- Вы (клиент) размещаете заказ (HTTP-запрос), который официант несет на кухню (сервер)
- Кухня принимает заказ и проверяет, может ли его сделать (сервер обрабатывает запрос)
- Если на кухне знают, как приготовить блюдо, то сделают заказ (сервер получает данные из базы данных)
- Если на кухне не распознают заказ или не могут его обслужить, то отправляют официанта обратно с плохими новостями (если сервер не может ответить на запрос, он отправляет обратно код ошибки — например, 404)
- Официант возвращается к клиенту в любом случае (вы получаете HTTP-ответ с соответствующим кодом, например, 200 OK или 403 Forbidden).
Здесь важно отметить, что кухня понятия не имеет, от кого исходит заказ. Технический способ сказать это так: «HTTP не имеет состояния», — он рассматривает каждый новый запрос как полностью независимый.
У нас есть способы обойти это правило — например, можно отправлять файлы cookie, которые помогают серверу идентифицировать клиента. При этом сами HTTP-сообщения все равно будут читаться и выполняться независимо друг от друга.
В этом подходе кроется достаточно серьезная проблема: кухня не может сама прислать официанта к клиенту. Она может только выдать официанту блюдо или сказать, что еды больше нет — и только в том случае, когда клиент отправил официанта на кухню. То есть кухня ничего не знает о клиенте: она получает информацию только о поступающих заказах. Говоря языком сервера, единственный способ для клиентов получать обновленную информацию с сервера — отправлять запросы.
Представьте себе чат-приложение, в котором вы разговариваете с другом. Вы отправляете сообщение на сервер в виде запроса с текстом в качестве полезной нагрузки. Сервер получает ваш запрос и сохраняет сообщение. Но у него нет возможности связаться с компьютером вашего друга. Компьютер вашего друга также должен отправить запрос на проверку наличия новых сообщений — только тогда сервер может отправить ваше сообщение другому пользователю.
В таком виде оба клиента должны постоянно проверять сервер на наличие обновлений, внося неловкие задержки между каждым сообщением.
При этом в современном обществе любой пользователь рассчитывает, что сервер мгновенно покажет собеседнику, что он получил сообщение. HTTP-запрос и ответ отлично работают, когда пользователю нужно загрузить статическую страницу. Но этого становится недостаточно, когда нужна прямая коммуникация в режиме реального времени.
Короткий опрос
Одним из самых простых решений этой проблемы является метод «Короткий опрос». Просто заставьте клиента повторно пинговать сервер, скажем, каждые 500 мс. Таким образом, пользователь получит новые данные каждые 500 мс. Однако у этого подхода есть несколько очевидных недостатков:
- Задержка данных как минимум на 500 мс
- Пользователь потребляет ресурсы сервера при большом количестве запросов
- Большинство запросов возвращаются пустыми, если данные не часто обновляются.
Долгий опрос
Еще одним обходным путем задержки получения данных является метод «Долгий опрос». При его использовании сервер получает запрос от клиента, но не отвечает на него, пока не получит новые данные из другого запроса. Долгий опрос более эффективен, чем многократная проверка связи с сервером, поскольку он избавляет от хлопот, связанных с анализом заголовков запросов, запросом новых данных и отправкой часто пустых ответов.
Однако теперь сервер должен отслеживать несколько запросов и порядок их получения. Кроме того, время ожидания запросов может истекать, поэтому пользователю необходимо периодически отправлять новые запросы.
События, отправленные сервером: (SSE)/EventSource
Другой метод отправки сообщений — Server-Sent Events API , который позволяет серверу отправлять обновления клиенту, используя интерфейс JavaScript EventSource. Этот интерфейс создает постоянное однонаправленное соединение с сервером через HTTP и использует специальный заголовок текста/потока событий. В итоге все запросы обрабатываются кодом как события JavaScript, поэтому практически нет задержки между запросом и ответом.
Это почти то, что мы ищем — теперь мы можем получать обновления с сервера. Поскольку события Server-Sent (SSE) — однонаправленные, они отлично подходят для приложений, в которых вам не нужно отправлять на сервер какие-либо данные. К таким сервисам можно приравнять ленту новостей в Twitter или информационную панель с котировками акций в режиме реального времени.
Однако SSE не поддерживается старыми браузерами, а большинство существующих браузеров ограничивают количество одновременных подключений SSE. Но и этого еще недостаточно для создания современного мессенджера. Получать обновления в реальном времени — хорошо, но мы хотели бы иметь возможность их отправлять — и тоже в режиме реального времени.
Подробнее про веб-сокеты
Итак, нам нужен способ для моментальной отправки информации на сервер и такого же быстрого получения обновлений с сервера. Это возвращает нас к двусторонней («дуплексной») связи, с которой хорошо справляются именно веб-сокеты.
Поддерживаемый почти всеми современными браузерами API WebSocket позволяет открывать именно такое двустороннее соединение с сервером. Кроме того, сервер может отслеживать каждого клиента и отправлять сообщения подмножеству клиентов. То есть с помощью веб-сокетов мы можем пригласить всех друзей в наш чат и отправлять сообщения всем или некоторым из них, а не только одному человеку, как при использовании других протоколов связи.
Веб-сокеты под капотом
Итак, как именно работает это волшебство? Не пугайтесь настройки — современные библиотеки WebSocket, такие как socket.io, настраиваются почти автоматически. Более глубоко разобраться в настройке можно здесь , а мы приведем краткую выжимку по устройству веб-сокетов.
В тексте мы уже несколько раз упоминали HTTP — протокол и набор правил взаимодействия компьютеров в сети. Он состоит из запросов и ответов, каждый из которых содержит строку запроса («GET /assets/icon.png»), заголовки и необязательное тело сообщения, используемое, например, в POST-запросах для отправки некоторых данных на сервер.
WebSocket — это еще один протокол для отправки и получения сообщений. Как и HTTP, веб-сокеты отправляют сообщения через соединение TCP (протокол управления передачей данных). Это стандарт, который обеспечивает надежную и предсказуемую доставку данных, отправляемых в пакетах.
Читайте также: DevTools: как открыть инструменты разработчика в браузере и почему они делают работу эффективнее
Итак, HTTP и WebSocket используют один и тот же механизм доставки на уровне пакетов, но протоколы структурирования сообщений у них различаются:
- Чтобы установить соединение веб-сокетов с сервером, клиент сначала отправляет HTTP-запрос «рукопожатия» с заголовком нашего обновления, указывая, что клиент хочет установить соединение WebSocket.
- Запрос отправляется на ws: или wss:: URI (аналог http или https).
- Если сервер устанавливает соединение WebSocket, и это соединение разрешено, например, если запрос исходит от клиента, прошедшего проверку подлинности или внесенного в белый список, то сервер отправляет ответ об успешном рукопожатии. На это указывает HTTP-код 101 Switching Protocols.
После обновления соединения протокол переключается с HTTP на WebSocket. И хотя все пакеты по-прежнему отправляются через TCP, связь теперь соответствует формату сообщений WebSocket. Это происходит, потому что TCP является дуплексным протоколом, где клиент и сервер могут отправлять сообщения одновременно. Все данные могут быть фрагментированы, поэтому через этот формат можно отправить даже очень большое сообщение — например, изображение. В этом случае веб-сокеты разбивают его на фреймы. Каждый фрейм содержит небольшой заголовок, который указывает длину и тип полезной нагрузки, а также информацию, является ли этот кадр последним.
Сервер может открывать соединения WebSocket с несколькими клиентами — даже несколько соединений с одним и тем же клиентом. Затем он может отправить сообщение одному, нескольким или всем этим клиентам. На практике это означает, что к нашему чату могут подключиться несколько человек, и мы можем отправлять сообщения некоторым из них одновременно.
Наконец, когда соединение можно закрыть, либо клиент, либо сервер могут отправить сообщение «закрыть».
Как все это работает
В нашем примере для внешнего интерфейса мы будем использовать JavaScript, с помощью которого установим соединение с сервером с поддержкой WebSockets. После этого интерфейс будет воспринимать сообщения как события JavaScript — так же, как он обрабатывает все генерируемые пользователем события, такие как клики и нажатия клавиш.
В ванильном JavaScript мы делаем это, создавая новый объект WebSocket и добавляя обработчик событий для действий open, message и close. Мы также используем метод send для отправки данных на сервер:
const socket = new WebSocket('ws://localhost:8080'); socket.addEventListener('open', (event) => socket.send('Hello Server!'); >); socket.addEventListener('message', (event) => console.log('Message from server ', event.data); >); socket.addEventListener('close', (event) => console.log('The connection has been closed'); >);
На сервере нам нужно аналогично обработать запросы веб-сокетов. Например, в Node.js мы можем использовать популярный пакет ws для открытия соединения и обработки сообщений:
const WebSocket = require('ws'); const ws = new WebSocket.Server( port: 8080 >); ws.on('connection', (wsConnection) => wsConnection.on('message', (message) => console.log(`server received: $message>`); >); wsConnection.send('got your message!'); >);
Хотя в этом примере мы отправляем просто строки, самым распространенным вариантом использования WebSocket является отправка строковых данных в формате JSON, либо в двоичном формате. Это позволяет структурировать сообщения в удобном для пользователя виде.
В качестве еще одного примера можно привести Socket.io, популярный интерфейсный фреймворк для создания и управления соединениями веб-сокетов. В нем есть фантастическое пошаговое руководство по созданию приложения для чата Node/JavaScript. Эта библиотека автоматически переключается между WebSocket и методом «Долгий опрос», а также упрощает рассылку сообщений группам подключенных пользователей.
Как устанавливать соединение веб-сокетов
Процесс начинается с рукопожатия WebSocket — он включает в себя использование ws или wss, о котором мы писали чуть выше. Чтобы быстрее понять их, можно считать ws или wss эквивалентными HTTP и безопасному HTTP (HTTPS), соответственно.
Ожидается, что при использовании этой схемы серверы и клиенты будут следовать стандартному протоколу подключения WebSocket. Установка соединения веб-сокетов начинается с обновления HTTP-запроса, который содержит пару заголовков, таких как Connection: Upgrade, Upgrade: WebSocket, Sec-WebSocket-Key .
Запрос
Заголовок Upgrade в коде запроса ниже означает рукопожатие WebSocket, в то время как Sec-WebSocket-Key содержит случайное значение с использованием кодировки Base64. Это значение произвольно генерируется во время каждого рукопожатия WebSocket. Кроме того, заголовок ключа также является частью этого запроса.
Перечисленные выше заголовки образуют GET-запрос:
b6gjhT32u488lpuRwKaOWs==
Ответ
При получении ответа заголовок Sec-WebSocket-Accept часть значения, представленного в заголовке запроса Sec-WebSocket-Key . Это связано со спецификацией протокола — такой подход используется для подтверждения, что сервер поддерживает веб-сокет и позволяет обойти ошибки, в случае, когда веб-сокеты не поддерживаются:
rG8wsswmHTJ85lJgAE3M5RTmcCE=
Где используют веб-сокеты
Хотя вы можете вручную написать серверный код с использованием веб-сокетов, они уже очень удобно встроены во многие популярные платформы. Помимо Socket.io, существует множество других реализаций веб-сокетов в разных языках программирования:
- ActionCable в Ruby on Rails
- Channels в Django для Python
- Gorilla на Go
- Meteor — полноценный JavaScript-фреймворк, основанный на WebSocket вместо HTTP
- Apollo — сервер GraphQL, который помогает получать данные в режиме реального времени с помощью веб-сокетов.
Итак, какие типы проектов чаще всего используют веб-сокеты?
- Мессенджеры: концептуально самая простая реализация веб-сокетов. Пользователи отправляют сообщения на сервер, который мгновенно отправляет эти сообщения получателю. Также сервер может хранить группы подключений в каналах — так можно отправлять сообщения нескольким людям одновременно в общие чаты или просматривать сообщения от нескольких людей, например, в канале Slack.
- Многопользовательские игры: общий шаблон для многопользовательских игр заключается в том, что сервер хранит игровое состояние пользователей. Игроки выполняют какие-то действия, которые сразу отправляются на сервер. Он обновляет состояние игры и быстро отправляет его обратно пользователям. С HTTP каждый игрок должен был бы регулярно запрашивать состояние игры. С WebSocket каждое действие мгновенно передается всем игрокам.
- Приложения для совместной работы: Веб-сокеты позволяют сразу нескольким пользователям работать в одном документе и мгновенно обновлять его для всех.
- Инструменты разработчика: инструменты непрерывной интеграции, такие как CircleCI, используют веб-сокеты для мгновенного уведомления о завершении сборки.
- Карты: обновляются моментально при даже небольшом изменении GPS-координат пользователя.
Итоги
Протокол WebSocket — прекрасный инструмент для создания сервисов, которые могут обновляться в реальном времени. Но, как и все инструменты, это не панацея.
Использование WebSocket может быть излишним для простых приложений. Для обычной ленты новостей, ленты метрик или любого приложения, в котором вам нужно обновить контент, но не получать информацию взамен, веб-сокеты будут просто не нужны. Особенно учитывая, что настраивать запросы и HTTP-вызовы намного проще, чем веб-сокеты.
Профессия «Node.js-разработчик»
- Освойте платформу Node.JS и начните использовать JavaScript для работы с бэкендом
- Научитесь использовать современные инструменты для создания бэкенд-приложений, включая Express и Fastify
- Получите возможность стать Fullstack-разработчиком и повысить свою ценность на рынке труда