WebSocket на Swift: создание real-time приложений продвинутого уровня
В современной мобильной разработке real-time взаимодействие стало не просто фишкой, а необходимостью. Финансовые котировки, онлайн-игры, мессенджеры, совместное редактирование документов — все это требует мгновенной передачи данных между клиентом и сервером. В экосистеме Apple фреймворки Swift и Foundation предоставляют мощные инструменты для работы с протоколом WebSocket. В этой статье мы рассмотрим продвинутые техники: от нативного URLSessionWebSocketTask до кастомной обработки ошибок, многопоточности и оптимизации производительности.
1. Нативная реализация WebSocket через URLSessionWebSocketTask (iOS 13+)
Начиная с iOS 13, Apple предоставляет встроенную поддержку WebSocket через URLSessionWebSocketTask. Это предпочтительный способ, так как он не требует сторонних зависимостей и глубоко интегрирован с системой управления соединениями.
1.1. Создание и настройка сессии
Для начала создадим менеджер WebSocket с поддержкой повторного подключения и пулинга потоков:
import Foundation
class WebSocketManager: NSObject {
private var webSocketTask: URLSessionWebSocketTask?
private var session: URLSession!
private let queue = DispatchQueue(label: "com.websocket.queue", qos: .userInitiated)
private var isConnected = false
override init() {
super.init()
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
config.timeoutIntervalForResource = 300
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}
func connect(url: URL) {
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
receiveMessage()
}
func disconnect() {
webSocketTask?.cancel(with: .normalClosure, reason: nil)
isConnected = false
}
}
1.2. Продвинутая обработка сообщений с типизацией
Создадим enum для типов сообщений и реализуем парсинг:
enum WebSocketMessage {
case text(String)
case data(Data)
case ping
case pong
case error(Error)
}
extension WebSocketManager: URLSessionWebSocketDelegate {
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
isConnected = true
print("WebSocket connected")
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
isConnected = false
print("WebSocket closed: \(closeCode)")
// Автоматическое переподключение через 5 секунд
DispatchQueue.global().asyncAfter(deadline: .now() + 5) { [weak self] in
self?.reconnect()
}
}
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
self?.handleMessage(message)
self?.receiveMessage() // Циклический прием
case .failure(let error):
self?.handleError(error)
}
}
}
private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
switch message {
case .string(let text):
processText(text)
case .data(let data):
processData(data)
@unknown default:
break
}
}
}
2. Продвинутая работа с многопоточностью и производительностью
Real-time приложения требуют высокой производительности. Рассмотрим ключевые оптимизации.
2.1. Потокобезопасная очередь сообщений
class ThreadSafeMessageQueue {
private var queue = [WebSocketMessage]()
private let lock = NSLock()
func enqueue(_ message: WebSocketMessage) {
lock.lock()
queue.append(message)
lock.unlock()
}
func dequeueAll() -> [WebSocketMessage] {
lock.lock()
let messages = queue
queue.removeAll()
lock.unlock()
return messages
}
}
2.2. Использование Combine для реактивного стриминга
import Combine
class ReactiveWebSocket {
private let subject = PassthroughSubject<WebSocketMessage, Never>()
var publisher: AnyPublisher<WebSocketMessage, Never> {
subject.eraseToAnyPublisher()
}
func send(message: WebSocketMessage) {
subject.send(message)
}
// Фильтрация сообщений по типу
func textPublisher() -> AnyPublisher<String, Never> {
publisher
.compactMap { message in
if case .text(let text) = message { return text }
return nil
}
.eraseToAnyPublisher()
}
}
2.3. Оптимизация памяти: пулинг объектов и буферизация
class MessageBuffer {
private var buffer = [Data]()
private let maxSize = 100
private var flushTimer: DispatchSourceTimer?
func append(_ data: Data) {
buffer.append(data)
if buffer.count >= maxSize {
flush()
}
}
func flush() {
guard !buffer.isEmpty else { return }
let batch = buffer
buffer.removeAll()
// Отправка батча на сервер
sendBatch(batch)
}
private func sendBatch(_ batch: [Data]) {
// Сериализация и отправка
let combined = batch.reduce(Data(), +)
webSocketTask?.send(.data(combined)) { error in
if let error = error { print("Batch send error: \(error)") }
}
}
}
3. Обработка ошибок и восстановление соединения
В реальных приложениях стабильность критична. Реализуем механизм exponential backoff для повторных подключений.
3.1. Exponential backoff с джиттером
class ReconnectionHandler {
private var attempt = 0
private let maxAttempts = 10
private let baseDelay: TimeInterval = 1.0
func getNextDelay() -> TimeInterval {
attempt += 1
if attempt > maxAttempts {
return -1 // Сигнал о прекращении попыток
}
let exponential = baseDelay * pow(2.0, Double(attempt - 1))
let jitter = Double.random(in: 0...0.3 * exponential)
return exponential + jitter
}
func reset() {
attempt = 0
}
}
3.2. Graceful shutdown и сохранение состояния
class StatefulWebSocket {
private var pendingMessages = [Data]()
private let stateLock = NSLock()
func sendWithGuarantee(_ data: Data) {
stateLock.lock()
if isConnected {
webSocketTask?.send(.data(data)) { error in
if error != nil {
self.pendingMessages.append(data)
}
}
} else {
pendingMessages.append(data)
}
stateLock.unlock()
}
func flushPendingOnReconnect() {
stateLock.lock()
let messages = pendingMessages