Разработка мобильного приложения для внутренней коммуникации сотрудников
Корпоративный мессенджер — технически сложнее, чем кажется. Реальный реалтайм, E2E-шифрование, push при закрытом приложении, синхронизация истории между устройствами, групповые чаты с тысячами участников, статусы прочтения, медиа-обмен. Это не один CRUD с WebSocket — это отдельная инфраструктура.
Транспортный слой: WebSocket vs XMPP vs собственный протокол
Три реальных варианта для корпоративного мессенджера:
WebSocket + собственный протокол. Самый распространённый подход. Клиент держит постоянное WebSocket-соединение, сервер пушит события. Минус: нужно самостоятельно реализовывать reconnect, heartbeat, acknowledgement, очередь сообщений при потере связи.
XMPP (Extensible Messaging and Presence Protocol). Зрелый протокол с богатой экосистемой. Серверы: Ejabberd, OpenFire, Prosody. Библиотеки для мобильного: XMPPFramework (iOS), Smack (Android). XMPP из коробки даёт presence (онлайн-статусы), MUC (групповые чаты), стандартизированное E2E через OMEMO. Минус: сложнее кастомизировать протокол под нестандартные требования.
Matrix + Element SDK. Open-source, federated, E2E по умолчанию через Megolm. Homeserver: Synapse или Dendrite. Клиентские SDK: matrix-ios-sdk, matrix-android-sdk2. Подходит, если нужна федерация между организациями.
Для типичного корпоративного проекта с нуля — WebSocket + собственный протокол на backend (NestJS + Redis Pub/Sub + PostgreSQL). XMPP выбираем, если нужна интеграция со сторонними XMPP-клиентами или зрелая реализация OMEMO.
Реалтайм-архитектура на клиенте
Ключевая задача: WebSocket-соединение должно пережить переход приложения в background. На iOS — соединение закрывается при backgrounding (через ~30 секунд). На Android — убивается агрессивными doze и battery optimization.
Для iOS: URLSessionWebSocketTask держим в AppDelegate-уровне, но foreground-reconnect через applicationWillEnterForeground. Push через APNs с content-available: 1 для background wake-up — это не WebSocket, но позволяет получить новое сообщение и показать нотификацию через UNNotificationServiceExtension.
На Android: WebSocket в Service (foreground service с постоянным уведомлением) — единственный надёжный способ держать соединение живым на всех производителях. На некоторых Android OEM (Xiaomi, Huawei, OPPO) foreground service убивается системой даже с включённым автозапуском. Решение: Firebase FCM как fallback для гарантированной доставки + собственный WebSocket для реалтайма.
class MessagingService : Service() {
private lateinit var webSocketClient: OkHttpClient
private var webSocket: WebSocket? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, buildNotification())
connectWebSocket()
return START_STICKY // Перезапускаем при убийстве системой
}
private fun connectWebSocket() {
val request = Request.Builder()
.url("wss://api.company.com/ws")
.header("Authorization", "Bearer ${tokenManager.token}")
.build()
webSocket = webSocketClient.newWebSocket(request, object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
val message = Json.decodeFromString<IncomingMessage>(text)
messageRepository.save(message)
notificationManager.showIfNeeded(message)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
scheduleReconnect(delay = 5_000L)
}
})
}
}
E2E-шифрование
Signal Protocol — де-факто стандарт для E2E в мессенджерах. Используется в Signal, WhatsApp, Skype. Для мобильного: libsignal-protocol-java (Android), SignalProtocolKit (iOS).
Протокол основан на Double Ratchet Algorithm: каждое сообщение шифруется новым ключом, компрометация одного ключа не раскрывает остальные (forward secrecy).
// Шифрование исходящего сообщения
val sessionCipher = SessionCipher(signalStore, recipientAddress)
val encryptedMessage = sessionCipher.encrypt(plaintext.toByteArray())
val payload = Base64.encodeToString(encryptedMessage.serialize(), Base64.NO_WRAP)
Для групповых чатов — Sender Key Distribution: один ключ шифрует для всей группы, что значительно эффективнее по сравнению с шифрованием каждому участнику отдельно.
Критический момент: ключи хранятся только на устройстве. Если пользователь удалил приложение или сменил устройство — история сообщений недоступна. Это trade-off, о котором нужно честно предупреждать заказчика. Решение — опциональный бэкап ключей через iCloud Keychain / Google Drive с разрешения пользователя.
Хранение и синхронизация истории
Локальная база — Room (Android) / Core Data (iOS). Сообщения хранятся с полями id, conversationId, senderId, encryptedContent, timestamp, status (sent / delivered / read).
Синхронизация при переустановке: если E2E включён без бэкапа ключей — история недоступна. Если бэкап есть или E2E опциональный — запрашиваем историю с сервера постранично (cursor-based pagination).
Статусы доставки и прочтения — отдельный механизм. Read receipt: получатель отправляет ack с messageId по WebSocket. Отправитель обновляет статус в локальной базе. При офлайне — read receipts накапливаются и отправляются при восстановлении соединения пакетом.
Медиа и файлы
Загрузка медиа — через отдельный HTTP endpoint, не через WebSocket. Для изображений: сначала загружаем thumbnail (JPEG, 200px, quality 40), потом по запросу — оригинал. Видео: HLS-стриминг или progressive download через AVPlayer / ExoPlayer.
При загрузке больших файлов — chunked upload с возможностью возобновления (S3 Multipart Upload). Индикатор прогресса через okhttp3.MultipartBody с кастомным RequestBody, который отчитывается о прогрессе.
Медиа кэшируется локально через Glide (Android) / Kingfisher (iOS) с ограничением размера кэша. Старые медиафайлы удаляются по LRU-политике.
Push-уведомления и бейджи
Push при закрытом приложении — через FCM (Android) / APNs (iOS). На iOS UNNotificationServiceExtension позволяет расшифровать содержимое уведомления перед показом (для E2E-сообщений) и обновить бейдж.
Для Android — FCM high-priority message с content-available. На Android 13+ нужен явный запрос разрешения POST_NOTIFICATIONS — без этого push не придёт.
Счётчик непрочитанных (бейдж) на иконке приложения: на iOS UNUserNotificationCenter.setBadgeCount() (iOS 16+) или через push-payload badge. На Android — ShortcutBadger library, поскольку нативного API для бейджей нет (каждый лончер имеет свой механизм).
Этапы и сроки
| Этап | Содержание | Срок |
|---|---|---|
| Проектирование | Архитектура протокола, E2E-решение, база данных | 2-3 недели |
| Backend | WebSocket-сервер, API, хранилище | параллельно с клиентом |
| iOS + Android | Реалтайм, UI, медиа, push | 10-14 недель |
| E2E-шифрование | Signal Protocol или OMEMO | +3-4 недели |
| Тестирование | Нагрузочное, реалтайм, edge-кейсы | 2-3 недели |
MVP без E2E (базовые чаты, push, история) — 3-4 месяца. С E2E и групповыми чатами — 5-7 месяцев. Стоимость рассчитывается индивидуально.







