О чем эта статья?
- uWebsockets.js - высокопроизводительная реализация http/websocket сервера для nodejs
- AsyncAPI - спецификация для асинхронного API, с помощью которой можно создать описание Websocket API
- Простой пример websocket API с использованием библиотеки wsapix:
- создадим websocket сервер, используя uWebsockets.js
- настроим валидацию получаемых и отправляемых сообщений
- добавим генерацию документации из кода
Краткое описание технологии
Websocket — это протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером через постоянное соединение. Данные передаются по нему в обоих направлениях в виде «пакетов», без разрыва соединения и дополнительных HTTP-запросов.WebSocket особенно хорош для сервисов, которые нуждаются в постоянном обмене данными, например онлайн игры, торговые площадки, работающие в реальном времени, и т.д.
Как использовать технологию в Nodejs
В Nodejs есть ряд библиотек, которые позволяют построить клиент-серверное взаимодействие по протоколу Websocket поверх нативной имплементации HTTP в Nodejs. Например нативная библиотека ws или очень популярная библиотека socket.io. Статью с более подробной информации про эти библиотеки можно найти на хабре.Помимо нативной имплементации http/ws на Nodejs есть библиотека uWebsockets.js, которая написана на C++ и позволяет создать http/websocket сервер для Nodejs, превосходящий по производительности в несколько раз нативную библиотеку и на порядок socket.io.
Сравнение производительности имплементаций Websocket для Nodejs 12.18
Эта библиотека стремительно набирает популярность, уже сейчас ее скачивают более 1м раз в месяц. Однако в настоящий момент существующие фреймворки не поддерживают uWebsockets.js.
Спецификация для описания Websocket API
Известная всем OpenAPI-спецификация, используемая для стандартизированного описание REST API, к сожалению, не подходит для описания websocket API, однако AsyncApi подходит прекрасно. Также как и OpenAPI спецификация не завязана на какой-то язык программирования, она удобна для использования как человеком, так и компьютерной программой, может быть двух форматов: JSON и YAML:Данная спецификация также отлично подходит для описания API "event-driven" архитектуры через брокеры, такие как RabbitMQ, Apache Kafka, Solace и т.д.
Библиотеки, позволяющие генерировать OpenAPI спецификации из кода, уже реализованы почти на всех языках программирования. Для Nodejs такой подход реализован из коробки в Fastify и Nest.js. Данный подход помогает решить сразу несколько задач:
- Документация на основе спецификации, сгенерированной из кода, будет 100% соответствовать API
- Спецификацию можно использовать для тестирования запросов/ответов через postman или другие подобные сервисы
Пример Websocket сервера с использованием wsapix
В качестве примера напишем простой чат, в котором создадим websocket сервер на основе uWebsockets.js и подключим Ajv для валидации входящих/исходящих сообщений. Сервер и клиент будут обмениваться следующими сообщениями:От сервера клиенту:
- пользовать вошел в чат
- пользователь вышел из чата
- пользователь отправил текстовое сообщение
- пользователь отправил текстовое сообщение
- uWebsockets.js - http/ws сервер
- wsapix - фреймворк для создания websocket API
- ajv - библиотека для валидации сообщений
- typebox - для описания Json схемы
npm i github:uNetworking/uWebSockets.js#v19.3.0
Подключим библиотеки, создадим uWebsocket сервер на порту 3000:
import { App } from "uWebSockets.js"
import { Wsapix } from "wsapix"
import { Type } from "@sinclair/typebox"
import Ajv from "ajv"
const port = Number(process.env.PORT || 3000)
const server = App()
server.listen(port, () => {
console.log(`Server listen port ${port}`)
})
Создадим wsapix сервер и подключим валидацию входящих/исходящих сообщений:
const ajv = new Ajv({ strict: false })
const validator = (schema, data, error) => {
const valid = ajv.validate(schema, data)
if (!valid) {
error(ajv.errors!.map(({ message }) => message).join(",\n"))
}
return valid
}
const wsx = Wsapix.uWS({ server }, { validator })
Добавим простейшую проверку подключений - на этом шаге может быть реализована полноценная аунтификация подключенного пользователя, но для простоты будем передавать имя пользователя через параметр запроса:
wsx.use((client) => {
if (!client.query) {
// если имя не указано, прерываем подключение
return client.terminate(4000)
}
// сохраняем имя и генерируем id
client.state = { id: Date.now().toString(36), name: client.query }
})
Опишем схему и добавим контроллер для сообщений от клиентов:
const userMessageSchema = {
$id: "user:message",
description: "New user message",
payload: Type.Strict(Type.Object({
type: Type.String({ const: "user:message", description: "Message type" }),
text: Type.String({ description: "Message text" })
}, { $id: "user:message" }))
}
wsx.clientMessage({ type: "user:message" }, userMessageSchema, (client, data) => {
wsx.clients.forEach((c) => {
if (c === client) { return }
c.send({
type: "chat:message",
userId: client.state.userId,
text: data.text
})
})
})
Опишем схему для сообщений от сервера, которая будет использоваться для валидации исходящих сообщений, а также для генерации документации. Добавим обработчики событий подключения и разъединения клиентов:
// New chat message schema
const chatMessageSchema = {
$id: "chat:message",
description: "New message in chat",
payload: Type.Strict(Type.Object({
type: Type.String({ const: "chat:message", description: "Message type" }),
userId: Type.String({ description: "User Id" }),
text: Type.String({ description: "Message text" })
}, { $id: "chat:message" }))
}
wsx.serverMessage({ type: "chat:message" }, chatMessageSchema)
// User connect message schema
const userConnectedSchema = {
$id: "user:connected",
description: "User online status update",
payload: Type.Strict(Type.Object({
type: Type.String({ const: "user:connected", description: "Message type" }),
userId: Type.String({ description: "User id" }),
name: Type.String({ description: "User name" }),
}, { $id: "user:connected" }))
}
wsx.serverMessage({ type: "user:connected" }, userConnectedSchema)
// Handle connect event
wsx.on("connect", (client) => {
wsx.clients.forEach((c) => {
if (c === client) { return }
c.send({ type: "user:connected", ...client.state })
client.send({ type: "user:connected", ...c.state })
})
})
// User disconnect message schema
const userDisconnectedSchema = {
$id: "user:disconnected",
description: "User online status update",
payload: Type.Strict(Type.Object({
type: Type.String({ const: "user:disconnected", description: "Message type" }),
userId: Type.String({ description: "User Id" }),
name: Type.String({ description: "User name" }),
}, { $id: "user:disconnected" }))
}
wsx.serverMessage({ type: "user:disconnected" }, userDisconnectedSchema)
// Handle disconnect event
wsx.on("disconnect", (client) => {
wsx.clients.forEach((c) => {
if (c === client) { return }
c.send({
type: "user:disconnected",
...client.state
})
})
})
Добавим генерацию документации для Webscoket API из кода на главную страницу :
server.get("/", (res) => {
res.writeHeader('Content-Type', 'text/html')
res.end(wsx.htmlDocTemplate("/wsapix"))
})
server.get("/wsapix", (res) => {
res.writeHeader('Content-Type', 'application/json')
res.end(wsx.asyncapi({
info: {
version: "1.0.0",
title: "Chat websocket API"
}
}))
})
Проверяем полученный результат (http://localhost:3000/):
Пробуем подключиться и отравить сообщение:
Исходник данного примера доступен по ссылке
P.S. Если для вас не критична производительность или вы не хотите переходить с нативной библиотеки на uWebsockets.js, то можно создать сервер на базе нативной библиотеки http/ws:
import * as http from "http"
import express from "express"
import { Wsapix } from "wsapix"
const port = Number(process.env.PORT || 3000)
const app = express()
const server = new http.Server(app)
const wsx = Wsapix.WS({ server })
server.listen(port, () => {
console.log(`Server listen port ${port}`)
})
Websocket API на nodejs по новому
О чем эта статья? uWebsockets.js - высокопроизводительная реализация http/websocket сервера для nodejs AsyncAPI - спецификация для асинхронного API, с помощью которой можно создать описание Websocket...
habr.com