[Разработка под iOS, Разработка мобильных приложений, Swift] Чаты на вебсокетах в iOS, если у вас WAMP
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Разработка заняла примерно 9 месяцев, а я занимался реализацией клиент-серверного общения по сокету для iOS. Особенности нашей ситуации:
- Поддержка старых версий iOS, где нативных методов для общения по сокетам ещё не было — пришлось искать рабочую библиотеку и фиксить баги.
- Протокол WAMP на бэкенде — предстояло научить клиент декодировать any, декодировать протоколы и создать объект, который отвечает за отправку и приём сообщений.
Примечание: описанные ниже способы декодирования, можно применить и в других задачах.
Поиск живой библиотеки
Нативные методы для работы с вебсокетами были еще в iOS 11, а с версии iOS 13 они стали даже удобными. Но мы хотели сделать рабочий чат для всех с версии iOS 9, а уже потом потихоньку убирать поддержку. На это было несколько причин, в том числе и то, что у наших пользователей в США довольно много старых версий операционной системы.
Писать поддержку вебсокетов, начиная с iOS 9, самостоятельно было бы слишком долго, поэтому решил искать готовую библиотеку. Выбирал по популярности и количеству заведённых тикетов, в итоге остановился на Starscream. Но и она оказалась с проблемами — у библиотеки до сих пор висят десятки открытых тикетов и, по сравнению с нативным решением в iOS 13, она очень объёмная.
Смотрел ещё в сторону Socket.IO — самую большую из конкурентов — там меньше форков и лайков, но при этом в несколько раз больше незакрытых вопросов.
Starscream, в свою очередь, вызывала баги, и её пришлось форкать. Вот две основные проблемы, с которыми мы столкнулись (они обнаружились не сразу, а только в процессе тестирования, когда появилось больше данных):
- Отсутствие события на таймаут. Из-за этого были сложности с переподключением к сокету после восстановления интернет-соединения.
- Неверные очереди вызова коллбеков.
Ещё несколько багов:
- Коллбек не получал ошибку в ряде случаев.
- В некоторых местах был возврат функций без вызова коллбека по неизвестной причине.
- Ошибка компиляции на XCode 12. Прямо в процессе разработки вышел новый XCode, а библиотека не обновилась. Мы немного подождали, но в итоге пришлось фиксить самостоятельно каст к NSString.
Работа с WAMP
Как отправлять и получать данные было понятно, и пошла более высокоуровневая работа. Бэкенд выбрал протокол общения клиента с сервером WAMP, но для клиента он не работает из коробки, пришлось допиливать.
Минусы протокола:
– Кодирование всех типов событий в числах (PUBLISH — это 16, SUBSCRIBE — 32 и так далее), что сильно усложняет чтение логов для разработки и QA (пойди, сразу догадайся, что значит прилетевшее сообщение [33,11,5862354]).
– Механизм подписок на события (например, новые сообщения в чат или обновление количества участников) реализован через получение от бэкенда уникального id подписки, который ещё надо где-то хранить и ни в коем случае не терять во избежание утечек.
Плюсы:
+ Радует, что протокол за вас предусматривает всё или почти всё. Это облегчает взаимодействие разработчиков клиентской части и бэкенда.
Чтобы довести его до ума, передо мной стояли три основные задачи:
- Научиться кодировать и декодировать any. У нас клиент-серверное общение в JSON-формате и сервер присылает/получает сообщения, которые стандартно не декодируются.
- Кодировать и декодировать протоколы.
- Создать объект, который будет отвечать за отправку и приём сообщений.
Первые два пункта в большей мере описывают способы декодирования, которые, можно применить в других задачах.
Рассмотрим на примерах. Допустим, мы получаем от сервера сообщение:
[36, 1, 2, {}]
Расшифровываем согласно этому документу и получаем:
[EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict]
Как декодировать
Сначала преобразуем JSON в [Any], для этого написано расширение для UnkeyedDecodingContainer. Так выглядит использование:
var container = try decoder.unkeyedContainer()
var array = try container.decode([Any].self)
Из массива нужно убрать первый элемент, определить по нему тип сообщения (в данном случае это EventMessage) и преобразовать его в decoder для декодирования уже готовым инициализатором протокола Decodable.
Первый элемент массива всегда является числом, которое нужно связать с конкретным типом сообщения. Так выглядит enum для связи числа и типа:
enum WampMessageDecodableType: Int, Decodable, CaseIterable {
case event = 36
private var messageType: WampMessageDecodable.Type {
switch self {
case .event: return EventMessage.self
}
}
var factory: (Decoder) throws -> WampMessageDecodable {
messageType.init(from:)
}
}
Преобразуем число в соответствующий ему тип:
guard let typeValue = array.removeFirst() as? Int,
let messageType = WampMessageDecodableType(rawValue: typeValue)
else { throw Self.typeDecodingError }
Так как у массива нет ключей, а типы в нём разные, нужно преобразовать его в словарь, где ключом будет являться его индекс:
let data = try JSONSerialization.data(withJSONObject: array.indexToKeyDictionary)
Где indexToKeyDictionary кастомное расширение. Результат:
[1, 2, {}] => {0: 1, 1: 2, 3: {}}
Опишем расширение и структуру для получения декодера из Data:
struct DecoderHolder: Decodable {
let decoder: Decoder
init(from decoder: Decoder) throws {
self.decoder = decoder
}
}
extension JSONDecoder {
func getDecoder(from data: Data) throws -> Decoder {
try decode(DecoderHolder.self, from: data).decoder
}
}
Теперь получаем декодер и декодируем сообщение:
let keyDecoder = try JSONDecoder.defaultDecoder.getDecoder(from: data)
message = try messageType.factory(keyDecoder)
Чтобы декодирование работало с целочисленными ключами, используем CodingKeys с типом Int и расширение:
extension CodingKey where Self: RawRepresentable, RawValue == Int {
var stringValue: String {
.init(intValue ?? .min)
}
}
Теперь сообщения можно декодировать стандартным декодером и не описывать дополнительный маппинг. Так, например, выглядит WelcomeMessage:
struct WelcomeMessage: WampMessageDecodable {
let sessionId: Int
let details: WelcomeDetails
enum CodingKeys: Int, CodingKey {
case sessionId, details
}
}
C EventMessage немного сложнее, так как внутри он также содержит протокол. Декодируем его через вспомогательную структуру:
struct TypeHolder<T: Decodable>: Decodable {
let type: T
}
Так выглядит упрощённый EventMessage:
public protocol EventEntry: Decodable { }
struct EventMessage: WampMessageDecodable, SubscriptionIdProvidable {
let event: EventEntry
enum CodingKeys: Int, CodingKey {
case event
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let superDecoder = try container.superDecoder(forKey: .event)
let typeHolder: TypeHolder<EventMessageType> = try container.decode(forKey: .event)
event = try typeHolder.type.factory(superDecoder)
}
}
Где EventMessageType аналогичен WampMessageDecodableType:
enum EventMessageType: Int, Codable, CaseIterable {
case chatListUpdate = 100
private var messageType: EventEntry.Type {
switch self {
case .chatListUpdate: return ChatListUpdateEventEntry.self
}
}
var factory: (Decoder) throws -> EventEntry {
messageType.init(from:)
}
}
Рассмотрим ещё один пример декодирования ResultMessage. Так выглядит сообщение:
[50, 7814135, {}, [], {"userid": 123, "karma": 10}]
Сложность в том, что ResultMessage не может быть дженериком, так как тип ответа неизвестен заранее, и в зависимости от конкретного запроса возвращаемое значение (объект по третьему индексу) будет отличаться.
Решение состоит в том, чтобы сохранить декодер и использовать его позже, когда тип будет определён.
struct ResultMessage: WampMessageDecodable, RequestIdProvidable {
private var resultDecoder: Decoder
enum CodingKeys: Int, CodingKey {
case resultDecoder = 3
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
resultDecoder = try container.superDecoder(forKey: .resultDecoder)
}
}
extension ResultMessage {
func decodeResult<T: Decodable>() throws -> T {
try T.init(from: resultDecoder)
}
}
Кодирование происходит по схожим принципам и отличается незначительно.
Общение с сокетом
Объект WebsocketSessionImpl инкапсулирует в себе создание и разрыв соединения с сокетом, менеджмент получения и отправку сообщений, а также держит массив делегатов со слабыми ссылками. Когда делегаты деинициализируются, то соединение с сокетом закрывается с небольшой задержкой.
Подключение к сокету достаточно простое. Получаем handshake от первого делегата и коннектимся:
guard let request = firstDelegate?.handshakeRequest
else { return }
socket = .init(request: request)
socket?.delegate = self
socket?.connect()
WebsocketSessionImpl сохраняет коллбеки запросов для ивентов и подписок. И вызывает их при получении ответа.
Так выглядят сигнатуры главных методов общение с WebsocketSessionImpl:
func call<T: CallRequest>(
request: T,
completion: @escaping (Result<T.ResultType, Error>) -> Void
)
func subscribe<T: EventRequest>(
request: T,
completion: @escaping (Result<UUID, Error>) -> Void,
onEvent: @escaping (Result<T.ResultType, Error>) -> Void
)
func unsubscribe(
id: UUID,
completion: @escaping (Result<Void, Error>) -> Void
)
func publish<T: PublishRequest>(
request: T,
completion: @escaping (Result<Void, Error>) -> Void
)
Call — похож на get-запрос; publish — это post; subscribe — подписка на событие (например, получение нового сообщение из чата); unsubscribe — отписка от события. Результатом всех этих методов является инициализация сообщения определённого типа и отправка его в JSON-формате.
При этом коллбеки под обёрткой сохраняются в словарь и ожидают ответа:
private var results: [RequestId: SaveableCompletion] = [:]
Аналогично для событий:
private var events: [SubscriptionEvent: SaveableCompletion] = [:]
Подписка только одна, а внутри приложения разные модули могут подписываться на одно и то же событие. Поэтому при наличии подписки создается её копия с другим коллбеком:
private struct SubscriptionEvent: Hashable {
let id: Int
let localId = UUID()
let topic: TopicModel
func copy() -> Self {
.init(id: id, topic: topic)
}
}
При получении текста из сокета, вызывается метод:
func handleText(_ text: String) {
let data = try text.data(using: .utf8)
let message = try JSONDecoder.defaultDecoder.decode(WampBaseDecodableMessage.self, from: data).message
handleMessage(message)
}
Затем сообщение (в зависимости от типа) обрабатывается:
func handleMessage(_ message: WampMessageDecodable) {
switch message {
case _ as ChallengeMessage:
sendAuthenticateMessage()
case _ as WelcomeMessage:
isConnected = true
case let message as ErrorMessage:
handleErrorMessage(message)
case let message as RequestIdProvidable & WampMessageDecodable:
informThatRecieved(message)
case let message as SubscriptionIdProvidable & WampMessageDecodable:
informSubscriber(message)
default:
break
}
}
В качестве примера, отправка publish-сообщения:
public func publish<T: PublishRequest>(request: T, completion: @escaping (Result<Void, Error>) -> Void) {
let id = nextId
results[id] = .init(respondTo: PublishedMessage.self, completion)
let message = factory.publish(requestId: id, request: request)
write(message: message, with: completion)
}
func write<T>(message: WampMessageEncodable, with completion: @escaping (Result<T, Error>) -> Void) {
do {
try writeToSocket(message: message, with: { error in
guard let error = error else { return }
completion(.failure(error))
})
} catch let error {
completion(.failure(error))
}
}
func writeToSocket(message: WampMessageEncodable, with completion: ((Error?) -> Void)?) throws {
let baseMessage = try WampBaseEncodableMessage(message:message)
let data = try JSONEncoder.defaultEncoder.encode(baseMessage)
guard let text = String(data: data, encoding: .utf8)
else { return }
socket?.write(string: text, completion: completion)
}
Вместо заключения
Остальная реализация чата — это отдельная история, логика во многом переписывалась, а UI уже был написан, но всё это не касается сокета. Что же касается реализации клиент-серверного общения на Android с учетом WAMP — об этом в другой раз.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений, Дизайн мобильных приложений, Дизайн] Как мы делаем страховое приложение для людей
- [Разработка мобильных приложений, Проектирование и рефакторинг, Компиляторы, Swift, Управление проектами] Как Uber переписал приложение iOS на Swift (перевод)
- [Производство и разработка электроники, Смартфоны, IT-компании] В Индии на фабрике по сборке iPhone сотрудники устроили погром из-за неполной выплаты им зарплаты
- [Разработка веб-сайтов, Google Chrome, Браузеры, Тестирование веб-сервисов] Обходим проверку сертификата SSL
- [Разработка игр, Игры и игровые приставки] Бесплатный онлайн-круглый стол «Тенденции игрового рынка 2021. Какие игры делать в новом году»
- [Разработка игр, Игры и игровые приставки, IT-компании] Руководство CD PROJEKT RED взяло на себя ответственность за баги на старте Cyberpunk 2077. Сотрудники получат все бонусы
- [Разработка под iOS, AR и VR, IT-компании] Инженеры Apple консультируют китайских работников с помощью iPad с дополненной реальностью
- [Производство и разработка электроники, Энергия и элементы питания, Электроника для начинающих] Это просто бомба-2. Li-Ion — как не взлететь
- [Разработка игр] Баланс в настольном геймдизайне: строим графы с помощью Google App Script и Gephi
- [Dart, Конференции, Flutter] Онлайн-конференция DartUP 2020: так же лампово, как в офлайне. Отчёт о событии глазами Surf
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_swift, #_ios, #_klientserver (клиент-сервер), #_swift, #_wamp, #_dekodirovanie (декодирование), #_protokol (протокол), #_mobilnaja (мобильная), #_razrabotka (разработка), #_chat (чат), #_sokety (сокеты), #_biblioteka (библиотека), #_blog_kompanii_funcorp (
Блог компании FunCorp
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_swift
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 10:22
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Разработка заняла примерно 9 месяцев, а я занимался реализацией клиент-серверного общения по сокету для iOS. Особенности нашей ситуации:
Примечание: описанные ниже способы декодирования, можно применить и в других задачах. Поиск живой библиотеки Нативные методы для работы с вебсокетами были еще в iOS 11, а с версии iOS 13 они стали даже удобными. Но мы хотели сделать рабочий чат для всех с версии iOS 9, а уже потом потихоньку убирать поддержку. На это было несколько причин, в том числе и то, что у наших пользователей в США довольно много старых версий операционной системы. Писать поддержку вебсокетов, начиная с iOS 9, самостоятельно было бы слишком долго, поэтому решил искать готовую библиотеку. Выбирал по популярности и количеству заведённых тикетов, в итоге остановился на Starscream. Но и она оказалась с проблемами — у библиотеки до сих пор висят десятки открытых тикетов и, по сравнению с нативным решением в iOS 13, она очень объёмная. Смотрел ещё в сторону Socket.IO — самую большую из конкурентов — там меньше форков и лайков, но при этом в несколько раз больше незакрытых вопросов. Starscream, в свою очередь, вызывала баги, и её пришлось форкать. Вот две основные проблемы, с которыми мы столкнулись (они обнаружились не сразу, а только в процессе тестирования, когда появилось больше данных):
Ещё несколько багов:
Работа с WAMP Как отправлять и получать данные было понятно, и пошла более высокоуровневая работа. Бэкенд выбрал протокол общения клиента с сервером WAMP, но для клиента он не работает из коробки, пришлось допиливать. Минусы протокола: – Кодирование всех типов событий в числах (PUBLISH — это 16, SUBSCRIBE — 32 и так далее), что сильно усложняет чтение логов для разработки и QA (пойди, сразу догадайся, что значит прилетевшее сообщение [33,11,5862354]). – Механизм подписок на события (например, новые сообщения в чат или обновление количества участников) реализован через получение от бэкенда уникального id подписки, который ещё надо где-то хранить и ни в коем случае не терять во избежание утечек. Плюсы: + Радует, что протокол за вас предусматривает всё или почти всё. Это облегчает взаимодействие разработчиков клиентской части и бэкенда. Чтобы довести его до ума, передо мной стояли три основные задачи:
Первые два пункта в большей мере описывают способы декодирования, которые, можно применить в других задачах. Рассмотрим на примерах. Допустим, мы получаем от сервера сообщение: [36, 1, 2, {}]
Расшифровываем согласно этому документу и получаем: [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict]
Как декодировать Сначала преобразуем JSON в [Any], для этого написано расширение для UnkeyedDecodingContainer. Так выглядит использование: var container = try decoder.unkeyedContainer()
var array = try container.decode([Any].self) Из массива нужно убрать первый элемент, определить по нему тип сообщения (в данном случае это EventMessage) и преобразовать его в decoder для декодирования уже готовым инициализатором протокола Decodable. Первый элемент массива всегда является числом, которое нужно связать с конкретным типом сообщения. Так выглядит enum для связи числа и типа: enum WampMessageDecodableType: Int, Decodable, CaseIterable {
case event = 36 private var messageType: WampMessageDecodable.Type { switch self { case .event: return EventMessage.self } } var factory: (Decoder) throws -> WampMessageDecodable { messageType.init(from:) } } Преобразуем число в соответствующий ему тип: guard let typeValue = array.removeFirst() as? Int,
let messageType = WampMessageDecodableType(rawValue: typeValue) else { throw Self.typeDecodingError } Так как у массива нет ключей, а типы в нём разные, нужно преобразовать его в словарь, где ключом будет являться его индекс: let data = try JSONSerialization.data(withJSONObject: array.indexToKeyDictionary)
Где indexToKeyDictionary кастомное расширение. Результат: [1, 2, {}] => {0: 1, 1: 2, 3: {}}
Опишем расширение и структуру для получения декодера из Data: struct DecoderHolder: Decodable {
let decoder: Decoder init(from decoder: Decoder) throws { self.decoder = decoder } } extension JSONDecoder { func getDecoder(from data: Data) throws -> Decoder { try decode(DecoderHolder.self, from: data).decoder } } Теперь получаем декодер и декодируем сообщение: let keyDecoder = try JSONDecoder.defaultDecoder.getDecoder(from: data)
message = try messageType.factory(keyDecoder) Чтобы декодирование работало с целочисленными ключами, используем CodingKeys с типом Int и расширение: extension CodingKey where Self: RawRepresentable, RawValue == Int {
var stringValue: String { .init(intValue ?? .min) } } Теперь сообщения можно декодировать стандартным декодером и не описывать дополнительный маппинг. Так, например, выглядит WelcomeMessage: struct WelcomeMessage: WampMessageDecodable {
let sessionId: Int let details: WelcomeDetails enum CodingKeys: Int, CodingKey { case sessionId, details } } C EventMessage немного сложнее, так как внутри он также содержит протокол. Декодируем его через вспомогательную структуру: struct TypeHolder<T: Decodable>: Decodable {
let type: T } Так выглядит упрощённый EventMessage: public protocol EventEntry: Decodable { }
struct EventMessage: WampMessageDecodable, SubscriptionIdProvidable { let event: EventEntry enum CodingKeys: Int, CodingKey { case event } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let superDecoder = try container.superDecoder(forKey: .event) let typeHolder: TypeHolder<EventMessageType> = try container.decode(forKey: .event) event = try typeHolder.type.factory(superDecoder) } } Где EventMessageType аналогичен WampMessageDecodableType: enum EventMessageType: Int, Codable, CaseIterable {
case chatListUpdate = 100 private var messageType: EventEntry.Type { switch self { case .chatListUpdate: return ChatListUpdateEventEntry.self } } var factory: (Decoder) throws -> EventEntry { messageType.init(from:) } } Рассмотрим ещё один пример декодирования ResultMessage. Так выглядит сообщение: [50, 7814135, {}, [], {"userid": 123, "karma": 10}]
Сложность в том, что ResultMessage не может быть дженериком, так как тип ответа неизвестен заранее, и в зависимости от конкретного запроса возвращаемое значение (объект по третьему индексу) будет отличаться. Решение состоит в том, чтобы сохранить декодер и использовать его позже, когда тип будет определён. struct ResultMessage: WampMessageDecodable, RequestIdProvidable {
private var resultDecoder: Decoder enum CodingKeys: Int, CodingKey { case resultDecoder = 3 } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) resultDecoder = try container.superDecoder(forKey: .resultDecoder) } } extension ResultMessage { func decodeResult<T: Decodable>() throws -> T { try T.init(from: resultDecoder) } } Кодирование происходит по схожим принципам и отличается незначительно. Общение с сокетом Объект WebsocketSessionImpl инкапсулирует в себе создание и разрыв соединения с сокетом, менеджмент получения и отправку сообщений, а также держит массив делегатов со слабыми ссылками. Когда делегаты деинициализируются, то соединение с сокетом закрывается с небольшой задержкой. Подключение к сокету достаточно простое. Получаем handshake от первого делегата и коннектимся: guard let request = firstDelegate?.handshakeRequest
else { return } socket = .init(request: request) socket?.delegate = self socket?.connect() WebsocketSessionImpl сохраняет коллбеки запросов для ивентов и подписок. И вызывает их при получении ответа. Так выглядят сигнатуры главных методов общение с WebsocketSessionImpl: func call<T: CallRequest>(
request: T, completion: @escaping (Result<T.ResultType, Error>) -> Void ) func subscribe<T: EventRequest>( request: T, completion: @escaping (Result<UUID, Error>) -> Void, onEvent: @escaping (Result<T.ResultType, Error>) -> Void ) func unsubscribe( id: UUID, completion: @escaping (Result<Void, Error>) -> Void ) func publish<T: PublishRequest>( request: T, completion: @escaping (Result<Void, Error>) -> Void ) Call — похож на get-запрос; publish — это post; subscribe — подписка на событие (например, получение нового сообщение из чата); unsubscribe — отписка от события. Результатом всех этих методов является инициализация сообщения определённого типа и отправка его в JSON-формате. При этом коллбеки под обёрткой сохраняются в словарь и ожидают ответа: private var results: [RequestId: SaveableCompletion] = [:]
Аналогично для событий: private var events: [SubscriptionEvent: SaveableCompletion] = [:]
Подписка только одна, а внутри приложения разные модули могут подписываться на одно и то же событие. Поэтому при наличии подписки создается её копия с другим коллбеком: private struct SubscriptionEvent: Hashable {
let id: Int let localId = UUID() let topic: TopicModel func copy() -> Self { .init(id: id, topic: topic) } } При получении текста из сокета, вызывается метод: func handleText(_ text: String) {
let data = try text.data(using: .utf8) let message = try JSONDecoder.defaultDecoder.decode(WampBaseDecodableMessage.self, from: data).message handleMessage(message) } Затем сообщение (в зависимости от типа) обрабатывается: func handleMessage(_ message: WampMessageDecodable) {
switch message { case _ as ChallengeMessage: sendAuthenticateMessage() case _ as WelcomeMessage: isConnected = true case let message as ErrorMessage: handleErrorMessage(message) case let message as RequestIdProvidable & WampMessageDecodable: informThatRecieved(message) case let message as SubscriptionIdProvidable & WampMessageDecodable: informSubscriber(message) default: break } } В качестве примера, отправка publish-сообщения: public func publish<T: PublishRequest>(request: T, completion: @escaping (Result<Void, Error>) -> Void) {
let id = nextId results[id] = .init(respondTo: PublishedMessage.self, completion) let message = factory.publish(requestId: id, request: request) write(message: message, with: completion) } func write<T>(message: WampMessageEncodable, with completion: @escaping (Result<T, Error>) -> Void) { do { try writeToSocket(message: message, with: { error in guard let error = error else { return } completion(.failure(error)) }) } catch let error { completion(.failure(error)) } } func writeToSocket(message: WampMessageEncodable, with completion: ((Error?) -> Void)?) throws { let baseMessage = try WampBaseEncodableMessage(message:message) let data = try JSONEncoder.defaultEncoder.encode(baseMessage) guard let text = String(data: data, encoding: .utf8) else { return } socket?.write(string: text, completion: completion) } Вместо заключения Остальная реализация чата — это отдельная история, логика во многом переписывалась, а UI уже был написан, но всё это не касается сокета. Что же касается реализации клиент-серверного общения на Android с учетом WAMP — об этом в другой раз. =========== Источник: habr.com =========== Похожие новости:
Блог компании FunCorp ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_swift |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 10:22
Часовой пояс: UTC + 5