СМИ о нас
Пишем свой dependency
free WebSocket сервер
на Node.js
#IT
Время чтения: 11 мин 2 сек
30 Июля 2021
#IT
Поделиться статьей
Подпишитесь на рассылку
Статья программиста IT-компании Lad Сергея Кучина.
Node.js — это популярный инструмент для построения клиент-серверных приложений. При правильном использовании, Node.js способен обрабатывать большое количество сетевых запросов, используя всего один поток. Несомненно, сетевой ввод — вывод является одной из сильнейших сторон этой платформы. Казалось бы, что используя Node.js для написания серверного кода приложения, активно использующего различные сетевые протоколы, разработчики должны знать, как эти протоколы работают, но зачастую это не так. Виной тому еще одна сильная сторона Node.js, это его пакетный менеджер NPM, в котором можно найти готовое решение практически под любую задачу. Используя готовые пакеты, мы упрощаем себе жизнь, переиспользуем код (и это правильно), но в то же время скрываем от себя, за ширмой библиотек, суть происходящих процессов. В этой статье мы постараемся разобраться в протоколе WebSocket, реализуя часть спецификации, не используя внешних зависимостей. Добро пожаловать под кат.
Историческая справка
Для начала необходимо разобраться с исторической составляющей, а именно, зачем придумали сетевой протокол WebSocket и что послужило главной мотивацией для его создания. Изначально приложения, которым требовался активный обмен данными с сервером, использовали протокол http, что накладывало много ограничейний, связанных с этим протоколом. Ведь при создании http не предпологалось использовать его как двунаправленный протокол. Http работает по принципу request/reply — клиент отправляет запрос на сервер, а сервер на этот запрос формирует ответ и отправляет его клиенту. Каждый раз при такой схеме происходит установка нового соединения (напомню, что я рассказываю про стародревние времена до http 2.0). Протокол не подразумевает, что сервер сам может инициировать соединение с клиентом и отправить ему сообщение. Поэтому многие клиентские приложения, на подобии чатов, используя проткол http, были вынуждены с определенным интервалом опрашивать сервер на предмет изменений его состояния. Существует спецификация RFC6202, которая описывает лучшие практики относительно того, как серверу передавать сообщения клиенту по своей инициативе. Первая версия стандарта протокола WebSocket появилась в 2008 году, после чего несколько раз перерабатывалась. То, что мы знаем как WebSocket на данный момент появилось в 2011 году в виде 13ой версии протокола и описанной в стандарте RFC6455. Протокол находится на том же уровне сетевой модели OSI что и http и так же работает поверх tcp. WebSocket решает все описанные проблемы присущие http. Протокол WebSocket является двунаправленным, что означает, что после установки соединения, клиент и сервер могут обмениваться асинхронными сообщениями по открытому подключению. Инициировать подключение может как клиент так и сервер. К слову сказать, поддержка протокола WebSocket в браузере появилась в 2009 году и первым браузером, реализовавшем стандарт, был Google Chrome 4й версии. Но от к слов к делу, у нас есть протокол, давайте разберемся с ним и начнем его реализовывать. Работа с WebSocket делится на два больших этапа:

1. Создание соединения с помощью процесса рукопожатия (handshake)
2. Передача данных
Рукопожатие
Для того, чтобы клиент смог установить соединение с сервером, по протоколу WebSocket, нужно перевести http сервер в этот режим работы. Чтобы это сделать, нужно отправить GET запрос со специальными заголовками. Но чтобы понять, какие заголовки отправляются на сервер из браузера, при попытки установить сокетное соединение, не будем сразу смотреть в спецификацию к протоколу, а начнем писать сервер и увидем эти заголовки в консоли. Для начала напишем http сервер, который будет принимать любой запрос и выводить в консоль заголовки этого запроса. Код я буду писать на typescript и запускать с помощью ts-node.
import * as http from 'http';
import * as stream from 'stream';
 
