Введение в WebSocket на Swift: от теории к продвинутой практике
В современной мобильной разработке real-time взаимодействие стало не просто фишкой, а необходимостью. Пользователи ожидают, что чаты, ленты новостей, котировки и уведомления будут обновляться мгновенно, без лишних жестов pull-to-refresh. WebSocket — это тот самый протокол, который позволяет установить постоянное двунаправленное соединение между клиентом (вашим приложением на Swift) и сервером.
В этой статье мы выйдем за рамки простого "Hello, World!" и разберем продвинутые техники работы с WebSocket на Swift для платформ iOS и macOS. Вы узнаете, как настроить надежное соединение, обрабатывать сложные сценарии (потеря связи, переподключение, фоновый режим) и строить архитектуру, которая выдержит нагрузку реального биржевого приложения или чата с тысячами пользователей.
Мы рассмотрим как нативный URLSessionWebSocketTask (iOS 13+), так и популярную библиотеку Starscream для версий ниже. Код будет полностью рабочим и готовым к интеграции.
1. Продвинутая настройка WebSocket клиента на Swift
Начнем с фундамента. Вместо тривиального подключения, создадим менеджер, который будет управлять жизненным циклом соединения, обрабатывать ошибки и поддерживать reconnect (автоматическое переподключение).
1.1. Менеджер с автоматическим переподключением (Reconnect)
Реализуем класс WebSocketManager, который использует URLSessionWebSocketTask. Ключевая особенность — экспоненциальная задержка между попытками переподключения, чтобы не перегружать сервер.
import Foundation
final class WebSocketManager: NSObject {
private var webSocketTask: URLSessionWebSocketTask?
private var session: URLSession?
private var isConnected = false
private var reconnectAttempt = 0
private let maxReconnectDelay: TimeInterval = 30 // Максимальная задержка 30 сек
private let baseDelay: TimeInterval = 2 // Базовая задержка 2 сек
var onMessageReceived: ((String) -> Void)?
var onConnectionStateChanged: ((Bool) -> Void)?
func connect(url: URL) {
session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
let request = URLRequest(url: url, timeoutInterval: 10)
webSocketTask = session?.webSocketTask(with: request)
webSocketTask?.resume()
receiveMessage()
}
private func reconnect() {
guard let url = webSocketTask?.originalRequest?.url else { return }
let delay = min(baseDelay * pow(2.0, Double(reconnectAttempt)), maxReconnectDelay)
print("Reconnecting in \(delay) seconds... (Attempt \(reconnectAttempt + 1))")
DispatchQueue.global().asyncAfter(deadline: .now() + delay) { [weak self] in
self?.connect(url: url)
self?.reconnectAttempt += 1
}
}
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
switch message {
case .string(let text):
self?.onMessageReceived?(text)
case .data(let data):
if let text = String(data: data, encoding: .utf8) {
self?.onMessageReceived?(text)
}
@unknown default:
break
}
self?.reconnectAttempt = 0 // Сброс счетчика при успешном получении
self?.receiveMessage() // Продолжаем слушать
case .failure(let error):
print("Receive error: \(error.localizedDescription)")
self?.disconnect()
self?.reconnect()
}
}
}
func send(text: String) {
let message = URLSessionWebSocketTask.Message.string(text)
webSocketTask?.send(message) { error in
if let error = error {
print("Send error: \(error.localizedDescription)")
}
}
}
func disconnect() {
webSocketTask?.cancel(with: .goingAway, reason: nil)
isConnected = false
onConnectionStateChanged?(false)
}
}
extension WebSocketManager: URLSessionWebSocketDelegate {
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
isConnected = true
reconnectAttempt = 0
print("WebSocket connected")
onConnectionStateChanged?(true)
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
isConnected = false
print("WebSocket disconnected with code: \(closeCode.rawValue)")
onConnectionStateChanged?(false)
reconnect()
}
}
Этот менеджер решает главную проблему продакшн-приложений — устойчивость к обрывам связи. Обратите внимание на делегат didCloseWith: он автоматически запускает переподключение.
2. Реализация real-time чата с поддержкой комнат
Теперь построим на базе нашего менеджера простой, но показательный чат. Будем использовать протокол JSON для обмена сообщениями.
2.1. Модели данных и парсинг
Создадим модель сообщения и структуру для отправки/приема.
struct ChatMessage: Codable {
let type: String // "join", "leave", "message"
let room: String
let sender: String
let text: String?
let timestamp: TimeInterval
}
// Пример отправки сообщения в комнату
func sendChatMessage(manager: WebSocketManager, room: String, sender: String, text: String) {
let message = ChatMessage(type: "message", room: room, sender: sender, text: text, timestamp: Date().timeIntervalSince1970)
if let jsonData = try? JSONEncoder().encode(message),
let jsonString = String(data: jsonData, encoding: .utf8) {
manager.send(text: jsonString)
}
}
2.2. Обработка входящих сообщений в реальном времени
В контроллере чата подключаемся к менеджеру и обновляем UI.
class ChatViewController: UIViewController {
private let webSocketManager = WebSocketManager()
@IBOutlet weak var tableView: UITableView!
private var messages: [ChatMessage] = []
override func viewDidLoad() {
super.viewDidLoad()
setupWebSocket()
}
private func setupWebSocket() {
guard let url = URL(string: "wss://your-server.com/chat") else { return }
webSocketManager.onMessageReceived = { [weak self] text in
guard let self = self,
let data = text.data(using: .utf8),
let message = try? JSONDecoder().decode(ChatMessage.self, from: data) else { return }
DispatchQueue.main.async {
self.messages.append(message)
self.tableView.reloadData()
// Прокрутка к последнему сообщению
let indexPath = IndexPath(row