Введение в WebSocket на Swift: от теории к практике
Современные мобильные и десктопные приложения всё чаще требуют мгновенной передачи данных. Будь то чат, биржевой терминал, онлайн-игра или система мониторинга — пользователи ожидают, что информация будет обновляться в реальном времени без постоянного обновления экрана. Именно здесь на помощь приходит протокол WebSocket.
В отличие от классического HTTP-запроса, который работает по принципу «запрос-ответ», WebSocket устанавливает постоянное двунаправленное соединение между клиентом и сервером. Это означает, что сервер может отправлять данные клиенту в любой момент без предварительного запроса. В экосистеме Apple для работы с WebSocket доступны как нативные решения (URLSessionWebSocketTask), так и популярные сторонние библиотеки, например Starscream.
В этой статье мы разберём, как интегрировать WebSocket в ваше Swift-приложение для iOS и macOS, напишем рабочие примеры кода и рассмотрим лучшие практики.
1. Нативная реализация WebSocket через URLSession
Начиная с iOS 13 и macOS 10.15, Apple предоставляет встроенную поддержку WebSocket через класс URLSessionWebSocketTask. Это самый простой и рекомендуемый способ, не требующий установки сторонних зависимостей.
Создание и настройка соединения
Для начала необходимо создать экземпляр URLSession и задать конфигурацию. Затем мы формируем URL с протоколом ws:// (или wss:// для защищённого соединения) и создаём задачу WebSocket.
import Foundation
class WebSocketManager: NSObject {
private var webSocketTask: URLSessionWebSocketTask?
private let session: URLSession
override init() {
let config = URLSessionConfiguration.default
self.session = URLSession(configuration: config, delegate: nil, delegateQueue: OperationQueue())
super.init()
}
func connect() {
guard let url = URL(string: "wss://echo.websocket.org") else { return }
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
receiveMessage()
}
func disconnect() {
webSocketTask?.cancel(with: .normalClosure, reason: nil)
}
}
Отправка и получение сообщений
WebSocket поддерживает отправку как текстовых (URLSessionWebSocketTask.Message.string), так и бинарных данных (.data). Для получения сообщений необходимо рекурсивно вызывать метод receive, так как он обрабатывает только одно сообщение за раз.
extension WebSocketManager {
func sendMessage(_ text: String) {
let message = URLSessionWebSocketTask.Message.string(text)
webSocketTask?.send(message) { error in
if let error = error {
print("Ошибка отправки: \(error.localizedDescription)")
}
}
}
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
switch message {
case .string(let text):
print("Получено текстовое сообщение: \(text)")
case .data(let data):
print("Получены бинарные данные: \(data.count) байт")
@unknown default:
print("Неизвестный тип сообщения")
}
// Рекурсивно продолжаем слушать
self?.receiveMessage()
case .failure(let error):
print("Ошибка получения: \(error.localizedDescription)")
}
}
}
}
Обратите внимание на рекурсивный вызов receiveMessage() внутри замыкания успеха — это гарантирует, что ваше приложение будет постоянно получать новые данные от сервера.
2. Работа с библиотекой Starscream
Хотя нативный API покрывает большинство задач, библиотека Starscream остаётся популярным выбором благодаря удобному делегатному подходу и дополнительным возможностям, таким как автоматическое переподключение и поддержка старых версий iOS.
Установка и подключение
Starscream можно установить через Swift Package Manager (SPM) или CocoaPods. Для SPM добавьте пакет в Xcode: File → Add Packages → https://github.com/daltoniam/Starscream.git.
import Starscream
class StarscreamManager: WebSocketDelegate {
var socket: WebSocket!
func setupConnection() {
var request = URLRequest(url: URL(string: "wss://echo.websocket.org")!)
request.timeoutInterval = 5
socket = WebSocket(request: request)
socket.delegate = self
socket.connect()
}
// MARK: - WebSocketDelegate
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(let headers):
print("Подключено: \(headers)")
case .disconnected(let reason, let code):
print("Отключено: \(reason) с кодом \(code)")
case .text(let string):
print("Текст: \(string)")
case .binary(let data):
print("Бинарные данные: \(data)")
case .pong(let data):
print("Pong: \(data?.description ?? "nil")")
case .ping(let data):
print("Ping: \(data?.description ?? "nil")")
case .error(let error):
print("Ошибка: \(error?.localizedDescription ?? "nil")")
case .viabilityChanged(let isViable):
print("Соединение активно: \(isViable)")
case .reconnectSuggested(let shouldReconnect):
print("Рекомендуется переподключение: \(shouldReconnect)")
case .cancelled:
print("Соединение отменено")
default:
break
}
}
}
Ключевое преимущество Starscream — единый метод didReceive(event:client:), который обрабатывает все возможные события: подключение, отключение, получение данных, ошибки и даже пинг-понг сообщения.
3. Создание real-time чата на SwiftUI
Давайте соберём всё вместе и создадим простое приложение-чат с использованием SwiftUI и нативного WebSocket. Это поможет понять, как интегрировать WebSocket в реальный проект.
Модель данных и ViewModel
import SwiftUI
struct Message: Identifiable {
let id = UUID()
let text: String
let isIncoming: Bool
}
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = []
@Published var isConnected = false
private var webSocketTask: URLSessionWebSocketTask?
private let session = URLSession.shared
func connect() {
guard let url = URL(string: "wss://echo.websocket.org") else { return }
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
isConnected = true
listen()
}
func send(_ text: String) {
let message = Message(text: text, isIncoming: false)
messages.append(message)
let wsMessage = URLSessionWebSocketTask.Message.string(text)
webSocketTask?.send(wsMessage) { error in
if let error = error {
print("