export class SocketServer {
  constructor(private port: number) {
    http
      .createServer()
      .on('request', (request: http.IncomingMessage, socket: stream.Duplex) => {
        console.log(request.headers);
      })
      .listen(this.port);
      console.log('server start on port: ', this.port);
  }
}
 
new SocketServer(8080);
Сервер будет запущен на порту 8080. Теперь откроем консоль разработчика в браузере и напишем следующий код.
const socket = new WebSocket('ws://localhost:8080');
При создании объекта класса WebSocket, браузер попытается подключиться к серверу. У полученного объекта можно посмотреть текущее состояние подключения с помощью свойства readyState. Это свойство может принимать одно из четырех значений:

0 — установка соединения
1 — соединение установлено. Данные можно передавать
2 — соединение находится в процессе закрытия
3 — соединение закрыто

Если сейчас посмотреть свойство readyState, то оно будет в состоянии 0, но через некоторое время перейдет в состояние 3. Это происходит потому, что мы ничего не ответили на запрос перевода сервера на работу с другим протоколом. Подробнее про WebSocket API в браузере можно почитать тут

В консоли с запущенным сервером получим следующее:
{
  host: 'localhost:8080',
  connection: 'Upgrade',
  pragma: 'no-cache',
  'cache-control': 'no-cache',
  'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
  upgrade: 'websocket',
  origin: 'chrome-search://local-ntp',
  'sec-websocket-version': '13',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
  'sec-websocket-key': 'h/k2aB+Gu3cbgq/GoSDOqQ==',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits'
}
Для перехода на другой протокол используется стандартный механизм, описанный в стандарте http RFC2616. Происходит http запрос типа GET, в котором передаётся заголовок upgrade с названием протокола, на который клиент хочет переключить сервер. Если сервер поддерживает желаемый протокол, то он должен ответить кодом 101, если нет — вернуть ошибку. В описании протокола WebSocket дополнительно передаётся еще несколько заголовков, часть из которых опциональны:

sec-websocket-version версия проткола. На текущий момент это 13я версия
sec-websocket-extensions список расширений протокола, которые хочет использовать клиент. В данном случае, это сжатие сообщений
sec-websocket-protocol в этом заголовки клиент может передать список подпротоколов, на которых клиент хочет общаться с сервером. При этом сервер, если поддерживает эти подпротоколы, должен выбрать один из переданных и отправить его название в заголовках ответа. Подпротокол — это формат данных, в котором будут отправляться и приниматься сообщения.
sec-websocket-key самый важный заголовок для установки подключения. В нем передаётся случайный ключ. Этот ключ должен быть уникальным для каждого рукопожатия.

Чтобы клиент понял, что сервер успешно перешел на нужный протокол, сервер должен ответить кодом 101, а в ответе должен быть заголовок sec-websocket-accept, значение которого сервер должен сформировать, используя заголовок sec-websocket-key следующим образом:

1. Добавить к заголовку sec-websocket-key константу 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
2. Получить хеш sha-1 полученного объединенного значения
3. Перевести полученный хеш в строку в кодировке base64

