Server-Sent Events в Swift: реальный стриминг данных без WebSocket

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему SSE — это ваш новый лучший друг

Представьте: вы сидите в уютном кафе, пьёте кофе, а ваше iOS-приложение в фоне получает свежие курсы валют, уведомления о новых сообщениях или данные с датчиков IoT-устройства. И всё это — без постоянных пингов к серверу, без разряда батареи и без сложных протоколов вроде WebSocket. Знакомьтесь: Server-Sent Events (SSE) в Swift — технология, которая делает стриминг данных настолько простым, что вы удивитесь, почему не использовали её раньше.

В этой статье мы разберём, как реализовать Server-Sent Events в Swift для iOS, macOS, watchOS и tvOS. Вы узнаете, почему SSE часто оказывается удобнее WebSocket, как написать свой SSE-клиент с нуля и какие подводные камни ждут новичков. Поехали!

Что такое Server-Sent Events и зачем они нужны?

Server-Sent Events — это стандарт W3C, позволяющий серверу отправлять данные клиенту по одному HTTP-соединению. В отличие от WebSocket, SSE работает только в одну сторону (сервер → клиент), но именно это делает его идеальным для задач, где не нужна двусторонняя связь.

Когда выбирать SSE, а не WebSocket?

  • Уведомления и алерты — сервер шлёт события, клиент только читает.
  • Потоковые данные — котировки акций, метрики сервера, GPS-трекинг.
  • Ленты новостей — Twitter-like обновления.
  • Прогресс выполнения — загрузка файлов, обработка видео.

Главный плюс SSE перед WebSocket — простота. Вам не нужен отдельный протокол, достаточно обычного HTTP. А это значит, что любой сервер на Python, Node.js или Go сможет отправлять события вашему Swift-приложению без лишних библиотек.

Как работает SSE: разбираем протокол

Сервер отправляет обычный HTTP-ответ с заголовком Content-Type: text/event-stream. Данные форматируются так:

event: message
data: {"text": "Привет, мир!"}
id: 42

event: update
data: {"count": 10}
retry: 3000

Ключевые поля:

  • event — тип события (опционально).
  • data — тело события (может быть многострочным).
  • id — идентификатор для восстановления соединения.
  • retry — время в мс перед переподключением при ошибке.

Каждое событие отделяется двумя символами новой строки \n\n. Просто, правда?

Создаём SSE-клиент на Swift: от простого к сложному

В стандартной библиотеке Foundation нет готового класса для SSE, но написать свой — дело 15 минут. Используем URLSession с делегатом, который обрабатывает потоковые данные.

Шаг 1: Базовый класс EventSource

import Foundation

final class EventSource: NSObject {
    private var url: URL
    private var session: URLSession?
    private var task: URLSessionDataTask?
    private var currentData = Data()
    private var lastEventId: String?
    
    var onEvent: ((String, String) -> Void)? // event, data
    var onError: ((Error) -> Void)?
    var onOpen: (() -> Void)?
    
    init(url: URL) {
        self.url = url
        super.init()
    }
    
    func connect() {
        var request = URLRequest(url: url)
        request.setValue("text/event-stream", forHTTPHeaderField: "Accept")
        if let lastId = lastEventId {
            request.setValue(lastId, forHTTPHeaderField: "Last-Event-ID")
        }
        
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = TimeInterval(INT_MAX)
        config.timeoutIntervalForResource = TimeInterval(INT_MAX)
        
        session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
        task = session?.dataTask(with: request)
        task?.resume()
    }
    
    func disconnect() {
        task?.cancel()
        session?.invalidateAndCancel()
    }
}

Шаг 2: Обработка потока через делегат URLSession

extension EventSource: URLSessionDataDelegate {
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        currentData.append(data)
        
        // Ищем разделители событий \n\n
        while let eventRange = currentData.range(of: Data([0x0a, 0x0a])) {
            let eventData = currentData.subdata(in: 0..<eventRange.lowerBound)
            currentData = currentData.subdata(in: eventRange.upperBound..<currentData.count)
            
            parseEvent(eventData)
        }
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error as NSError?, error.code != NSURLErrorCancelled {
            onError?(error)
        }
        
        // Автопереподключение с задержкой (можно кастомизировать)
        DispatchQueue.global().asyncAfter(deadline: .now() + 3.0) { [weak self] in
            self?.connect()
        }
    }
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            completionHandler(.cancel)
            return
        }
        onOpen?()
        completionHandler(.allow)
    }
}

Шаг 3: Парсинг SSE-сообщений

private extension EventSource {
    func parseEvent(_ data: Data) {
        guard let string = String(data: data, encoding: .utf8) else { return }
        
        var eventType = "message"
        var dataString = ""
        var eventId: String?
        
        string.enumerateLines { line, _ in
            if line.hasPrefix("event:") {
                eventType = String(line.dropFirst(6)).trimmingCharacters(in: .whitespaces)
            } else if line.hasPrefix("data:") {
                let value = String(line.dropFirst(5)).trimmingCharacters(in: .whitespaces)
                dataString += value + "\n"
            } else if line.hasPrefix("id:") {
                eventId = String(line.dropFirst(3)).trimmingCharacters(in: .whitespaces)
            } else if line == ":keepalive" {
                // Комментарий, игнорируем
            }
        }
        
        if let id = eventId {
            lastEventId = id
        }
        
        onEvent?(eventType, dataString.trimmingCharacters(in: .newlines))
    }
}

Используем наш EventSource в реальном проекте

Теперь давайте подключимся к SSE-эндпоинту (например, к серверу на Python с библиотекой sse-starlette):

class NewsViewController: UIViewController {
    private var eventSource: EventSource?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSSE()
    }
    
    private func setupSSE() {
        guard let url = URL(string: "https://api.example.com/stream/news") else { return }
        
        eventSource = EventSource(url: url)
        eventSource?.onOpen = {
            print("Соединение установлено!")
        }
        eventSource?.onEvent = { [weak self] event, data in
            DispatchQueue.main.async {
                switch event {
                case "news":                

Блоги

Рекомендаций по книгам