Разработка мессенджера: от архитектуры до деплоя

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

1. Проектирование системы: архитектура, выбор базы данных и протоколов передачи данных

Проектирование системы: архитектура, выбор базы данных и протоколов передачи данных

Добро пожаловать на курс «Разработка мессенджера: от архитектуры до деплоя». Это первая и фундаментальная статья, в которой мы заложим основу нашего будущего приложения. Написание мессенджера — это «классика» системного дизайна (System Design). Кажется, что задача проста: переслать текст от Алисы к Бобу. Но что происходит, когда Алиса и Боб находятся на разных континентах, у Боба плохой интернет, а одновременно с ними общаются еще миллион пользователей?

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

Оценка нагрузки и требований

Прежде чем писать код, необходимо понять масштаб задачи. В системном дизайне это называется Back-of-the-envelope estimation (расчеты на салфетке). Допустим, мы проектируем мессенджер, похожий на WhatsApp или Telegram.

Предположим следующие вводные данные: * DAU (Daily Active Users): 10 миллионов пользователей. * Каждый пользователь отправляет в среднем 20 сообщений в день. * Средний размер сообщения — 100 байт (только текст).

Рассчитаем объем входящего трафика сообщений в день ():

Где — объем данных в день, — количество пользователей (10 млн), — количество сообщений на пользователя (20), — размер сообщения (100 байт).

Подставим значения:

Это кажется немного, всего 20 ГБ текста в день. Однако, если мы добавим медиафайлы (фото, видео), нагрузка вырастет в сотни раз. Кроме того, нам нужно хранить историю за несколько лет. Для 5 лет хранения текстовых данных формула будет выглядеть так:

Где — общий объем хранилища, — объем данных в день (20 ГБ), — количество дней в году, — количество лет (5).

Эти расчеты показывают, что обычная база данных на одном сервере быстро переполнится. Нам нужна масштабируемая архитектура.

Высокоуровневая архитектура

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

