SocketManager.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. //
  2. // Created by Erik Little on 10/14/17.
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to deal
  6. // in the Software without restriction, including without limitation the rights
  7. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. // copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. // THE SOFTWARE.
  21. import Dispatch
  22. import Foundation
  23. ///
  24. /// A manager for a socket.io connection.
  25. ///
  26. /// A `SocketManager` is responsible for multiplexing multiple namespaces through a single `SocketEngineSpec`.
  27. ///
  28. /// Example:
  29. ///
  30. /// ```swift
  31. /// let manager = SocketManager(socketURL: URL(string:"http://localhost:8080/")!)
  32. /// let defaultNamespaceSocket = manager.defaultSocket
  33. /// let swiftSocket = manager.socket(forNamespace: "/swift")
  34. ///
  35. /// // defaultNamespaceSocket and swiftSocket both share a single connection to the server
  36. /// ```
  37. ///
  38. /// Sockets created through the manager are retained by the manager. So at the very least, a single strong reference
  39. /// to the manager must be maintained to keep sockets alive.
  40. ///
  41. /// To disconnect a socket and remove it from the manager, either call `SocketIOClient.disconnect()` on the socket,
  42. /// or call one of the `disconnectSocket` methods on this class.
  43. ///
  44. /// **NOTE**: The manager is not thread/queue safe, all interaction with the manager should be done on the `handleQueue`
  45. ///
  46. open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDataBufferable, ConfigSettable {
  47. private static let logType = "SocketManager"
  48. // MARK: Properties
  49. /// The socket associated with the default namespace ("/").
  50. public var defaultSocket: SocketIOClient {
  51. return socket(forNamespace: "/")
  52. }
  53. /// The URL of the socket.io server.
  54. ///
  55. /// If changed after calling `init`, `forceNew` must be set to `true`, or it will only connect to the url set in the
  56. /// init.
  57. public let socketURL: URL
  58. /// The configuration for this client.
  59. ///
  60. /// **Some configs will not take affect until after a reconnect if set after calling a connect method**.
  61. public var config: SocketIOClientConfiguration {
  62. get {
  63. return _config
  64. }
  65. set {
  66. if status.active {
  67. DefaultSocketLogger.Logger.log("Setting configs on active manager. Some configs may not be applied until reconnect",
  68. type: SocketManager.logType)
  69. }
  70. setConfigs(newValue)
  71. }
  72. }
  73. /// The engine for this manager.
  74. public var engine: SocketEngineSpec?
  75. /// If `true` then every time `connect` is called, a new engine will be created.
  76. public var forceNew = false
  77. /// The queue that all interaction with the client should occur on. This is the queue that event handlers are
  78. /// called on.
  79. ///
  80. /// **This should be a serial queue! Concurrent queues are not supported and might cause crashes and races**.
  81. public var handleQueue = DispatchQueue.main
  82. /// The sockets in this manager indexed by namespace.
  83. public var nsps = [String: SocketIOClient]()
  84. /// If `true`, this client will try and reconnect on any disconnects.
  85. public var reconnects = true
  86. /// The minimum number of seconds to wait before attempting to reconnect.
  87. public var reconnectWait = 10
  88. /// The maximum number of seconds to wait before attempting to reconnect.
  89. public var reconnectWaitMax = 30
  90. /// The randomization factor for calculating reconnect jitter.
  91. public var randomizationFactor = 0.5
  92. /// The status of this manager.
  93. public private(set) var status: SocketIOStatus = .notConnected {
  94. didSet {
  95. switch status {
  96. case .connected:
  97. reconnecting = false
  98. currentReconnectAttempt = 0
  99. default:
  100. break
  101. }
  102. }
  103. }
  104. /// A list of packets that are waiting for binary data.
  105. ///
  106. /// The way that socket.io works all data should be sent directly after each packet.
  107. /// So this should ideally be an array of one packet waiting for data.
  108. ///
  109. /// **This should not be modified directly.**
  110. public var waitingPackets = [SocketPacket]()
  111. private(set) var reconnectAttempts = -1
  112. private var _config: SocketIOClientConfiguration
  113. private var currentReconnectAttempt = 0
  114. private var reconnecting = false
  115. // MARK: Initializers
  116. /// Type safe way to create a new SocketIOClient. `opts` can be omitted.
  117. ///
  118. /// - parameter socketURL: The url of the socket.io server.
  119. /// - parameter config: The config for this socket.
  120. public init(socketURL: URL, config: SocketIOClientConfiguration = []) {
  121. self._config = config
  122. self.socketURL = socketURL
  123. super.init()
  124. setConfigs(_config)
  125. }
  126. /// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity.
  127. /// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set<SocketIOClientOption>)`
  128. ///
  129. /// - parameter socketURL: The url of the socket.io server.
  130. /// - parameter config: The config for this socket.
  131. @objc
  132. public convenience init(socketURL: URL, config: [String: Any]?) {
  133. self.init(socketURL: socketURL, config: config?.toSocketConfiguration() ?? [])
  134. }
  135. /// :nodoc:
  136. deinit {
  137. DefaultSocketLogger.Logger.log("Manager is being released", type: SocketManager.logType)
  138. engine?.disconnect(reason: "Manager Deinit")
  139. }
  140. // MARK: Methods
  141. private func addEngine() {
  142. DefaultSocketLogger.Logger.log("Adding engine", type: SocketManager.logType)
  143. engine?.engineQueue.sync {
  144. self.engine?.client = nil
  145. // Close old engine so it will not leak because of URLSession if in polling mode
  146. self.engine?.disconnect(reason: "Adding new engine")
  147. }
  148. engine = SocketEngine(client: self, url: socketURL, config: config)
  149. }
  150. /// Connects the underlying transport and the default namespace socket.
  151. ///
  152. /// Override if you wish to attach a custom `SocketEngineSpec`.
  153. open func connect() {
  154. guard !status.active else {
  155. DefaultSocketLogger.Logger.log("Tried connecting an already active socket", type: SocketManager.logType)
  156. return
  157. }
  158. if engine == nil || forceNew {
  159. addEngine()
  160. }
  161. status = .connecting
  162. engine?.connect()
  163. }
  164. /// Connects a socket through this manager's engine.
  165. ///
  166. /// - parameter socket: The socket who we should connect through this manager.
  167. open func connectSocket(_ socket: SocketIOClient) {
  168. guard status == .connected else {
  169. DefaultSocketLogger.Logger.log("Tried connecting socket when engine isn't open. Connecting",
  170. type: SocketManager.logType)
  171. connect()
  172. return
  173. }
  174. engine?.send("0\(socket.nsp),", withData: [])
  175. }
  176. /// Called when the manager has disconnected from socket.io.
  177. ///
  178. /// - parameter reason: The reason for the disconnection.
  179. open func didDisconnect(reason: String) {
  180. forAll {socket in
  181. socket.didDisconnect(reason: reason)
  182. }
  183. }
  184. /// Disconnects the manager and all associated sockets.
  185. open func disconnect() {
  186. DefaultSocketLogger.Logger.log("Manager closing", type: SocketManager.logType)
  187. status = .disconnected
  188. engine?.disconnect(reason: "Disconnect")
  189. }
  190. /// Disconnects the given socket.
  191. ///
  192. /// This will remove the socket for the manager's control, and make the socket instance useless and ready for
  193. /// releasing.
  194. ///
  195. /// - parameter socket: The socket to disconnect.
  196. open func disconnectSocket(_ socket: SocketIOClient) {
  197. engine?.send("1\(socket.nsp),", withData: [])
  198. socket.didDisconnect(reason: "Namespace leave")
  199. }
  200. /// Disconnects the socket associated with `forNamespace`.
  201. ///
  202. /// This will remove the socket for the manager's control, and make the socket instance useless and ready for
  203. /// releasing.
  204. ///
  205. /// - parameter nsp: The namespace to disconnect from.
  206. open func disconnectSocket(forNamespace nsp: String) {
  207. guard let socket = nsps.removeValue(forKey: nsp) else {
  208. DefaultSocketLogger.Logger.log("Could not find socket for \(nsp) to disconnect",
  209. type: SocketManager.logType)
  210. return
  211. }
  212. disconnectSocket(socket)
  213. }
  214. /// Sends a client event to all sockets in `nsps`
  215. ///
  216. /// - parameter clientEvent: The event to emit.
  217. open func emitAll(clientEvent event: SocketClientEvent, data: [Any]) {
  218. forAll {socket in
  219. socket.handleClientEvent(event, data: data)
  220. }
  221. }
  222. /// Sends an event to the server on all namespaces in this manager.
  223. ///
  224. /// - parameter event: The event to send.
  225. /// - parameter items: The data to send with this event.
  226. open func emitAll(_ event: String, _ items: SocketData...) {
  227. guard let emitData = try? items.map({ try $0.socketRepresentation() }) else {
  228. DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
  229. type: SocketManager.logType)
  230. return
  231. }
  232. emitAll(event, withItems: emitData)
  233. }
  234. /// Sends an event to the server on all namespaces in this manager.
  235. ///
  236. /// Same as `emitAll(_:_:)`, but meant for Objective-C.
  237. ///
  238. /// - parameter event: The event to send.
  239. /// - parameter items: The data to send with this event.
  240. open func emitAll(_ event: String, withItems items: [Any]) {
  241. forAll {socket in
  242. socket.emit(event, with: items, completion: nil)
  243. }
  244. }
  245. /// Called when the engine closes.
  246. ///
  247. /// - parameter reason: The reason that the engine closed.
  248. open func engineDidClose(reason: String) {
  249. handleQueue.async {
  250. self._engineDidClose(reason: reason)
  251. }
  252. }
  253. private func _engineDidClose(reason: String) {
  254. waitingPackets.removeAll()
  255. if status != .disconnected {
  256. status = .notConnected
  257. }
  258. if status == .disconnected || !reconnects {
  259. didDisconnect(reason: reason)
  260. } else if !reconnecting {
  261. reconnecting = true
  262. tryReconnect(reason: reason)
  263. }
  264. }
  265. /// Called when the engine errors.
  266. ///
  267. /// - parameter reason: The reason the engine errored.
  268. open func engineDidError(reason: String) {
  269. handleQueue.async {
  270. self._engineDidError(reason: reason)
  271. }
  272. }
  273. private func _engineDidError(reason: String) {
  274. DefaultSocketLogger.Logger.error("\(reason)", type: SocketManager.logType)
  275. emitAll(clientEvent: .error, data: [reason])
  276. }
  277. /// Called when the engine opens.
  278. ///
  279. /// - parameter reason: The reason the engine opened.
  280. open func engineDidOpen(reason: String) {
  281. handleQueue.async {
  282. self._engineDidOpen(reason: reason)
  283. }
  284. }
  285. private func _engineDidOpen(reason: String) {
  286. DefaultSocketLogger.Logger.log("Engine opened \(reason)", type: SocketManager.logType)
  287. status = .connected
  288. nsps["/"]?.didConnect(toNamespace: "/")
  289. for (nsp, socket) in nsps where nsp != "/" && socket.status == .connecting {
  290. connectSocket(socket)
  291. }
  292. }
  293. /// Called when the engine receives a pong message.
  294. open func engineDidReceivePong() {
  295. handleQueue.async {
  296. self._engineDidReceivePong()
  297. }
  298. }
  299. private func _engineDidReceivePong() {
  300. emitAll(clientEvent: .pong, data: [])
  301. }
  302. /// Called when the sends a ping to the server.
  303. open func engineDidSendPing() {
  304. handleQueue.async {
  305. self._engineDidSendPing()
  306. }
  307. }
  308. private func _engineDidSendPing() {
  309. emitAll(clientEvent: .ping, data: [])
  310. }
  311. private func forAll(do: (SocketIOClient) throws -> ()) rethrows {
  312. for (_, socket) in nsps {
  313. try `do`(socket)
  314. }
  315. }
  316. /// Called when when upgrading the http connection to a websocket connection.
  317. ///
  318. /// - parameter headers: The http headers.
  319. open func engineDidWebsocketUpgrade(headers: [String: String]) {
  320. handleQueue.async {
  321. self._engineDidWebsocketUpgrade(headers: headers)
  322. }
  323. }
  324. private func _engineDidWebsocketUpgrade(headers: [String: String]) {
  325. emitAll(clientEvent: .websocketUpgrade, data: [headers])
  326. }
  327. /// Called when the engine has a message that must be parsed.
  328. ///
  329. /// - parameter msg: The message that needs parsing.
  330. open func parseEngineMessage(_ msg: String) {
  331. handleQueue.async {
  332. self._parseEngineMessage(msg)
  333. }
  334. }
  335. private func _parseEngineMessage(_ msg: String) {
  336. guard let packet = parseSocketMessage(msg) else { return }
  337. guard !packet.type.isBinary else {
  338. waitingPackets.append(packet)
  339. return
  340. }
  341. nsps[packet.nsp]?.handlePacket(packet)
  342. }
  343. /// Called when the engine receives binary data.
  344. ///
  345. /// - parameter data: The data the engine received.
  346. open func parseEngineBinaryData(_ data: Data) {
  347. handleQueue.async {
  348. self._parseEngineBinaryData(data)
  349. }
  350. }
  351. private func _parseEngineBinaryData(_ data: Data) {
  352. guard let packet = parseBinaryData(data) else { return }
  353. nsps[packet.nsp]?.handlePacket(packet)
  354. }
  355. /// Tries to reconnect to the server.
  356. ///
  357. /// This will cause a `SocketClientEvent.reconnect` event to be emitted, as well as
  358. /// `SocketClientEvent.reconnectAttempt` events.
  359. open func reconnect() {
  360. guard !reconnecting else { return }
  361. engine?.disconnect(reason: "manual reconnect")
  362. }
  363. /// Removes the socket from the manager's control. One of the disconnect methods should be called before calling this
  364. /// method.
  365. ///
  366. /// After calling this method the socket should no longer be considered usable.
  367. ///
  368. /// - parameter socket: The socket to remove.
  369. /// - returns: The socket removed, if it was owned by the manager.
  370. @discardableResult
  371. open func removeSocket(_ socket: SocketIOClient) -> SocketIOClient? {
  372. return nsps.removeValue(forKey: socket.nsp)
  373. }
  374. private func tryReconnect(reason: String) {
  375. guard reconnecting else { return }
  376. DefaultSocketLogger.Logger.log("Starting reconnect", type: SocketManager.logType)
  377. // Set status to connecting and emit reconnect for all sockets
  378. forAll {socket in
  379. guard socket.status == .connected else { return }
  380. socket.setReconnecting(reason: reason)
  381. }
  382. _tryReconnect()
  383. }
  384. private func _tryReconnect() {
  385. guard reconnects && reconnecting && status != .disconnected else { return }
  386. if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts {
  387. return didDisconnect(reason: "Reconnect Failed")
  388. }
  389. DefaultSocketLogger.Logger.log("Trying to reconnect", type: SocketManager.logType)
  390. emitAll(clientEvent: .reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)])
  391. currentReconnectAttempt += 1
  392. connect()
  393. let interval = reconnectInterval(attempts: currentReconnectAttempt)
  394. DefaultSocketLogger.Logger.log("Scheduling reconnect in \(interval)s", type: SocketManager.logType)
  395. handleQueue.asyncAfter(deadline: DispatchTime.now() + interval, execute: _tryReconnect)
  396. }
  397. func reconnectInterval(attempts: Int) -> Double {
  398. // apply exponential factor
  399. let backoffFactor = pow(1.5, attempts)
  400. let interval = Double(reconnectWait) * Double(truncating: backoffFactor as NSNumber)
  401. // add in a random factor smooth thundering herds
  402. let rand = Double.random(in: 0 ..< 1)
  403. let randomFactor = rand * randomizationFactor * Double(truncating: interval as NSNumber)
  404. // add in random factor, and clamp to min and max values
  405. let combined = interval + randomFactor
  406. return Double(fmax(Double(reconnectWait), fmin(combined, Double(reconnectWaitMax))))
  407. }
  408. /// Sets manager specific configs.
  409. ///
  410. /// parameter config: The configs that should be set.
  411. open func setConfigs(_ config: SocketIOClientConfiguration) {
  412. for option in config {
  413. switch option {
  414. case let .forceNew(new):
  415. self.forceNew = new
  416. case let .handleQueue(queue):
  417. self.handleQueue = queue
  418. case let .reconnects(reconnects):
  419. self.reconnects = reconnects
  420. case let .reconnectAttempts(attempts):
  421. self.reconnectAttempts = attempts
  422. case let .reconnectWait(wait):
  423. reconnectWait = abs(wait)
  424. case let .reconnectWaitMax(wait):
  425. reconnectWaitMax = abs(wait)
  426. case let .randomizationFactor(factor):
  427. randomizationFactor = factor
  428. case let .log(log):
  429. DefaultSocketLogger.Logger.log = log
  430. case let .logger(logger):
  431. DefaultSocketLogger.Logger = logger
  432. case _:
  433. continue
  434. }
  435. }
  436. _config = config
  437. if socketURL.absoluteString.hasPrefix("https://") {
  438. _config.insert(.secure(true))
  439. }
  440. _config.insert(.path("/socket.io/"), replacing: false)
  441. // If `ConfigSettable` & `SocketEngineSpec`, update its configs.
  442. if var settableEngine = engine as? ConfigSettable & SocketEngineSpec {
  443. settableEngine.engineQueue.sync {
  444. settableEngine.setConfigs(self._config)
  445. }
  446. engine = settableEngine
  447. }
  448. }
  449. /// Returns a `SocketIOClient` for the given namespace. This socket shares a transport with the manager.
  450. ///
  451. /// Calling multiple times returns the same socket.
  452. ///
  453. /// Sockets created from this method are retained by the manager.
  454. /// Call one of the `disconnectSocket` methods on this class to remove the socket from manager control.
  455. /// Or call `SocketIOClient.disconnect()` on the client.
  456. ///
  457. /// - parameter nsp: The namespace for the socket.
  458. /// - returns: A `SocketIOClient` for the given namespace.
  459. open func socket(forNamespace nsp: String) -> SocketIOClient {
  460. assert(nsp.hasPrefix("/"), "forNamespace must have a leading /")
  461. if let socket = nsps[nsp] {
  462. return socket
  463. }
  464. let client = SocketIOClient(manager: self, nsp: nsp)
  465. nsps[nsp] = client
  466. return client
  467. }
  468. // Test properties
  469. func setTestStatus(_ status: SocketIOStatus) {
  470. self.status = status
  471. }
  472. }