Так же сервер должен передать в заголовках ответа заголовки Upgrade: WebSocket и Connection: Upgrade. Звучит не сложно, давайте реализуем. Для генерации загловка sec-websocket-key нам потребуется встроеный в node.js модуль crypto. Необходимо в начале импортировать его.
import * as crypto from 'crypto';
А затем изменить конструктор класса SocketServer
private HANDSHAKE_CONSTANT = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
constructor(private port: number) {
  http
    .createServer()
    .on('upgrade', (request: http.IncomingMessage, socket: stream.Duplex) => {
      const clientKey = request.headers['sec-websocket-key'];
      const handshakeKey = crypto
        .createHash('sha1')
        .update(clientKey + this.HANDSHAKE_CONSTANT)
        .digest('base64');
      const responseHeaders = [
        'HTTP/1.1 101',
        'upgrade: websocket',
        'connection: upgrade',
        `sec-webSocket-accept: ${handshakeKey}`,
        '\r\n',
      ];
      socket.write(responseHeaders.join('\r\n'));
    })
    .listen(this.port);
  console.log('server start on port: ', this.port);
}
У http сервера Node.js есть специальное событие на upgrade соединения, используем его. Перезапустив сокет сервер с этими изменениями и снова попытавшись создать соединение в браузере, мы получим объект сокета, который будет в состоянии 1. Мы успешно создали соединение с нашим сервером и завершили первый этап. Переёдем ко второму.
Передача данных
Передача данных по сокетам происходит с помощью фреймов. Каждый фрейм — это единица информации с данными и метаинформацией. Фреймы сокетов никак не соотносятся с делением информации на фреймы или пакеты на более низких уровнях сетевой модели. За счет того, что информация передаётся фреймами, появляется возможность фрагментировать сообщения, т.е. пересылать сообщения частями. За счет фрагметации можно по сокетам передвать сообщения неизвестной длины, например, если нам нужно передать в виде сообщения большой файл, который мы читаем из стороннего источника (так тоже можно). Для того, чтобы разобраться с фреймами, нужно понимать, по каким правилам он формируется. В стандарте приведена следующая структура фрейма.
Визуально структура фрейма, на этой картинки, выглядит достаточно сложно. Поэтому я поделил его на несколько частей.
Неизменная часть фрейма. Длина этой части 2 байта
FIN Этот бит показывает конечный это фрейм или нет. Если значение его 1, то фрейм конечный, если 0, то этот фрейм принадлежит фрагментированному сообщению и его следует буферизировать. Сообщения могут состоять из одного фрейма.
RSV1, RSV2, RSV3 Эти три бита нужны для расширений протокола и используются ими. В нашем примере они будут нулевыми
OPCODE Эти 4 бита определяют тип фрейма. Фреймы делятся на два больших типа: управляющие фреймы и фреймы с данными. Всего фреймы с данными могут быть двух типов. Текстовые данный, в кодировке UTF8, и бинарные. Управляющих фреймов всего 3 ping, pong, close. Остальные коды зарезервированны для дальнейшего возможного использования.
• х0 Обозначает, что это фрейм — продолжение фрагментированного сообщения
• х1 Фрейм с текстовым сообщением
• х2 Фрейм с бинарными данными
• х8 Фрейм инициирующий закрытие подключения
• х9 Фрейм Ping
• xA Фрейм Pong
MASK Этот бит говорит — замаскированны данные внутри фрейма или нет. Если 0, то данные не замаскированны, если 1, то данные замаскированны. Спецификация протокола требует, чтобы данные с клиента были всегда замаскированны, а с сервера всегда не замаскированны. Сама маска фрейма, если она есть, хранится в следующей части фрейма.
Длина сообщения Эти 7 бит определяют, чем будут являться следующие байты фрейма.
Изменяемая часть фрейма. Длина этой части от 0 до 12 байт
• Если длина сообщения <= 125, то это короткое сообщение и это значение интерпритируется, именно, как длина сообщения. Поэтому в изменяемой части фрейма будет только маска, если это сообщение с клиента
• Если длина сообщения = 126 то следующие 2 байта хранят его размер
• Если длина сообщения = 127 то следующие 8 байт хранят его размер
Данные фрейма
Определив какой размер данных, в полученном нами фрейме, мы можем прочитать его. Но данные с клиента всегда должны быть маскированны. Маска — это случайные 4 байта, которые клиент передаёт во фрейме. Отправляя данные, клиент накладывает эту маску на данные с помощью функции XOR. Для того, чтобы сервер мог расшифровать информацию, нужно повторно наложить маску с помощью функции XOR.

