Вариант работы с вебсокетами в iOS на языке Swift / Написал менеджер для работы с websocket

    Всем привет.

    4 года назад я уже разбирался с вебсокетами в iOS, тогда я решил задачу с помощью одной из библиотек cocoapods, статья есть на Хабре. А сегодня хочу продемонстрировать еще одно решение, нативное без cocoapods.

    Я написал свой менеджер для работы с вебсокетами (Менеджер в данном случае синглтон класс который может быть вызван в любом месте приложения, так как это синглтон его экземпляр будет создан только один раз для всего приложения).

    Воспользуюсь я для своих целей нативными средствами.

    import Foundation
    
    class WSManager {
        public static let shared = WSManager() // создаем Синглтон
        private init(){}
        
        private var dataArray = [МОДЕЛЬ_МОИХ_ДАННЫХ]()
        
        let webSocketTask = URLSession(configuration: .default).webSocketTask(with: URL(string: "wss://ТУТ_ВАШ_АДРЕС"*))
        
       //функция вызова подключения
        public func connectToWebSocket() {
            webSocketTask.resume()
            self.receiveData() { _ in }
        }
        
    //функция подписки на что либо
        public func subscribeBtcUsd() {
            let message = URLSessionWebSocketTask.Message.string("SUBSCRIBE: НА_ЧТО_ПОДПИСЫВАЕМСЯ")
            webSocketTask.send(message) { error in
                if let error = error {
                    print("WebSocket couldn’t send message because: \(error)")
                }
            }
        }
    
    //функция отписки от чего либо    
        public func unSubscribeBtcUsd() {
               let message = URLSessionWebSocketTask.Message.string("UNSUBSCRIBE: ОТ_ЧЕГО_ОТПИСЫВАЕМСЯ ")
               webSocketTask.send(message) { error in
                   if let error = error {
                       print("WebSocket couldn’t send message because: \(error)")
                   }
               }
           }
        
    //функция получения данных, с эскейпингом чтобы получить данные наружу
        func receiveData(completion: @escaping ([МОДЕЛЬ_МОИХ_ДАННЫХ]?) -> Void) {
          webSocketTask.receive { result in
            switch result {
                case .failure(let error):
                  print("Error in receiving message: \(error)")
                case .success(let message):
                  switch message {
                    case .string(let text):
                        let data: Data? = text.data(using: .utf8)
                        let srvData = try? CODABLE_МОДЕЛЬ_ТОГО_ЧТО_ДОЛЖНО_ПРИЙТИ.decode(from: data ?? Data())
                        for singleData in srvData ?? [] {
                            self.dataArray.append(МОДЕЛЬ_МОИХ_ДАННЫХ(параметр1:  singleData.parametr1, параметр2:  singleData.parametr2, параметр3:  singleData.parametr3))  
                        }
    
                    case .data(let data):
    // В вашем варианте данные могут приходить сразу сюда
                        print("Received data: \(data)")
                  @unknown default:
                    debugPrint("Unknown message")
                  }
                  
                  self.receiveData() {_ in } // рекурсия
            }
          }
            completion(self.dataArray) // отправляем в комплишн то что насобирали в нашу модель
        }
    }
    

    Вот такой менеджер получился, пример вызова

    import UIKit
    
    class MainViewController: UIViewController {
        
        private var dataArray = [МОДЕЛЬ_МОИХ_ДАННЫХ]()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            WSManager.shared.connectToWebSocket() // подключаемся 
            WSManager.shared.subscribeBtcUsd() //подписываемся на получение данных
            self.getData() //получаем данные
        }
    
        private func getData() {
            //получаем данные
            WSManager.shared.receiveData() { [weak self] (data) in
                guard let self = self else { return }
                guard let data = data else { return }
                 self.dataArray = data // кладем данные в переменную и дальше можно делать с ними то что требуется
            }
        }
    
    }
    

    *по поводу адреса
    wss:// это аналог https://
    ws:// это аналог http://

    Вот такой вариант работы с вебсокетом получился, если есть вопросы, пожелания, поучения как сделать лучше, пишите, буду рад :)

    Тестовый пример доступен у меня в гитхабе

    Также я использую extension для Decodable который доступен тоже у меня в гитхабе
    я про вот эту часть

    let srvData = try? CODABLE_МОДЕЛЬ_ТОГО_ЧТО_ДОЛЖНО_ПРИЙТИ.decode(from: data ?? Data())

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 17

      0
      Я конечно не сварщик не swift программист, но хотел поинтересоваться, тестируют ли программы на Swift?
      И как корректно тестировать статик синглтоны? Может на swift есть какая-то магическая изоляция глобального публичного состоятия?
        0
        я пока в тестах неочень, возможно смогу чуть позже более развернуто ответить) спустя 3-6 месяцев) но пока такой задачи нет. Но вообще первое что гуглится stackoverflow.com/questions/8256989/singleton-and-unit-testing и medium.com/@martinrybak/how-to-mock-singletons-and-static-methods-in-unit-tests-cbe915933c7d буду смотреть в ту сторону если понадобится
          0
          И как корректно тестировать статик синглтоны?
          Не очень сварщик в плане того, что нечасто удается писать тесты. Но тестирование синглтона — мне не видится проблемой. Синглтон: обычный класс со статическим полем, в котором лежит предсозданный экземпляр. Можно тестировать этот уже готовый экземпляр, а можно — создать ещё экземпляры, и гонять тесты на них.
          Другой вопрос — это тестирование кода, в котором происходит работа с синглтоном. Но опять же, если синглтон инъектить как зависимость закрытую протоколом, то класс, в который синглон инъектирован, тестируется так же как обычно.
          То есть с точки зрения тестируемости, синглтон может быть вполне безвреден.
          Что касается
          изоляция глобального публичного состояния
          — тут, да, код выглядит довольно опасным. Все места в программе работающие с синглтоном будут работать с общим проперти dataArray, и если пользователь зачем-то начнёт работать с этим синглтоном из разных потоков, то начнется гонка за dataArray. Результатом станет емнип неопределенное поведение.
          0

          По моему мнению работа с веб-сокетами это своего рода компромисс который был создан с прицелом на веб-браузеры. Поэтому для нативных iOS/Android приложений перспективнее использовать транспорт tcp/tsl который поддерживают многие протоколы для работы с сообщениями например amqp, wamp или mqtt.


          Единственное что можно поставить плюсом веб-сокетам для нативных приложений — это использование портов и протоколов https, которые обычно доступны и обычно не блокируются разными бранмауэрами или провайдерами сетевых услуг.

            0

            Если уж на то пошло — можно и grpc/graphql предложить.

              0

              Это все же вопрос-ответные протоколы. А веб-сокеты чаще используются для проталкивания сообщений с сервера клиенту, как собственное и упомянутые amqp, wamp или mqtt

                0

                Стриминг в grpc отлично работает.

            0
            Можете подсказать зачем импортируется cfnetwork? Сколько не смотрел, не нашел его использование.

            Ну и уточняющий момент: webSocketTask доступен начиная с айос13 в urlsession. Вы не поддерживаете предыдущие версии ос?
              0
              cfnetwork случайно, удалил, спасибо.
              по поводу второго, это просто пример как можно использовать нативно, пока что используем cocoapods решения.
              0
              1. Для («wss://ТУТ_ВАШ_АДРЕС») сделать static переменную
              2. «shared» менеджера объявить как private а методы менеджера — static; раз уж мы работаем с синглтоном. Меньше потом писать — чище код.
                0
                неочень понял зачем так делать, вы попробуйте, будет ли у вас работать?
                0
                1) Синглтон и свойство dataArray. Массив никак не освобождается, при каждом запросе данные добавляются и передаются из массива в комплишен. Рано или поздно, приложение ляжет намертво или будет ужасно тормозить. Это первый момент.

                2) Второй момент. У вас подписка на тиковую историю, причем подписка не унифицирована и как явное значение передается в каждом методе. К примеру, если получать информацию по сотне инструментов, придется создать сотню методов на подписку и столько же на отписку. Почему не использовать дженерик и не унифицировать запрос?

                3) Вы никак не обрабатываете обрыв соединения. С учетом того, что в случае синглтона, менеджер был инициирован однажды, по сути, он не может быть пересоздан. Я бы переписал пример без «вечного» объекта и эту строку вынес бы в конструктор:

                let webSocketTask = URLSession(configuration: .default).webSocketTask(with: URL(string: "wss://ТУТ_ВАШ_АДРЕС"*))

                4) Метод getData() содержит вот такой гард:

                guard let self = self else { return }

                Зачем? Вызов произошел после загрузки вью. По сути, контроллер загружен и работает нормально. Внутри метода идет простая передача массива в свойство вашего контроллера.

                self?.dataArray = ...
                прекрасно отработает внутри метода.
                  0
                  Осмелюсь предположить, что вызов guard let self = self else { return } в данном случае – это чисто синтаксический сахар, визуальный вид и «потому, что так книга пишет». :)
                  0
                  А это под любыми версиями iOS работает? Мы пользовали cocoapods версию для вебсокетов как раз потому, что Apple только недавно добавила поддержку сокетов в свой SDK.
                    0
                    Нет. Только для ios 13. Для предыдущих версий хорошо работает Starscream.
                    0
                    Самое интересное в Эппл реализации это то что надо в рекурсии вызывать receiveData(), но это надо делать только on .success.

                    Я оставлю здесь пару ссылок где более развёрнуто описано как работать с новыми веб-сокетами:
                    1. appspector.com/blog/websockets-in-ios-using-urlsessionwebsockettask
                    2. kristaps.me/websockets-ios-13-swift

                    И понятно что это только для iOS 13. У меня вопрос, кто-нибудь пробовал использовать комбинацию Starscream подключаемую как динамическую библиотеку для iOS 12- и встроенный для iOS13+. Есть какая-то выгода? По идеи памяти должно меньше кушать, ведь «все» системные либы динамические и скорее всего уже в памяти (случай для iOS13). Сама реализация думаю всем понятна, что надо написать обёртку для вебсокетов и менеджер который выбирает необходимую реализацию в зависимости от версии iOS.
                      0
                      Весь код сюда пихать не стану, лишь часть примера ради
                      Буквально на днях пилил себе такой менеджер.
                      Только для нашего сервера пришлось его сделать не на уровне простой URL, а через URL Request. Так появилась возможность использовать хеддер авторизации и дополнительный query в URL:
                      final class WebSocket: NSObject {
                          private var urlSession: URLSession?
                          private var webSocketTask: URLSessionWebSocketTask?
                      
                          init?(host: String, path: String, headers: [String: String], parameters: [String: String]) {
                              super.init()
                              urlSession = URLSession(configuration: .default,
                                                      delegate: self,
                                                      delegateQueue: nil)
                              
                              var urlComponents = URLComponents()
                              urlComponents.scheme = "wss"
                              urlComponents.host = host
                              urlComponents.path = path
                              
                              var items: [URLQueryItem] = []
                              
                              for (key, value) in parameters {
                                  let qItem = URLQueryItem(name: key, value: value)
                                  items.append(qItem)
                              }
                              
                              if items.count > 0  {
                                  urlComponents.queryItems = items
                              }
                              
                              guard let url = urlComponents.url else {
                                  log.error("Could not create URL from components")
                                  return nil
                              }
                              
                              log.info("Socket URL: \(url.absoluteString)")
                              
                              var request = URLRequest(url: url)
                              request.timeoutInterval = 30
                              headers.forEach { (key, value) in
                                  request.addValue(value, forHTTPHeaderField: key)
                              }
                              
                              webSocketTask = urlSession?.webSocketTask(with: request)
                          }
                      }


                      Плюс, для удобства сделал все на колбэках вместо делегата, в итоге конечное использование выглядит примерно так:
                      let websocket = WebSocket(host: server,
                                                    path: "/dash_wss",
                                                    headers: headers,
                                                    parameters: parameters)
                              
                              websocket?
                                  .connect()
                                  .onConnected(callback: {
                                      log.info("Socket connected")
                                  })
                                  .onDisconnected(callback: { (error) in
                                      log.info("Socket disconnected with error: \(error?.localizedDescription ?? "No error")")
                                  })
                                  .onError(callback: { (error) in
                                      log.error("Socket error \(error)")
                                  })
                                  .onTextMessage(callack: { (message) in
                                      log.info("Socket message \(message)")
                                  })


                      Делать синглтон из него не стал, так как используется только для одного экрана
                      Если кому надо полный код, могу запилить в гитхабе.
                      Если будут замечания, корректировки, буду рад принять

                      Only users with full accounts can post comments. Log in, please.