Основные компоненты нашей системы:

  • Load Balancer (Балансировщик нагрузки): Распределяет входящий трафик между серверами.
  • API Gateway: Единая точка входа, маршрутизирует запросы к нужным сервисам.
  • Auth Service: Отвечает за регистрацию и авторизацию.
  • Chat Service: Обрабатывает отправку и получение сообщений.
  • Presence Service: Отслеживает статус пользователя (онлайн/оффлайн).
  • Notification Service: Отправляет Push-уведомления, если пользователь оффлайн.
  • !Высокоуровневая архитектура микросервисного мессенджера

    Протоколы передачи данных

    Как клиенту общаться с сервером? Самый очевидный вариант — HTTP. Но у него есть проблема: клиент всегда инициализирует запрос. Сервер не может просто так «постучаться» к клиенту и сказать: «Эй, тебе пришло сообщение».

    Варианты решения:

  • Short Polling (Короткий опрос): Клиент каждые 2 секунды спрашивает сервер: «Есть новые сообщения?». Это создает огромную нагрузку на сервер и тратит батарею телефона.
  • Long Polling (Длинный опрос): Клиент отправляет запрос, а сервер не отвечает, пока не появится сообщение (или не выйдет таймаут). Это лучше, но все еще накладно при активной переписке.
  • WebSocket: Это протокол, обеспечивающий постоянное двустороннее соединение поверх TCP. После рукопожатия (handshake) клиент и сервер могут отправлять данные друг другу в любой момент с минимальными задержками.
  • Для нашего мессенджера мы выберем гибридный подход: * HTTP (REST API): Для действий, не требующих мгновенной реакции (регистрация, загрузка профиля, получение списка контактов, загрузка истории сообщений). * WebSocket: Исключительно для отправки и получения сообщений в реальном времени и статусов «печатает...».

    Выбор базы данных

    Выбор базы данных — критический этап. В мессенджере есть два принципиально разных типа данных:

    1. Данные пользователей (Профиль, Друзья, Настройки)

    Эти данные структурированы, меняются нечасто и требуют строгой согласованности (ACID). Нам важно, чтобы пользователь не мог зарегистрироваться дважды с одним email.

    * Выбор: Реляционная база данных (PostgreSQL или MySQL).

    2. История сообщений (Chat Logs)

    Здесь требования другие: * Огромный объем данных (как мы посчитали выше — терабайты). * Огромная скорость записи (Write-heavy workload). * Доступ обычно идет по ключу (ID чата) и сортировке по времени. * Реляционные связи не так важны.

    Обычный PostgreSQL начнет «тормозить» на таблице с миллиардами строк при вставке. Здесь идеально подходят NoSQL базы данных семейства Wide Column Store или Document Store.

    * Выбор: Cassandra (или ее более быстрый аналог ScyllaDB) или MongoDB.

    В индустрии для мессенджеров (Discord, Facebook Messenger) часто используют Cassandra из-за ее способности линейно масштабироваться при записи. Мы можем просто добавлять новые серверы, и кластер будет переваривать больше сообщений.

    3. Эфемерные данные (Статус Online/Offline)

    Эти данные меняются каждую секунду. Хранить их на диске — слишком медленно.

    * Выбор: Redis. Это база данных в оперативной памяти (In-Memory). Она работает молниеносно и поддерживает TTL (время жизни ключа), что удобно для статусов «онлайн».

    Масштабирование базы данных: Шардинг

    Даже самая мощная база данных имеет предел. Когда сообщений становится слишком много, мы применяем шардинг (sharding).

    Шардинг — это разделение данных одной логической таблицы на несколько физических серверов (шардов). Для мессенджера логично шардировать данные по chat_id или user_id.

    Формула выбора шарда () часто выглядит так:

    Где — номер сервера (шарда), на который пойдут данные, — идентификатор (например, ID чата), — общее количество серверов, а — операция взятия остатка от деления.

    Например, если у нас 4 сервера () и ID чата равен 10:

    Значит, история этого чата будет храниться на сервере №2.

    > «Преждевременная оптимизация — корень всех зол». > — Дональд Кнут

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

    Итоговый стек технологий для курса

    Подводя итог, в рамках этого курса мы будем строить систему на следующем стеке:

  • Backend: Go (Golang) или Node.js (мы выберем Go за его производительность и работу с конкурентностью).
  • Протокол: WebSocket для чата, REST для остального.
  • База данных: PostgreSQL (для пользователей) + MongoDB/Cassandra (для сообщений).
  • Кэш: Redis.
  • Брокер сообщений: Kafka или RabbitMQ (для асинхронной обработки, например, отправки Push-уведомлений).
  • В следующей статье мы перейдем к настройке окружения и созданию базового каркаса приложения.

    2. Серверная часть: REST API, аутентификация JWT и модели данных

    Серверная часть: REST API, аутентификация JWT и модели данных

    В предыдущей статье мы определились с архитектурой нашего мессенджера: микросервисы, язык Go, базы данных PostgreSQL и Cassandra. Теперь пришло время перейти от теории к практике и начать строить фундамент системы. Любое приложение начинается с пользователей. Если мы не можем надежно зарегистрировать пользователя и защитить его данные, остальной функционал (чаты, звонки) не имеет смысла.

    В этой статье мы спроектируем схему хранения данных пользователей, разберем принципы безопасного хранения паролей, настроим REST API и внедрим аутентификацию на основе JWT (JSON Web Tokens).

    Модели данных: Пользователи

    Начнем с проектирования базы данных для сервиса авторизации (Auth Service). Как мы решили ранее, для структурированных данных пользователей мы используем PostgreSQL.

    Таблица Users

    Нам необходимо хранить базовую информацию. Ключевое требование — уникальность email и username.

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

    Хеширование и Соление (Salting)

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

    Однако простого хеширования недостаточно. Существуют так называемые «радужные таблицы» (Rainbow Tables) — гигантские базы данных с уже вычисленными хешами для популярных паролей. Чтобы защититься от них, мы используем соль (salt) — случайную строку, которая добавляется к паролю перед хешированием.

    Математически процесс выглядит так:

    Где: * — итоговый хеш, который мы сохраняем в базу данных. * — криптографическая хеш-функция (например, Argon2 или bcrypt). * — исходный пароль пользователя. * — операция конкатенации (объединения строк). * — соль (уникальная случайная последовательность для каждого пользователя).

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

    !Визуализация процесса хеширования пароля с использованием соли

    Аутентификация: Проблема состояния

    Протокол HTTP является stateless (без сохранения состояния). Это значит, что сервер не помнит, кто отправил предыдущий запрос. Когда вы отправляете сообщение после входа в систему, сервер должен как-то понять, что это именно вы.

    Раньше для этого использовали сессии (Session ID), которые хранились в Cookies и в памяти сервера. Но в микросервисной архитектуре это неудобно: если у вас 10 серверов, им всем нужно иметь доступ к общему хранилищу сессий (например, Redis), что создает лишнюю нагрузку.

    Современный стандарт — JWT (JSON Web Token). Это токен, который содержит всю необходимую информацию о пользователе и подписан криптографической подписью сервера.

    Структура JWT

    Токен состоит из трех частей, разделенных точками: Header.Payload.Signature.

  • Header (Заголовок): Тип токена и алгоритм шифрования.
  • Payload (Полезная нагрузка): Данные пользователя (ID, роль, срок действия). Не храните здесь секретные данные (пароли), так как Payload легко декодируется.
  • Signature (Подпись): Гарантирует, что токен не был изменен.
  • Формула создания подписи:

    Где: * — цифровая подпись (Signature). * — алгоритм создания кода аутентификации сообщений с использованием хеш-функции SHA256. * — закодированный в Base64 заголовок (Header). * — закодированная в Base64 полезная нагрузка (Payload). * — конкатенация строк. * — секретный ключ, который хранится только на сервере.

    Если злоумышленник попытается изменить user_id в Payload, подпись перестанет совпадать, так как у него нет секретного ключа для генерации новой валидной подписи.

    !Структура и компоненты JSON Web Token

    Access и Refresh токены

    У JWT есть недостаток: если токен украдут, злоумышленник получит доступ к аккаунту навсегда (пока токен не истечет). Чтобы минимизировать риски, используется пара токенов:

  • Access Token (Токен доступа): Живет короткое время (например, 15 минут). Используется для запросов к API (получить сообщения, отправить файл).
  • Refresh Token (Токен обновления): Живет долго (например, 30 дней). Используется только для получения новой пары токенов.
  • Сценарий работы:

  • Клиент логинится и получает Access и Refresh токены.
  • Клиент делает запросы с Access токеном.
  • Через 15 минут Access токен протухает. Сервер возвращает ошибку 401.
  • Клиент отправляет Refresh токен на специальный эндпоинт.
  • Сервер проверяет Refresh токен в базе данных (да, Refresh токены стоит хранить в БД для возможности их отзыва) и выдает новую пару.
  • Проектирование REST API

    REST (Representational State Transfer) — это архитектурный стиль взаимодействия компонентов распределенного приложения в сети. Мы будем использовать стандартные HTTP методы: GET, POST, PUT, DELETE.

    Вот список основных эндпоинтов для нашего Auth Service:

    | Метод | URL | Описание | | :--- | :--- | :--- | | POST | /api/v1/auth/register | Регистрация нового пользователя. Принимает JSON с email и паролем. | | POST | /api/v1/auth/login | Вход в систему. Возвращает пару Access/Refresh токенов. | | POST | /api/v1/auth/refresh | Обновление токенов. Принимает Refresh токен. | | GET | /api/v1/users/me | Получение профиля текущего пользователя (требует Access токен). | | GET | /api/v1/users/{id} | Получение публичной информации о другом пользователе. | | POST | /api/v1/users/logout | Выход из системы (удаление Refresh токена из БД). |

    Пример реализации на Go (псевдокод)

    Рассмотрим, как может выглядеть обработчик регистрации на языке Go. Мы используем структуру для парсинга входящего JSON.

    Middleware для проверки авторизации

    Чтобы защитить эндпоинты (например, чтение переписки), нам нужен Middleware (промежуточное ПО). Это функция, которая перехватывает запрос до того, как он попадет в основной обработчик.

    Логика Middleware:

  • Извлечь токен из заголовка Authorization: Bearer <token>.
  • Проверить подпись токена с помощью секретного ключа.
  • Если подпись верна и срок действия не истек — пропустить запрос дальше, добавив user_id в контекст запроса.
  • Иначе — вернуть 401 Unauthorized.
  • Заключение

    Сегодня мы заложили «камень преткновения» безопасности нашего мессенджера. Мы спроектировали таблицу пользователей, научились правильно прятать пароли с помощью соли и хеширования, а также разобрали механизм JWT для авторизации в микросервисной среде.

    Теперь, когда у нас есть пользователи, которые могут безопасно входить в систему, нам нужно дать им возможность общаться. В следующей статье мы перейдем к самой захватывающей части — Real-time взаимодействию через WebSocket, чтобы сообщения доставлялись мгновенно.

    3. Магия реального времени: настройка WebSockets и обработка событий чата

    Магия реального времени: настройка WebSockets и обработка событий чата

    В предыдущих статьях мы спроектировали микросервисную архитектуру и реализовали надежную систему аутентификации с использованием JWT. Теперь у нас есть пользователи, которые могут безопасно входить в систему, но они все еще не могут общаться друг с другом.

    Классический HTTP-запрос работает по принципу «вопрос-ответ»: клиент спрашивает — сервер отвечает. Но в чате инициатором общения часто выступает собеседник, а не вы. Сервер должен иметь возможность «протолкнуть» (push) сообщение на ваше устройство, как только оно появится.

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

    Почему не HTTP?

    Представьте, что вы ждете важное письмо. В мире HTTP вы бы каждые 5 секунд подходили к почтовому ящику, открывали его и проверяли пустоту. Это называется Polling (опрос).

    Если пользователей 10 миллионов, и каждый делает запрос раз в 5 секунд, мы получаем:

    Где — количество запросов в секунду (RPS), — количество пользователей (), — интервал опроса (5 с).

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

    WebSocket решает эту проблему, создавая постоянный двусторонний канал связи (TCP-туннель), который остается открытым, пока пользователь находится в приложении.

    Протокол WebSocket и Handshake

    WebSocket начинается как обычный HTTP-запрос. Клиент отправляет запрос с заголовком Upgrade: websocket. Если сервер согласен, он отвечает статусом 101 Switching Protocols, и с этого момента соединение превращается в поток бинарных фреймов.

    !Визуализация процесса установления соединения WebSocket через механизм Handshake

    Реализация на Go: Паттерн Hub

    В языке Go (Golang) работа с конкурентностью реализована через горутины (goroutines) и каналы (channels). Это делает его идеальным выбором для WebSocket-серверов, где нужно держать миллионы открытых соединений.

    Мы будем использовать популярную библиотеку gorilla/websocket, так как она предоставляет более удобный API, чем стандартная библиотека.

    Архитектурно мы реализуем паттерн Hub (Хаб). Хаб — это центральный компонент, который знает обо всех активных клиентах и управляет рассылкой сообщений.

    Структура Клиента

    Каждый подключенный пользователь будет представлен структурой Client.

    Структура Хаба

    Хаб хранит список всех клиентов и управляет их регистрацией. Важно помнить, что доступ к карте (map) в Go не является потокобезопасным, поэтому мы будем использовать каналы для синхронизации или мьютексы. В данном примере используем каналы для чистоты архитектуры.

    Чтение и запись: Насосы (Pumps)

    Для каждого клиента мы запустим две горутины:

  • ReadPump: Читает сообщения из сокета и отправляет их в Хаб.
  • WritePump: Слушает канал send клиента и пишет данные в сокет.
  • Такое разделение необходимо, потому что WebSocket поддерживает одновременное чтение и запись, но запись в один и тот же сокет из нескольких горутин одновременно может привести к ошибкам.

    Оценка потребления памяти

    Почему Go так хорош для этого? Давайте посчитаем теоретическое потребление памяти для 1 миллиона соединений.

    Размер стека одной горутины начинается с 2 КБ. На каждого клиента у нас 2 горутины (чтение и запись) плюс структуры данных.

    Где: * — общий объем памяти. * — количество соединений (). * — размер стека горутины (2 КБ). * — размер структур Client и буферов (допустим, около 1 КБ).

    5 ГБ оперативной памяти для поддержания миллиона соединений — это отличный результат. На языках с потоками ОС (например, Java с классическими Thread) это потребовало бы в десятки раз больше памяти.

    Протокол событий (Event Protocol)

    По WebSocket передаются просто байты. Чтобы приложение понимало, что пришло — новое сообщение, уведомление о прочтении или статус «печатает...», нам нужен протокол уровня приложения. Обычно это JSON.

    Определим структуру сообщения:

    На сервере мы будем парсить поле type и запускать соответствующий обработчик.

    Обработка событий

  • new_message: Сохранить сообщение в Cassandra (об этом в следующей статье) и переслать получателю.
  • typing_start: Переслать получателю без сохранения в БД (это эфемерное событие).
  • message_read: Обновить статус сообщения в БД и уведомить отправителя, что его сообщение прочитано.
  • Проблема масштабирования и Redis Pub/Sub

    Реализация выше работает отлично, пока у нас один сервер. Но что, если серверов 10?

    Алиса подключена к Серверу А, а Боб — к Серверу Б. Хаб на Сервере А ничего не знает о клиентах Сервера Б. Если Алиса напишет Бобу, Сервер А посмотрит в свою карту clients, не найдет там Боба и сообщение потеряется.

    Для решения этой проблемы мы будем использовать Redis Pub/Sub (Publish/Subscribe).

    Когда Сервер А получает сообщение для Боба, он не ищет Боба у себя локально сразу. Он публикует событие в канал Redis:

    PUBLISH channel_bob "{...message JSON...}"

    Все сервера чата подписаны на каналы своих активных пользователей. Сервер Б, к которому подключен Боб, получит это событие от Redis и отправит его в WebSocket Боба.

    !Архитектура обмена сообщениями между несколькими серверами через Redis Pub/Sub

    Заключение

    Сегодня мы настроили «нервную систему» нашего мессенджера. Мы:

  • Поняли, почему HTTP не подходит для чатов.
  • Разобрали архитектуру WebSocket-сервера на Go.
  • Спроектировали JSON-протокол для событий.
  • Наметили решение проблемы масштабирования через Redis.
  • Теперь наши сообщения летают по сети мгновенно. Но пока они нигде не сохраняются. Если Боб выйдет из сети, он потеряет историю. В следующей статье мы займемся проектированием схемы данных в Cassandra для надежного хранения миллиардов сообщений.

    4. Клиентская часть: разработка UI, управление состоянием и интеграция с API

    Клиентская часть: разработка UI, управление состоянием и интеграция с API

    Мы прошли долгий путь: спроектировали архитектуру, создали базу данных, написали REST API и настроили WebSocket-сервер. Но для конечного пользователя все наши труды — это лишь невидимый механизм. Пользователь видит интерфейс. Если кнопки не нажимаются, а сообщения появляются с задержкой, то неважно, насколько гениален ваш бэкенд — приложением пользоваться не будут.

    В этой статье мы займемся разработкой клиентской части (Frontend). Мы не будем просто верстать формы, мы решим сложные инженерные задачи: управление глобальным состоянием, синхронизация данных через WebSocket и оптимизация рендеринга бесконечных списков сообщений.

    Выбор технологического стека

    Для разработки современного мессенджера нам нужен фреймворк, который эффективно работает с обновлением DOM (Document Object Model) и позволяет строить компонентную архитектуру.

    Мы выберем связку React + TypeScript.

    Почему TypeScript? В мессенджере критически важна структура данных. Сообщение — это не просто текст. Это объект с ID, автором, датой, статусом (отправлено/прочитано/ошибка), типом контента (текст/картинка) и вложениями. Ошибка в одном поле может сломать рендеринг всего чата. Строгая типизация спасает от 90% подобных ошибок на этапе написания кода.

    Архитектура состояния (State Management)

    Самая сложная часть фронтенда мессенджера — это управление данными. У нас есть три источника правды, которые конфликтуют между собой:

  • Серверное состояние: То, что лежит в базе данных (история переписки).
  • Локальное состояние: То, что пользователь ввел в поле ввода, но еще не отправил.
  • Real-time состояние: То, что прилетает через WebSocket прямо сейчас.
  • Если мы будем передавать данные через props (свойства компонентов) от родителя к ребенку на 10 уровней вниз, мы столкнемся с проблемой Prop Drilling. Код превратится в спагетти. Нам нужен Global Store (Глобальное хранилище).

    !Циклический поток данных: View -> Action -> Reducer -> Store -> View

    Структура глобального хранилища

    В нашем Store (например, на базе Redux Toolkit или Zustand) мы будем хранить нормализованные данные. Вместо массива чатов, где внутри каждого чата лежит массив сообщений, мы используем структуру «Словарь» (Map/Object).

    Почему это важно? Представьте, что пришло новое сообщение для чата с id=55. Если у нас массив, нам нужно пробежать по нему, найти чат и обновить его. Сложность поиска — . Если у нас словарь (объект), доступ к чату происходит мгновенно по ключу — .

    Пример структуры State:

    Интеграция с API и WebSocket

    Клиент должен уметь работать в гибридном режиме. При открытии чата мы загружаем историю через REST API (HTTP), а новые сообщения получаем через WebSocket.

    Проблема дубликатов и синхронизации

    Представьте ситуацию: вы открыли чат, загрузились последние 50 сообщений. В этот же момент через WebSocket прилетает новое сообщение. Если логика написана неверно, сообщение может добавиться дважды или нарушить порядок сортировки.

    Для решения этой задачи мы используем принцип «Единого источника правды» в Store. И HTTP-загрузчик, и WebSocket-слушатель отправляют данные в один и тот же Reducer, который проверяет уникальность ID перед добавлением.

    Оптимистичный UI (Optimistic UI)

    Пользователи ненавидят ждать. Когда вы отправляете сообщение в Telegram или WhatsApp, оно появляется в чате мгновенно, еще до того, как сервер подтвердил получение. Это называется Optimistic UI.

    Алгоритм работы:

  • Пользователь нажимает «Отправить».
  • Мы генерируем временный ID (например, temp-123) и сразу добавляем сообщение в Redux Store со статусом sending.
  • UI отрисовывает сообщение (обычно полупрозрачным или с иконкой часиков).
  • Фоном отправляется запрос на сервер.
  • Успех: Сервер возвращает настоящий ID. Мы заменяем временный ID на настоящий и меняем статус на sent.
  • Ошибка: Сервер не ответил. Мы меняем статус на error и показываем кнопку «Повторить».
  • Виртуализация списков (Virtualization)

    Это, пожалуй, самая важная техническая часть для производительности. Представьте, что в чате 100 000 сообщений. Если мы создадим 100 000 <div> элементов в браузере, страница зависнет и упадет. Браузер не рассчитан на такое количество DOM-узлов.

    Решение — Виртуализация (или оконный рендеринг). Мы рендерим только те сообщения, которые видны пользователю на экране, плюс небольшой запас сверху и снизу.

    !Принцип работы виртуализации: рендерится только видимая область списка

    Рассчитаем высоту контейнера для скроллбара. Даже если элементов нет в DOM, скроллбар должен вести себя так, будто они есть.

    Полная высота виртуального контейнера () вычисляется так:

    Где: * — общая высота контейнера в пикселях (которую мы задаем пустому блоку). * — общее количество сообщений в истории. * — средняя высота одного сообщения (в пикселях).

    Когда пользователь скроллит, мы вычисляем, какие индексы сообщений должны быть видны, используя формулу смещения:

    Где: * — индекс первого видимого сообщения. * — округление вниз до целого числа. * — текущая позиция скролла (scrollTop) в пикселях. * — средняя высота сообщения.

    Таким образом, вместо 100 000 элементов в DOM находится всего 20-30. Это позволяет скроллить историю любой длины плавно и без лагов.

    Обработка разрывов соединения

    Интернет нестабилен. Пользователь может зайти в лифт или спуститься в метро. WebSocket-соединение разорвется.

    Наш клиент должен реализовывать логику Reconnection (Переподключения):

  • Событие onclose у WebSocket.
  • Запуск таймера с экспоненциальной задержкой (Exponential Backoff). Сначала пробуем через 1с, потом через 2с, 4с, 8с.
  • После восстановления соединения мы не можем просто продолжить. За время оффлайна могли прийти новые сообщения.
  • Синхронизация: Клиент отправляет на сервер ID последнего полученного сообщения. Сервер присылает все, что было после него.
  • Безопасность на клиенте (XSS)

    Мессенджеры — главная цель для XSS (Cross-Site Scripting) атак. Если злоумышленник отправит сообщение <script>alert('hacked')</script>, и мы просто вставим его в HTML, скрипт выполнится у получателя.

    В React защита от XSS встроена по умолчанию: все переменные экранируются перед вставкой в DOM. Однако, если нам нужно поддерживать форматирование (жирный, курсив), мы не можем просто выводить текст.

    Мы должны использовать безопасные парсеры Markdown или HTML-санитизаторы (например, DOMPurify), которые вырезают опасные теги (script, iframe, object), оставляя безопасные (b, i, p).

    Заключение

    Разработка клиента мессенджера — это баланс между скоростью работы интерфейса и сложностью управления данными. Мы использовали: * Redux для глобального состояния и кэширования. * Optimistic UI для мгновенного отклика. * Виртуализацию для работы с огромными списками. * WebSocket для реального времени.

    Теперь у нас есть полностью рабочее приложение: бэкенд хранит данные, а фронтенд красиво их показывает. Но как доставить это приложение пользователям? Как обновлять его без простоя? В следующей, заключительной статье курса, мы разберем DevOps и Деплой: Docker, CI/CD и настройку Nginx.

    5. Финальные штрихи: шифрование сообщений, оптимизация и деплой приложения

    Финальные штрихи: шифрование сообщений, оптимизация и деплой приложения

    Поздравляю! Мы прошли огромный путь. Мы спроектировали микросервисную архитектуру, написали бэкенд на Go, настроили WebSocket для реального времени и создали реактивный интерфейс на React. У нас есть работающее приложение. Но готово ли оно к выходу в большой мир?

    Если мы запустим его сейчас, нас ждут три проблемы: наши сообщения сможет прочитать любой системный администратор, сервер упадет под нагрузкой из-за «тяжелых» картинок, а обновление кода превратится в ручной ад.

    В этой финальной статье курса мы превратим наш прототип в профессиональный продукт. Мы внедрим End-to-End шифрование, оптимизируем производительность и настроим автоматический деплой (CI/CD) с помощью Docker.

    Безопасность: End-to-End шифрование (E2E)

    В текущей реализации мы используем HTTPS. Это шифрует данные между клиентом и сервером (TLS). Но сервер видит текст сообщений. Это значит, что если базу данных взломают или недобросовестный сотрудник получит к ней доступ, переписка пользователей будет скомпрометирована.

    Золотой стандарт мессенджеров (Signal, WhatsApp, Telegram Secret Chats) — это End-to-End Encryption (E2E). При таком подходе ключи шифрования хранятся только на устройствах пользователей. Сервер выступает лишь как почтальон, передающий зашифрованные конверты, которые он не может открыть.

    !Принцип работы E2E шифрования: сервер передает данные, но не может их прочитать

    Обмен ключами: Алгоритм Диффи-Хеллмана

    Главная проблема криптографии: как Алисе и Бобу договориться о секретном ключе шифрования, если они общаются по незащищенному каналу, который прослушивает Ева?

    Для этого используется протокол Диффи-Хеллмана на эллиптических кривых (ECDH). Давайте рассмотрим упрощенную математическую модель классического алгоритма Диффи-Хеллмана, чтобы понять суть магии.

  • Алиса и Боб публично договариваются о двух числах: (генератор) и (простое число).
  • Алиса придумывает секретное число (приватный ключ) и никому его не говорит.
  • Боб придумывает секретное число (свой приватный ключ).
  • Алиса вычисляет публичный ключ и отправляет его Бобу:

    Где — публичный ключ Алисы, — общее число-генератор, — секретный ключ Алисы, — общее простое число, — операция взятия остатка от деления.

    Боб делает то же самое, вычисляет и отправляет Алисе:

    Где — публичный ключ Боба, — секретный ключ Боба.

    Теперь самое интересное. Алиса берет полученное от Боба число и возводит в свою секретную степень :

    Где — итоговый общий секрет.

    Боб берет число Алисы и возводит в свою степень :

    Математика гарантирует, что результат у них будет одинаковым. Этот и становится ключом для шифрования сообщений (например, алгоритмом AES). Злоумышленник, зная , , и , не может вычислить , не решив задачу дискретного логарифмирования, что вычислительно невозможно для больших чисел.

    В реальной разработке не стоит писать криптографию с нуля. Используйте проверенные библиотеки, реализующие Signal Protocol (например, libsignal).

    Оптимизация производительности

    Шифрование — это нагрузка на процессор (CPU), но главные «тормоза» мессенджера обычно связаны с сетью и базой данных.

    1. Оптимизация базы данных: Индексы

    Поиск сообщения в базе без индекса имеет сложность , где — количество записей. Если сообщений миллиард, поиск займет минуты. Индексы (обычно B-Tree) ускоряют поиск до логарифмической сложности.

    Где — время поиска, — количество записей.

    Для PostgreSQL обязательно создайте составной индекс для таблицы сообщений:

    Это позволит мгновенно получать последние сообщения конкретного чата.

    2. Медиа-контент и CDN

    Текст весит мало. Картинки и видео — много. Если ваш сервер в Германии, а пользователь в Австралии, загрузка фото займет секунды.

    Решение: CDN (Content Delivery Network). Это сеть серверов по всему миру. Когда пользователь загружает аватарку, он скачивает её с ближайшего к нему сервера.

    Также важно сжимать изображения. Никогда не отдавайте оригиналы фото с камеры (5-10 МБ). Конвертируйте их в формат WebP или AVIF на лету. Они обеспечивают качество, сравнимое с JPEG, при размере файла на 30-50% меньше.

    Деплой: От кода к продакшену

    Как запустить нашу сложную систему из микросервисов, баз данных и фронтенда на реальном сервере? Копировать файлы по FTP — прошлый век.

    Docker и Контейнеризация

    Мы упакуем каждый сервис в Docker-контейнер. Контейнер — это изолированная среда, в которой есть всё необходимое для запуска приложения (код, библиотеки, настройки). Главный принцип: «Работает на моем компьютере — работает везде».

    Пример Dockerfile для нашего Go-сервиса:

    Оркестрация с Docker Compose

    У нас много сервисов: Backend, Frontend, Postgres, Redis, Cassandra. Чтобы запустить их одной командой, используем Docker Compose.

    Файл docker-compose.yml описывает связи:

    Теперь весь мессенджер поднимается командой docker-compose up -d.

    Nginx как Reverse Proxy

    Мы не должны выставлять наши сервисы (порт 8080) напрямую в интернет. Перед ними должен стоять Nginx.

    Задачи Nginx:

  • SSL Termination: Берет на себя расшифровку HTTPS трафика.
  • Раздача статики: Быстро отдает файлы фронтенда (HTML, JS, CSS).
  • Маршрутизация: Запросы на /api отправляет на бэкенд, остальные — на фронтенд.
  • Балансировка нагрузки: Если у нас 5 бэкендов, Nginx распределит запросы между ними.
  • !Схема продакшен-окружения с использованием Nginx в качестве входной точки

    CI/CD: Автоматизация поставки

    Ручной деплой (зайти на сервер, скачать код, перезапустить Docker) приводит к ошибкам. Мы настроим CI/CD (Continuous Integration / Continuous Delivery).

    Пайплайн (конвейер) в GitHub Actions или GitLab CI выглядит так:

  • Push: Разработчик отправляет код в репозиторий.
  • Test: Автоматически запускаются Unit-тесты. Если они упали — процесс останавливается.
  • Build: Собираются Docker-образы и отправляются в реестр (Docker Hub).
  • Deploy: CI-система подключается к серверу по SSH и обновляет контейнеры.
  • Заключение курса

    Мы прошли путь от идеи до деплоя.

  • Архитектура: Мы выбрали микросервисы и правильные базы данных (Cassandra для логов, Postgres для юзеров).
  • Бэкенд: Реализовали REST API и JWT авторизацию.
  • Real-time: Настроили WebSockets и Redis Pub/Sub для мгновенной доставки.
  • Фронтенд: Создали быстрый UI с виртуализацией списков.
  • DevOps: Упаковали всё в Docker и защитили шифрованием.
  • Разработка мессенджера — это бесконечный процесс. Дальше вас ждут задачи реализации голосовых звонков (WebRTC), поиска по сообщениям (Elasticsearch) и борьбы со спамом. Но фундамент, который вы построили, готов выдержать эти вызовы.

    Спасибо, что были с нами на этом курсе. Пишите код, масштабируйте системы и создавайте продукты, которые соединяют людей!