Это основные знания, которые потребуются для реализации WebSocket сервера.
Реализуем часть протокола
Что бы в реализации сервера была какая то цель, нужно эту цель придумать. Целью кода данной статьи будет написание WebSocket сервера, который реализует часть протокола сокетов и позволяет переписываться нескольким клиентам из консоли браузера. Для начала нужно реализовать функционал опроса клиента с помощью управляющих фреймов Ping. Нам нужно знать, что клиент еще жив и готов принимать данные с сервера. Фрейм Ping, управляющий фрейм, но он так же может содержать данные. Когда клиент получит такое сообщение по сокету, он должен отправить на сервер фрейм Pong с теми данными, которые были во фрейме Ping. До реализации этого функционала, давайте пропишем в класс сервера необходимые константы
private MASK_LENGTH = 4; // Длина маски. Указана в спецификации
private OPCODE = {
  PING: 0x89, // Первый байт управляющего фрейма Ping
  SHORT_TEXT_MESSAGE: 0x81, // Первый байт фрейма с данными, которые убираются в 125 байт
};
private DATA_LENGTH = {
  MIDDLE: 128, // Нужно, чтобы исключить первый бит из байта с длинной сообщения
  SHORT: 125, // Максимальная длина короткого сообщения
  LONG: 126, // Означает, что следующие 2 байта содержат длину сообщения
  VERY_LONG: 127, // Означает, что следующие 8 байт содержат длину сообщения
};
Далее реализуем наш метод по формированию фрейма Ping
private ping(message?: string) {
  const payload = Buffer.from(message || '');
  const meta = Buffer.alloc(2);
  meta[0] = this.OPCODE.PING;
  meta[1] = payload.length;
  return Buffer.concat([meta, payload]);
}
По большому счету, в данном случае, это не требуется. Нам совершенно не обязательно пересылать какие-то данные клиенту вместе с управляющим фреймом Ping. Поэтому этот метод можно удалить, а вместо него в класс добавить еще одну константу. Также для того, чтобы реализовать функционал чата, нам потребуется хранить объекты подключений. Заведем под это отдельную коллекцию в классе сервера.
private CONTROL_MESSAGES = {
  PING: Buffer.from([this.OPCODE.PING, 0x0]),
};
private connections: Set<stream.Duplex> = new Set();
Модицифируем конструктор, добавим отправку фрейма Ping подключившимся клиентам с интервалом в 5 секунд, а также добавляем новых клиентов в коллекцию.
setInterval(() => socket.write(this.CONTROL_MESSAGES.PING), heartbeatTimeout);
this.connections.add(socket);
Теперь мы можем принимать соединения по сокетам и поддерживать его с помощью пингов. Осталось научить наш сервер маршрутизировать сообщения от клиентов. В спецификации к протоколу написано, что клиенты всегда должны отправлять сообщения на сервер в маскированном виде, а сообщения сервера всегда без маски. Из этого следует, что нам нужно раскодировать сообщение, а для этого нужно понять, что за сообщение пришло на сервер, получить маску, длину сообщения и сами данные. Напишем для этого метод
private decryptMessage(message: Buffer) {
  const length = message[1] ^ this.DATA_LENGTH.MIDDLE; // 1
  if (length <= this.DATA_LENGTH.SHORT) {
    return {
      length,
      mask: message.slice(2, 6), // 2
      data: message.slice(6),
    };
  }
  if (length === this.DATA_LENGTH.LONG) {
    return {
      length: message.slice(2, 4).readInt16BE(), // 3
      mask: message.slice(4, 8),
      data: message.slice(8),
    };
  }
  if (length === this.DATA_LENGTH.VERY_LONG) {
    return {
      payloadLength: message.slice(2, 10).readBigInt64BE(), // 4
      mask: message.slice(10, 14),
      data: message.slice(14),
    };
  }
  throw new Error('Wrong message format');
}
1. В этой строке нам нужно получить длину данных внутри фрейма. Мы делаем это с помощью операции XOR и констранты, которая представляет число 128 в двоичном виде, которое выглядит как 10000000. В данном случае мы это делаем, исходя из того, что данные от клиента всегда приходят в маскированном виде, а значит первый бит этого байта всегда будет 1.
2. Согласно спецификации для фреймов с длиной 126, длина сообщения передаётся в двух следующих байтах
3. Согласно спецификации для фреймов с длиной 127, длина сообщения передаётся в восьми следующих байтах

