Введение: почему 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":