| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- //
- // Created by Erik Little on 10/14/17.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- import Dispatch
- import Foundation
- ///
- /// A manager for a socket.io connection.
- ///
- /// A `SocketManager` is responsible for multiplexing multiple namespaces through a single `SocketEngineSpec`.
- ///
- /// Example:
- ///
- /// ```swift
- /// let manager = SocketManager(socketURL: URL(string:"http://localhost:8080/")!)
- /// let defaultNamespaceSocket = manager.defaultSocket
- /// let swiftSocket = manager.socket(forNamespace: "/swift")
- ///
- /// // defaultNamespaceSocket and swiftSocket both share a single connection to the server
- /// ```
- ///
- /// Sockets created through the manager are retained by the manager. So at the very least, a single strong reference
- /// to the manager must be maintained to keep sockets alive.
- ///
- /// To disconnect a socket and remove it from the manager, either call `SocketIOClient.disconnect()` on the socket,
- /// or call one of the `disconnectSocket` methods on this class.
- ///
- /// **NOTE**: The manager is not thread/queue safe, all interaction with the manager should be done on the `handleQueue`
- ///
- open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDataBufferable, ConfigSettable {
- private static let logType = "SocketManager"
- // MARK: Properties
- /// The socket associated with the default namespace ("/").
- public var defaultSocket: SocketIOClient {
- return socket(forNamespace: "/")
- }
- /// The URL of the socket.io server.
- ///
- /// If changed after calling `init`, `forceNew` must be set to `true`, or it will only connect to the url set in the
- /// init.
- public let socketURL: URL
- /// The configuration for this client.
- ///
- /// **Some configs will not take affect until after a reconnect if set after calling a connect method**.
- public var config: SocketIOClientConfiguration {
- get {
- return _config
- }
- set {
- if status.active {
- DefaultSocketLogger.Logger.log("Setting configs on active manager. Some configs may not be applied until reconnect",
- type: SocketManager.logType)
- }
- setConfigs(newValue)
- }
- }
- /// The engine for this manager.
- public var engine: SocketEngineSpec?
- /// If `true` then every time `connect` is called, a new engine will be created.
- public var forceNew = false
- /// The queue that all interaction with the client should occur on. This is the queue that event handlers are
- /// called on.
- ///
- /// **This should be a serial queue! Concurrent queues are not supported and might cause crashes and races**.
- public var handleQueue = DispatchQueue.main
- /// The sockets in this manager indexed by namespace.
- public var nsps = [String: SocketIOClient]()
- /// If `true`, this client will try and reconnect on any disconnects.
- public var reconnects = true
- /// The minimum number of seconds to wait before attempting to reconnect.
- public var reconnectWait = 10
- /// The maximum number of seconds to wait before attempting to reconnect.
- public var reconnectWaitMax = 30
- /// The randomization factor for calculating reconnect jitter.
- public var randomizationFactor = 0.5
- /// The status of this manager.
- public private(set) var status: SocketIOStatus = .notConnected {
- didSet {
- switch status {
- case .connected:
- reconnecting = false
- currentReconnectAttempt = 0
- default:
- break
- }
- }
- }
- /// A list of packets that are waiting for binary data.
- ///
- /// The way that socket.io works all data should be sent directly after each packet.
- /// So this should ideally be an array of one packet waiting for data.
- ///
- /// **This should not be modified directly.**
- public var waitingPackets = [SocketPacket]()
- private(set) var reconnectAttempts = -1
- private var _config: SocketIOClientConfiguration
- private var currentReconnectAttempt = 0
- private var reconnecting = false
- // MARK: Initializers
- /// Type safe way to create a new SocketIOClient. `opts` can be omitted.
- ///
- /// - parameter socketURL: The url of the socket.io server.
- /// - parameter config: The config for this socket.
- public init(socketURL: URL, config: SocketIOClientConfiguration = []) {
- self._config = config
- self.socketURL = socketURL
- super.init()
- setConfigs(_config)
- }
- /// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity.
- /// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set<SocketIOClientOption>)`
- ///
- /// - parameter socketURL: The url of the socket.io server.
- /// - parameter config: The config for this socket.
- @objc
- public convenience init(socketURL: URL, config: [String: Any]?) {
- self.init(socketURL: socketURL, config: config?.toSocketConfiguration() ?? [])
- }
- /// :nodoc:
- deinit {
- DefaultSocketLogger.Logger.log("Manager is being released", type: SocketManager.logType)
- engine?.disconnect(reason: "Manager Deinit")
- }
- // MARK: Methods
- private func addEngine() {
- DefaultSocketLogger.Logger.log("Adding engine", type: SocketManager.logType)
- engine?.engineQueue.sync {
- self.engine?.client = nil
- // Close old engine so it will not leak because of URLSession if in polling mode
- self.engine?.disconnect(reason: "Adding new engine")
- }
- engine = SocketEngine(client: self, url: socketURL, config: config)
- }
- /// Connects the underlying transport and the default namespace socket.
- ///
- /// Override if you wish to attach a custom `SocketEngineSpec`.
- open func connect() {
- guard !status.active else {
- DefaultSocketLogger.Logger.log("Tried connecting an already active socket", type: SocketManager.logType)
- return
- }
- if engine == nil || forceNew {
- addEngine()
- }
- status = .connecting
- engine?.connect()
- }
- /// Connects a socket through this manager's engine.
- ///
- /// - parameter socket: The socket who we should connect through this manager.
- open func connectSocket(_ socket: SocketIOClient) {
- guard status == .connected else {
- DefaultSocketLogger.Logger.log("Tried connecting socket when engine isn't open. Connecting",
- type: SocketManager.logType)
- connect()
- return
- }
- engine?.send("0\(socket.nsp),", withData: [])
- }
- /// Called when the manager has disconnected from socket.io.
- ///
- /// - parameter reason: The reason for the disconnection.
- open func didDisconnect(reason: String) {
- forAll {socket in
- socket.didDisconnect(reason: reason)
- }
- }
- /// Disconnects the manager and all associated sockets.
- open func disconnect() {
- DefaultSocketLogger.Logger.log("Manager closing", type: SocketManager.logType)
- status = .disconnected
- engine?.disconnect(reason: "Disconnect")
- }
- /// Disconnects the given socket.
- ///
- /// This will remove the socket for the manager's control, and make the socket instance useless and ready for
- /// releasing.
- ///
- /// - parameter socket: The socket to disconnect.
- open func disconnectSocket(_ socket: SocketIOClient) {
- engine?.send("1\(socket.nsp),", withData: [])
- socket.didDisconnect(reason: "Namespace leave")
- }
- /// Disconnects the socket associated with `forNamespace`.
- ///
- /// This will remove the socket for the manager's control, and make the socket instance useless and ready for
- /// releasing.
- ///
- /// - parameter nsp: The namespace to disconnect from.
- open func disconnectSocket(forNamespace nsp: String) {
- guard let socket = nsps.removeValue(forKey: nsp) else {
- DefaultSocketLogger.Logger.log("Could not find socket for \(nsp) to disconnect",
- type: SocketManager.logType)
- return
- }
- disconnectSocket(socket)
- }
- /// Sends a client event to all sockets in `nsps`
- ///
- /// - parameter clientEvent: The event to emit.
- open func emitAll(clientEvent event: SocketClientEvent, data: [Any]) {
- forAll {socket in
- socket.handleClientEvent(event, data: data)
- }
- }
- /// Sends an event to the server on all namespaces in this manager.
- ///
- /// - parameter event: The event to send.
- /// - parameter items: The data to send with this event.
- open func emitAll(_ event: String, _ items: SocketData...) {
- guard let emitData = try? items.map({ try $0.socketRepresentation() }) else {
- DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
- type: SocketManager.logType)
- return
- }
- emitAll(event, withItems: emitData)
- }
- /// Sends an event to the server on all namespaces in this manager.
- ///
- /// Same as `emitAll(_:_:)`, but meant for Objective-C.
- ///
- /// - parameter event: The event to send.
- /// - parameter items: The data to send with this event.
- open func emitAll(_ event: String, withItems items: [Any]) {
- forAll {socket in
- socket.emit(event, with: items, completion: nil)
- }
- }
- /// Called when the engine closes.
- ///
- /// - parameter reason: The reason that the engine closed.
- open func engineDidClose(reason: String) {
- handleQueue.async {
- self._engineDidClose(reason: reason)
- }
- }
- private func _engineDidClose(reason: String) {
- waitingPackets.removeAll()
- if status != .disconnected {
- status = .notConnected
- }
- if status == .disconnected || !reconnects {
- didDisconnect(reason: reason)
- } else if !reconnecting {
- reconnecting = true
- tryReconnect(reason: reason)
- }
- }
- /// Called when the engine errors.
- ///
- /// - parameter reason: The reason the engine errored.
- open func engineDidError(reason: String) {
- handleQueue.async {
- self._engineDidError(reason: reason)
- }
- }
- private func _engineDidError(reason: String) {
- DefaultSocketLogger.Logger.error("\(reason)", type: SocketManager.logType)
- emitAll(clientEvent: .error, data: [reason])
- }
- /// Called when the engine opens.
- ///
- /// - parameter reason: The reason the engine opened.
- open func engineDidOpen(reason: String) {
- handleQueue.async {
- self._engineDidOpen(reason: reason)
- }
- }
- private func _engineDidOpen(reason: String) {
- DefaultSocketLogger.Logger.log("Engine opened \(reason)", type: SocketManager.logType)
- status = .connected
- nsps["/"]?.didConnect(toNamespace: "/")
- for (nsp, socket) in nsps where nsp != "/" && socket.status == .connecting {
- connectSocket(socket)
- }
- }
- /// Called when the engine receives a pong message.
- open func engineDidReceivePong() {
- handleQueue.async {
- self._engineDidReceivePong()
- }
- }
- private func _engineDidReceivePong() {
- emitAll(clientEvent: .pong, data: [])
- }
- /// Called when the sends a ping to the server.
- open func engineDidSendPing() {
- handleQueue.async {
- self._engineDidSendPing()
- }
- }
- private func _engineDidSendPing() {
- emitAll(clientEvent: .ping, data: [])
- }
- private func forAll(do: (SocketIOClient) throws -> ()) rethrows {
- for (_, socket) in nsps {
- try `do`(socket)
- }
- }
- /// Called when when upgrading the http connection to a websocket connection.
- ///
- /// - parameter headers: The http headers.
- open func engineDidWebsocketUpgrade(headers: [String: String]) {
- handleQueue.async {
- self._engineDidWebsocketUpgrade(headers: headers)
- }
- }
- private func _engineDidWebsocketUpgrade(headers: [String: String]) {
- emitAll(clientEvent: .websocketUpgrade, data: [headers])
- }
- /// Called when the engine has a message that must be parsed.
- ///
- /// - parameter msg: The message that needs parsing.
- open func parseEngineMessage(_ msg: String) {
- handleQueue.async {
- self._parseEngineMessage(msg)
- }
- }
- private func _parseEngineMessage(_ msg: String) {
- guard let packet = parseSocketMessage(msg) else { return }
- guard !packet.type.isBinary else {
- waitingPackets.append(packet)
- return
- }
- nsps[packet.nsp]?.handlePacket(packet)
- }
- /// Called when the engine receives binary data.
- ///
- /// - parameter data: The data the engine received.
- open func parseEngineBinaryData(_ data: Data) {
- handleQueue.async {
- self._parseEngineBinaryData(data)
- }
- }
- private func _parseEngineBinaryData(_ data: Data) {
- guard let packet = parseBinaryData(data) else { return }
- nsps[packet.nsp]?.handlePacket(packet)
- }
- /// Tries to reconnect to the server.
- ///
- /// This will cause a `SocketClientEvent.reconnect` event to be emitted, as well as
- /// `SocketClientEvent.reconnectAttempt` events.
- open func reconnect() {
- guard !reconnecting else { return }
- engine?.disconnect(reason: "manual reconnect")
- }
- /// Removes the socket from the manager's control. One of the disconnect methods should be called before calling this
- /// method.
- ///
- /// After calling this method the socket should no longer be considered usable.
- ///
- /// - parameter socket: The socket to remove.
- /// - returns: The socket removed, if it was owned by the manager.
- @discardableResult
- open func removeSocket(_ socket: SocketIOClient) -> SocketIOClient? {
- return nsps.removeValue(forKey: socket.nsp)
- }
- private func tryReconnect(reason: String) {
- guard reconnecting else { return }
- DefaultSocketLogger.Logger.log("Starting reconnect", type: SocketManager.logType)
- // Set status to connecting and emit reconnect for all sockets
- forAll {socket in
- guard socket.status == .connected else { return }
- socket.setReconnecting(reason: reason)
- }
- _tryReconnect()
- }
- private func _tryReconnect() {
- guard reconnects && reconnecting && status != .disconnected else { return }
- if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts {
- return didDisconnect(reason: "Reconnect Failed")
- }
- DefaultSocketLogger.Logger.log("Trying to reconnect", type: SocketManager.logType)
- emitAll(clientEvent: .reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)])
- currentReconnectAttempt += 1
- connect()
- let interval = reconnectInterval(attempts: currentReconnectAttempt)
- DefaultSocketLogger.Logger.log("Scheduling reconnect in \(interval)s", type: SocketManager.logType)
- handleQueue.asyncAfter(deadline: DispatchTime.now() + interval, execute: _tryReconnect)
- }
- func reconnectInterval(attempts: Int) -> Double {
- // apply exponential factor
- let backoffFactor = pow(1.5, attempts)
- let interval = Double(reconnectWait) * Double(truncating: backoffFactor as NSNumber)
- // add in a random factor smooth thundering herds
- let rand = Double.random(in: 0 ..< 1)
- let randomFactor = rand * randomizationFactor * Double(truncating: interval as NSNumber)
- // add in random factor, and clamp to min and max values
- let combined = interval + randomFactor
- return Double(fmax(Double(reconnectWait), fmin(combined, Double(reconnectWaitMax))))
- }
- /// Sets manager specific configs.
- ///
- /// parameter config: The configs that should be set.
- open func setConfigs(_ config: SocketIOClientConfiguration) {
- for option in config {
- switch option {
- case let .forceNew(new):
- self.forceNew = new
- case let .handleQueue(queue):
- self.handleQueue = queue
- case let .reconnects(reconnects):
- self.reconnects = reconnects
- case let .reconnectAttempts(attempts):
- self.reconnectAttempts = attempts
- case let .reconnectWait(wait):
- reconnectWait = abs(wait)
- case let .reconnectWaitMax(wait):
- reconnectWaitMax = abs(wait)
- case let .randomizationFactor(factor):
- randomizationFactor = factor
- case let .log(log):
- DefaultSocketLogger.Logger.log = log
- case let .logger(logger):
- DefaultSocketLogger.Logger = logger
- case _:
- continue
- }
- }
- _config = config
- if socketURL.absoluteString.hasPrefix("https://") {
- _config.insert(.secure(true))
- }
- _config.insert(.path("/socket.io/"), replacing: false)
- // If `ConfigSettable` & `SocketEngineSpec`, update its configs.
- if var settableEngine = engine as? ConfigSettable & SocketEngineSpec {
- settableEngine.engineQueue.sync {
- settableEngine.setConfigs(self._config)
- }
- engine = settableEngine
- }
- }
- /// Returns a `SocketIOClient` for the given namespace. This socket shares a transport with the manager.
- ///
- /// Calling multiple times returns the same socket.
- ///
- /// Sockets created from this method are retained by the manager.
- /// Call one of the `disconnectSocket` methods on this class to remove the socket from manager control.
- /// Or call `SocketIOClient.disconnect()` on the client.
- ///
- /// - parameter nsp: The namespace for the socket.
- /// - returns: A `SocketIOClient` for the given namespace.
- open func socket(forNamespace nsp: String) -> SocketIOClient {
- assert(nsp.hasPrefix("/"), "forNamespace must have a leading /")
- if let socket = nsps[nsp] {
- return socket
- }
- let client = SocketIOClient(manager: self, nsp: nsp)
- nsps[nsp] = client
- return client
- }
- // Test properties
- func setTestStatus(_ status: SocketIOStatus) {
- self.status = status
- }
- }
|