С помощью этой функции мы можем получать всю необходимую информацию для обмена сообщениями между клиентами. Напишем метод, который будет демаскировать данные
private unmasked(mask: Buffer, data: Buffer) {
  return Buffer.from(data.map((byte, i) => byte ^ mask[i % this.MASK_LENGTH]));
}
Демаскирование происходит путем применения функции XOR к каждому байту данных и соответствующему ему байту маски. Длина маски указана в спецификации и составляет 4 байта. Теперь можно написать метод для отправки коротких сообщений по сокету клиенту.
public sendShortMessage(message: Buffer, socket: stream.Duplex) {
  const meta = Buffer.alloc(2);
  meta[0] = this.OPCODE.SHORT_TEXT_MESSAGE;
  meta[1] = message.length;
  socket.write(Buffer.concat([meta, message]));
}
Нам осталось финализировать конструктор класса. Добавим туда рассылку полученных сообщений от клиента всем активным клиентам, а также добавим отправку всем клиентам сообщения при подключении нового клиента.
socket.on('data', (data: Buffer) => {
  if (data[0] === this.OPCODE.SHORT_TEXT_MESSAGE) { // Обрабатываем в данном примере только короткие сообщения
    const meta = this.decryptMessage(data);
    const message = this.unmasked(meta.mask, meta.data);
    this.connections.forEach(socket => {
      this.sendShortMessage(message, socket);
    });
  }
});
 
this.connections.forEach(socket => {
  this.sendShortMessage(
    Buffer.from(`Подключился новый участник чата. Всего в чате ${this.connections.size}`),
    socket,
  );
});
Теперь можно запустить сервер. Для проверки работоспособности можно открыть две вкладки браузера и в консоли каждой вклдаки написать следующий код.
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = ({ data }) => console.log(data);
Затем отправить сообщение в одной из вкладок
socket.send('Hello world!');
Итоги
Конечно, если в вашем приложении нужны WebSocket'ы, а скорее всего они нужны, не стоит реализовывать протокол самостоятельно без существенной необходимости. Всегда можно выбрать подходящее решение из многообразия библиотек в npm. Лучше переиспользовать уже написанный и протестированный код. Но понимание как это работает "под капотом", всегда даст много больше, чем просто использование чужого кода. Приведенный выше пример доступен на github
Другие статьи из раздела СМИ о нас
07 Октября
СМИ о нас  
Почему обучение технического писателя — это нетривиальная задача
01 Октября
СМИ о нас  
IT-компания Lad помогает бизнесу и государству управлять HR-капиталом
28 Сентября
СМИ о нас  
ООО «Платформа строительных сервисов» — серебряный партнер конференции #ГИСОГД2021
27 Сентября
СМИ о нас  
Проект управления HR капиталом региона представлен на «ПРОФ-IT»
27 Сентября
СМИ о нас  
Список региональных IT-проектов – победителей конкурса «ПРОФ-IT.2021»
27 Сентября
СМИ о нас  
IX Всероссийский форум «ПРОФ-IT» — фотолента
СМИ о нас  
ТВ: На старт благотворительного забега “Открытые сердца” вышли более 200 человек
30 Августа
СМИ о нас  
Топ-17 крупнейших ИT-компаний Нижнего Новгорода
18 Августа
СМИ о нас  
Нижегородская область лидирует по доступности мер поддержки IT-компаний
05 Августа
СМИ о нас  
Прокачиваем анимацию с react-native-reanimated. Часть 1
02 Августа
СМИ о нас  
Чем занимается технический писатель: опыт IT-компании Lad
27 Июля
СМИ о нас  
Сколько стоит рекомендация IT-специалиста в Нижнем. Аналитика
16 Июля
СМИ о нас  
IT-компания Lad вошла в ТОП-5 работодателей по найму Junior-разработчиков
06 Июля
СМИ о нас  
Технический писатель: с чего начать
28 Июня
СМИ о нас  
10 эко-приложений, которые помогают спасать природу
27 Июня
СМИ о нас  
IT-кампус на 7 000 студентов создадут в Нижнем Новгороде
23 Июня
СМИ о нас  
В Нижнем Новгороде пройдет паблик-ток о развитии IT-сферы
23 Июня
СМИ о нас  
В Арсенале обсудят IT в Нижегородской области
16 Июня
СМИ о нас  
Обучающая программа «Веб-разработка» от HiBrain стартовала при ННГУ
15 Июня
СМИ о нас  
Оплатить ЖКХ в режиме онлайн теперь можно на портале «Карты жителя Нижегородской области»
10 Июня
СМИ о нас  
«Прекрасная идея» или напрасные затраты? IT-бизнес — о проекте редевелопмента Започаинья
10 Июня
СМИ о нас  
Нижегородские IT-компании смогут побороться за победу во всероссийском конкурсе «IT-проект: Back To The Product»
03 Июня
СМИ о нас  
Скидки и сертификаты стали доступны на «Карте жителя Нижегородской области»
01 Июня
СМИ о нас  
Фестиваль детских инновационных проектов прошел в технопарке «Кванториум Нижний Новгород»
26 Мая
СМИ о нас  
Нижегородские школьники разработали маску дополненной реальности для сервиса «Карта жителя Нижегородской области»
20 Мая
СМИ о нас  
Как интегратор 1С балуется плюшками
17 Мая
СМИ о нас  
Историческая викторина в честь 800-летия Нижнего Новгорода стартовала на “Карте жителя”
12 Мая
СМИ о нас  
Жители региона смогут получать цифровые квитанции через сервис «Карта жителя Нижегородской области
12 Мая
СМИ о нас  
Квитанции об оплате услуг ЖКХ можно получить на портале «Карта жителя»
27 Апреля
СМИ о нас  
Джентльменский набор приложений для смартфона в 2021 году
23 Апреля
СМИ о нас  
IT-компания Lad вошла в число крупнейших работодателей рынка IT в Нижнем Новгороде
16 Апреля
СМИ о нас  
Какой кошелек выбрать?
05 Апреля
СМИ о нас  
Формы платформы
04 Апреля
СМИ о нас  
На 30% Минстрой России прогнозирует повышение индекса IQ городов к 2024 году
01 Апреля
СМИ о нас  
К 2024 году в России планируется на 30% повысить индекс цифровизации городов
22 Марта
СМИ о нас  
Эко-квест «800 шагов к чистому городу»
18 Марта
СМИ о нас  
Все школы в России обеспечат доступом к интернету в этом году
06 Марта
СМИ о нас  
Команды из Нижегородской области стали победителями межрегионального онлайн-хакатона
06 Марта
СМИ о нас  
Кейс «Instagram-маски в честь 800-летия Нижнего Новгорода» от IT-компании Lad
01 Марта
СМИ о нас  
Где работать в ИТ в 2021: Lad
28 Февраля
СМИ о нас  
Нижегородский онлайн-хакатон «VRARHack52» соберет юных разработчиков со всей страны
22 Февраля
СМИ о нас  
Диагностируем проблемы в микросервисной архитектуре на Node.js с помощью OpenTracing и Jaeger
25 Декабря
СМИ о нас  
В российских школах заменяют Word и Excel на отечественные аналоги
24 Декабря
СМИ о нас  
Дмитрий Петров о налоговых льготах для IT-сферы
СМИ о нас  
Нижегородские IT-компании получат налоговые льготы
22 Декабря
СМИ о нас  
Профит для бизнеса от работы контактного центра на удаленке: кейс компании Lad
10 Декабря
СМИ о нас  
Росатом интегрирует решения российских разработчиков в проекты Smart City
07 Декабря
СМИ о нас  
Портал "Карта жителя Нижегородской области" стал удобнее
06 Декабря
СМИ о нас  
На портале «Карта жителя Нижегородской области» обновлен профиль пользователя
30 Ноября
СМИ о нас  
iCluster обсудит лучшие практики импортозамещения
27 Ноября
СМИ о нас  
Lad: цифровая трансформация бизнеса
19 Ноября
СМИ о нас  
IT-компания Lad поделится лучшими практиками
18 Ноября
СМИ о нас  
Компания Lad проведет день открытых дверей и расскажет о трендах IT-разработки
14 Ноября
СМИ о нас  
Участники регионального iCluster расскажут об отраслевых трендах
29 Октября
СМИ о нас  
О будущем проекта "Карта жителя Нижегородской области"
22 Октября
СМИ о нас  
Эксперт Lad дал комментарий Tadviser об импортозамещении офисного софта
14 Октября
СМИ о нас  
Как ИТ-специалисту устроиться на работу в хорошую компанию
07 Октября
СМИ о нас  
Карту жителя Нижегородской области начнут тестировать в октябре
06 Октября
СМИ о нас  
К сервису «Карта жителя Нижегородской области» присоединились первые банки-партнеры
01 Октября
СМИ о нас  
Пакет офисных решений "Р7-Офис" поступит в школы Ингушетии
01 Октября
СМИ о нас  
Ингушетия получит 3 тыс. лицензий на российское ПО для системы образования
25 Августа
СМИ о нас  
НБД-Банк и IT-компания Lad предлагают малому бизнесу онлайн-кассы в аренду
18 Июня
СМИ о нас  
Алтайский край переходит на российское ПО
06 Мая
СМИ о нас  
Work must go on!
03 Апреля
СМИ о нас  
IT-компании предлагают свои продукты бесплатно
26 Марта
СМИ о нас  
ПСС на Digital.Forum Construction 2020
24 Марта
СМИ о нас  
Быстрая цифровизация строительной отрасли
16 Марта
СМИ о нас  
Партнерская акция IT-компании Lad и НБД-Банка
13 Февраля
СМИ о нас  
Отечественное ПО для белгородских школьников
СМИ о нас  
Платформа HiBrain - разработка IT-компании Lad
СМИ о нас  
Digital Summit 2019
24 Октября
СМИ о нас  
В Нижнем Новгороде iCluster провёл IT-фестиваль "iFest 2019"
21 Октября
СМИ о нас  
Lad на фестивале программистов IFest
03 Октября
СМИ о нас  
Обрабатываем заказы из интернет магазина с помощью RabbitMQ и TypeScript
27 Сентября
СМИ о нас  
Lad примет участие в Digital Summit 2019
СМИ о нас  
Как IT-технологии изменили жизнь нижегородцев?
06 Августа
СМИ о нас  
В Нижегородской области запустят ПСС
07 Июня
СМИ о нас  
На ПМЭФ презентованы проекты IT-компании Lad
06 Июня
СМИ о нас  
На ПМЭФ презентованы ключевые нижегородские ИТ проекты
29 Мая
СМИ о нас  
В Нижнем Новгороде запустили цифровую платформу IT-образования HiBrain
24 Апреля
СМИ о нас  
Kari втрое ускорила оформление рассрочки
12 Февраля
СМИ о нас  
Цифровизация Retail-отрасли: актуальные тренды
23 Ноября
СМИ о нас  
Лучшие IT-проекты выбрали в Нижнем Новгороде
Поделитесь вашим мнением
Ответ успешно отправлен Мы свяжемся с вами в ближайшее время

Файлы cookies

Мы используем файлы cookies. Это необходимо для анализа трафика и корректной работы сайта. Продолжая работу с сайтом, вы подтверждаете свое согласие на применение этих технологий.

подробнее хорошо