diff --git a/README.md b/README.md index ce733c7b..5c0d49b4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Socket.IO-Client-Swift ====================== -Socket.IO-client for Swift. Supports ws/wss connections and binary. For socket.io 1.0+ and Swift 1.1. +Socket.IO-client for Swift. Supports ws/wss/polling connections and binary. For socket.io 1.0+ and Swift 1.1. For Swift 1.2 use the 1.2 branch. @@ -15,12 +15,13 @@ API === Constructor ----------- -`init(socketURL: String, opts:[String: AnyObject]? = nil)` - Constructs a new client for the given URL. opts can be omitted (will use default values.) +`init(socketURL: String, opts:[String: AnyObject]? = nil)` - Constructs a new client for the given URL. opts can be omitted (will use default values. See example) Methods ------- 1. `socket.on(name:String, callback:((data:NSArray?, ack:AckEmitter?) -> Void))` - Adds a handler for an event. Items are passed by an array. `ack` can be used to send an ack when one is requested. See example. +2. `socket.onAny(callback:((event:String, items:AnyObject?)) -> Void)` - Adds a handler for all events. It will be called on any received event. 3. `socket.emit(event:String, args:AnyObject...)` - Sends a message. Can send multiple args. -4. `socket.emitWithAck(event:String, args:AnyObject...) -> SocketAckHandler` - Sends a message that requests an acknoweldgement from the server. Returns a SocketAckHandler which you can use to add an onAck handler. See example. +4. `socket.emitWithAck(event:String, args:AnyObject...) -> SocketAckHandler` - Sends a message that requests an acknowledgement from the server. Returns a SocketAckHandler which you can use to add an onAck handler. See example. 5. `socket.connect()` - Establishes a connection to the server. A "connect" event is fired upon successful connection. 6. `socket.connectWithParams(params:[String: AnyObject])` - Establishes a connection to the server passing the specified params. A "connect" event is fired upon successful connection. 7. `socket.close()` - Closes the socket. Once a socket is closed it should not be reopened. @@ -41,9 +42,13 @@ let socket = SocketIOClient(socketURL: "https://localhost:8080", opts: [ "reconnects": true, // default true "reconnectAttempts": 5, // default -1 (infinite tries) "reconnectWait": 5, // default 10 - "nsp": "swift" // connects to the specified namespace. Default is / + "nsp": "swift", // connects to the specified namespace. Default is / + "forcePolling": true // if true, the socket will only use XHR polling, default is false (polling/WebSockets) ]) +// Called on every event +socket.onAny {println("got event: \($0.event) with items \($0.items)")} + // Socket Events socket.on("connect") {data, ack in println("socket connected") @@ -67,7 +72,7 @@ socket.on("ackEvent") {data, ack in } socket.emitWithAck("ackTest", "test").onAck {data in - println(data) + println(data?[0]) } ack?("Got your event", "dude") diff --git a/SwiftIO/SocketAckHandler.swift b/SwiftIO/SocketAckHandler.swift index 5a9d9096..17a42364 100644 --- a/SwiftIO/SocketAckHandler.swift +++ b/SwiftIO/SocketAckHandler.swift @@ -24,7 +24,7 @@ import Foundation -typealias AckCallback = (AnyObject?) -> Void +typealias AckCallback = (NSArray?) -> Void class SocketAckHandler { let ackNum:Int! diff --git a/SwiftIO/SocketEngine.swift b/SwiftIO/SocketEngine.swift new file mode 100644 index 00000000..1c743385 --- /dev/null +++ b/SwiftIO/SocketEngine.swift @@ -0,0 +1,596 @@ +// +// SocketEngine.swift +// Socket.IO-Swift +// +// Created by Erik Little on 3/3/15. +// +// 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 Foundation + +// This is used because in Swift 1.1, turning on -O causes a +// memory access violation in SocketEngine#parseEngineMessage +private var fixSwift:AnyObject? + +extension String { + private var length:Int { + return countElements(self) + } +} + +private typealias PollWaitQueue = [() -> Void] + +private enum PacketType: String { + case OPEN = "0" + case CLOSE = "1" + case PING = "2" + case PONG = "3" + case MESSAGE = "4" + case UPGRADE = "5" + case NOOP = "6" +} + +class SocketEngine: NSObject, SRWebSocketDelegate { + unowned let client:SocketIOClient + private let workQueue = NSOperationQueue() + private let emitQueue = dispatch_queue_create( + "emitQueue".cStringUsingEncoding(NSUTF8StringEncoding), DISPATCH_QUEUE_SERIAL) + private let parseQueue = dispatch_queue_create( + "parseQueue".cStringUsingEncoding(NSUTF8StringEncoding), DISPATCH_QUEUE_SERIAL) + private let handleQueue = dispatch_queue_create( + "handleQueue".cStringUsingEncoding(NSUTF8StringEncoding), DISPATCH_QUEUE_SERIAL) + private var forcePolling = false + private var pingTimer:NSTimer? + private var postWait = [String]() + private var _polling = true + private var probing = false + private var probeWait = PollWaitQueue() + private let session:NSURLSession! + private var waitingForPoll = false + private var waitingForPost = false + private var _websocket = false + private var websocketConnected = false + var connected = false + var pingInterval:Int? + var polling:Bool { + return self._polling + } + var sid = "" + var urlPolling:String? + var urlWebSocket:String? + var websocket:Bool { + return self._websocket + } + var ws:SRWebSocket? + + init(client:SocketIOClient, forcePolling:Bool = false) { + self.client = client + self.forcePolling = forcePolling + self.session = NSURLSession(configuration: NSURLSessionConfiguration.ephemeralSessionConfiguration(), + delegate: nil, delegateQueue: self.workQueue) + } + + func close() { + self.pingTimer?.invalidate() + self.send(PacketType.CLOSE.rawValue) + } + + private func createBinaryDataForSend(data:NSData) -> (NSData?, String?) { + if self.websocket { + var byteArray = [UInt8](count: 1, repeatedValue: 0x0) + byteArray[0] = 4 + var mutData = NSMutableData(bytes: &byteArray, length: 1) + mutData.appendData(data) + return (mutData, nil) + } else { + var str = "b4" + str += data.base64EncodedStringWithOptions( + NSDataBase64EncodingOptions.Encoding64CharacterLineLength) + + return (nil, str) + } + } + + private func createURLs(params:[String: AnyObject]? = nil) -> (String, String) { + var url = "\(self.client.socketURL)/socket.io/?transport=" + var urlPolling:String + var urlWebSocket:String + + if self.client.secure { + urlPolling = "https://" + url + "polling" + urlWebSocket = "wss://" + url + "websocket" + } else { + urlPolling = "http://" + url + "polling" + urlWebSocket = "ws://" + url + "websocket" + } + + if params != nil { + for (key, value) in params! { + let keyEsc = key.stringByAddingPercentEncodingWithAllowedCharacters( + NSCharacterSet.URLHostAllowedCharacterSet())! + urlPolling += "&\(keyEsc)=" + urlWebSocket += "&\(keyEsc)=" + + if value is String { + let valueEsc = (value as String).stringByAddingPercentEncodingWithAllowedCharacters( + NSCharacterSet.URLHostAllowedCharacterSet())! + urlPolling += "\(valueEsc)" + urlWebSocket += "\(valueEsc)" + } else { + urlPolling += "\(value)" + urlWebSocket += "\(value)" + } + } + } + + return (urlPolling, urlWebSocket) + } + + private func doPoll() { + if self.urlPolling == nil || self.websocket || self.waitingForPoll || !self.connected { + return + } + + let req = NSURLRequest(URL: NSURL(string: self.urlPolling! + "&sid=\(self.sid)")!) + self.waitingForPoll = true + + self.session.dataTaskWithRequest(req) {[weak self] data, res, err in + if self == nil { + return + } else if err != nil { + if self!.polling { + self?.handlePollingFailed(err) + } + return + } + + // println(data) + + if let str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String { + // println(str) + + dispatch_async(self?.parseQueue) {[weak self] in + self?.parsePollingMessage(str) + return + } + } + + self?.waitingForPoll = false + self?.doPoll() + }.resume() + } + + private func flushProbeWait() { + // println("flushing probe wait") + dispatch_async(self.emitQueue) {[weak self] in + if self == nil { + return + } + + for waiter in self!.probeWait { + waiter() + } + + self?.probeWait.removeAll(keepCapacity: false) + } + } + + private func flushWaitingForPost() { + if self.postWait.count == 0 || !self.connected { + return + } else if self.websocket { + self.flushWaitingForPostToWebSocket() + return + } + + var postStr = "" + + for packet in self.postWait { + let len = countElements(packet) + + postStr += "\(len):\(packet)" + } + + self.postWait.removeAll(keepCapacity: false) + + let req = NSMutableURLRequest(URL: NSURL(string: self.urlPolling! + "&sid=\(self.sid)")!) + + req.HTTPMethod = "POST" + req.setValue("application/html-text", forHTTPHeaderField: "Content-Type") + + let postData = postStr.dataUsingEncoding(NSUTF8StringEncoding, + allowLossyConversion: false)! + + + req.setValue(String(postData.length), forHTTPHeaderField: "Content-Length") + req.HTTPBody = postData + + self.waitingForPost = true + + self.session.dataTaskWithRequest(req) {[weak self] data, res, err in + if err != nil { + if self!.polling { + self?.handlePollingFailed(err) + } + return + } else if self == nil { + return + } + + self?.waitingForPost = false + dispatch_async(self!.emitQueue) { + self?.flushWaitingForPost() + self?.doPoll() + return + }}.resume() + } + + // We had packets waiting for send when we upgraded + // Send them raw + private func flushWaitingForPostToWebSocket() { + for msg in self.postWait { + self.ws?.send(msg) + } + + self.postWait.removeAll(keepCapacity: true) + } + + // A poll failed, tell the client about it + // We check to see if we were closed by the server first + private func handlePollingFailed(reason:NSError?) { + if !self.client.reconnecting { + self.connected = false + self.ws?.close() + self.pingTimer?.invalidate() + self.waitingForPoll = false + self.waitingForPost = false + self.client.pollingDidFail(reason) + } + } + + func open(opts:[String: AnyObject]? = nil) { + if self.waitingForPost || self.waitingForPoll || self.websocket || self.connected { + assert(false, "We're in a bad state, this shouldn't happen.") + } + + let (urlPolling, urlWebSocket) = self.createURLs(params: opts) + + self.urlPolling = urlPolling + self.urlWebSocket = urlWebSocket + let reqPolling = NSURLRequest(URL: NSURL(string: urlPolling + "&b64=1")!) + + self.session.dataTaskWithRequest(reqPolling) {[weak self] data, res, err in + var err2:NSError? + if self == nil { + return + } else if err != nil || data == nil { + self?.handlePollingFailed(err) + return + } + + if let dataString = NSString(data: data, encoding: NSUTF8StringEncoding) { + var mutString = RegexMutable(dataString) + let parsed:[String]? = mutString["(\\d*):(\\d)(\\{.*\\})?"].groups() + + if parsed == nil || parsed?.count != 4 { + return + } + + let length = parsed![1] + let type = parsed![2] + let jsonData = parsed![3].dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) + + if type != "0" { + NSLog("Error handshaking") + return + } + + self?.connected = true + + if let json = NSJSONSerialization.JSONObjectWithData(jsonData!, + options: NSJSONReadingOptions.AllowFragments, error: &err2) as? NSDictionary { + if let sid = json["sid"] as? String { + // println(json) + self?.sid = sid + + if !self!.forcePolling { + self?.ws = SRWebSocket(URL: + NSURL(string: urlWebSocket + "&sid=\(self!.sid)")!) + self?.ws?.delegate = self + self?.ws?.open() + } + } else { + NSLog("Error handshaking") + return + } + + if let pingInterval = json["pingInterval"] as? Int { + self?.pingInterval = pingInterval / 1000 + } + } + + self?.doPoll() + self?.startPingTimer() + }}.resume() + } + + // Translatation of engine.io-parser#decodePayload + private func parsePollingMessage(str:String) { + if str.length == 1 { + return + } + + // println(str) + + let strArray = Array(str) + var length = "" + var n = 0 + var msg = "" + + func testLength(length:String, inout n:Int) -> Bool { + if let num = length.toInt() { + n = num + } else { + return true + } + + return false + } + + for var i = 0, l = str.length; i < l; i = i &+ 1 { + let chr = String(strArray[i]) + + if chr != ":" { + length += chr + } else { + if length == "" || testLength(length, &n) { + self.handlePollingFailed(nil) + return + } + + msg = String(strArray[i&+1...i&+n]) + + if let lengthInt = length.toInt() { + if lengthInt != msg.length { + println("parsing error") + return + } + } + + if msg.length != 0 { + // Be sure to capture the value of the msg + dispatch_async(self.handleQueue) {[weak self, msg] in + fixSwift = msg + self?.parseEngineMessage(fixSwift) + return + } + } + + i += n + length = "" + } + } + } + + private func parseEngineMessage(message:AnyObject?) { + // println(message!) + if let data = message as? NSData { + // Strip off message type + self.client.parseSocketMessage(data.subdataWithRange(NSMakeRange(1, data.length - 1))) + return + } + + var messageString = message as String + var strMessage = RegexMutable(messageString) + + // We should upgrade + if strMessage == "3probe" { + self.upgradeTransport() + return + } + + let type = strMessage["^(\\d)"].groups()?[1] + + if type != PacketType.MESSAGE.rawValue { + // TODO Handle other packets + if messageString.hasPrefix("b4") { + // binary in base64 string + + messageString.removeRange(Range(start: messageString.startIndex, + end: advance(messageString.startIndex, 2))) + + if let data = NSData(base64EncodedString: messageString, + options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters) { + // println("sending \(data)") + self.client.parseSocketMessage(data) + } + + return + } else if type == PacketType.NOOP.rawValue { + self.doPoll() + return + } + + if messageString == PacketType.CLOSE.rawValue { + // do nothing + return + } + // println("Got something idk what to do with") + // println(messageString) + } + + // Remove message type + messageString.removeAtIndex(messageString.startIndex) + // println("sending \(messageString)") + + self.client.parseSocketMessage(messageString) + } + + private func probeWebSocket() { + if self.websocketConnected { + self.sendWebSocketMessage("probe", withType: PacketType.PING) + } + } + + func send(msg:String, datas:[NSData]? = nil) { + let _send = {[weak self] (msg:String, datas:[NSData]?) -> () -> Void in + return { + if self == nil || !self!.connected { + return + } + + if self!.websocket { + // println("sending ws: \(msg):\(datas)") + self?.sendWebSocketMessage(msg, withType: PacketType.MESSAGE, datas: datas) + } else { + // println("sending poll: \(msg):\(datas)") + self?.sendPollMessage(msg, withType: PacketType.MESSAGE, datas: datas) + } + } + } + + dispatch_async(self.emitQueue) {[weak self] in + if self == nil { + return + } + + if self!.probing { + self?.probeWait.append(_send(msg, datas)) + } else { + _send(msg, datas)() + } + } + } + + func sendPing() { + if self.websocket { + self.sendWebSocketMessage("", withType: PacketType.PING) + } else { + self.sendPollMessage("", withType: PacketType.PING) + } + } + + private func sendPollMessage(msg:String, withType type:PacketType, datas:[NSData]? = nil) { + // println("Sending: poll: \(msg) as type: \(type.rawValue)") + let strMsg = "\(type.rawValue)\(msg)" + + self.postWait.append(strMsg) + + if datas != nil { + for data in datas! { + let (nilData, b64Data) = self.createBinaryDataForSend(data) + + self.postWait.append(b64Data!) + } + } + + if waitingForPost { + self.doPoll() + return + } else { + self.flushWaitingForPost() + } + } + + private func sendWebSocketMessage(str:String, withType type:PacketType, datas:[NSData]? = nil) { + // println("Sending: ws: \(str) as type: \(type.rawValue)") + self.ws?.send("\(type.rawValue)\(str)") + + if datas != nil { + for data in datas! { + let (data, nilString) = self.createBinaryDataForSend(data) + if data != nil { + self.ws?.send(data!) + } + } + } + } + + // Starts the ping timer + private func startPingTimer() { + if self.pingInterval == nil { + return + } + + self.pingTimer?.invalidate() + dispatch_async(dispatch_get_main_queue()) { + self.pingTimer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(self.pingInterval!), target: self, + selector: Selector("sendPing"), userInfo: nil, repeats: true) + } + } + + private func upgradeTransport() { + if self.websocketConnected { + self.probing = false + self._websocket = true + self.waitingForPoll = false + self._polling = false + self.sendWebSocketMessage("", withType: PacketType.UPGRADE) + self.flushProbeWait() + } + } + + // Called when a message is recieved + func webSocket(webSocket:SRWebSocket!, didReceiveMessage message:AnyObject?) { + // println(message) + + dispatch_async(self.handleQueue) {[weak self] in + self?.parseEngineMessage(message) + return + } + } + + // Called when the socket is opened + func webSocketDidOpen(webSocket:SRWebSocket!) { + self.websocketConnected = true + self.probing = true + self.probeWebSocket() + } + + // Called when the socket is closed + func webSocket(webSocket:SRWebSocket!, didCloseWithCode code:Int, reason:String!, wasClean:Bool) { + self.websocketConnected = false + self.probing = false + + if self.websocket { + self.pingTimer?.invalidate() + self.connected = false + self._websocket = false + self._polling = true + self.client.webSocketDidCloseWithCode(code, reason: reason, wasClean: wasClean) + } else { + self.flushProbeWait() + } + } + + // Called when an error occurs. + func webSocket(webSocket:SRWebSocket!, didFailWithError error:NSError!) { + self.websocketConnected = false + self._polling = true + self.probing = false + + if self.websocket { + self.pingTimer?.invalidate() + self.connected = false + self.client.webSocketDidFailWithError(error) + } else { + self.flushProbeWait() + } + } +} \ No newline at end of file diff --git a/SwiftIO/SocketEvent.swift b/SwiftIO/SocketEvent.swift index 26d84995..7f5f1339 100644 --- a/SwiftIO/SocketEvent.swift +++ b/SwiftIO/SocketEvent.swift @@ -74,29 +74,29 @@ class SocketEvent { if !hasBinary { if nsp == nil { if ack == nil { - message = "42[\"\(event)\"" + message = "2[\"\(event)\"" } else { - message = "42\(ack!)[\"\(event)\"" + message = "2\(ack!)[\"\(event)\"" } } else { if ack == nil { - message = "42/\(nsp!),[\"\(event)\"" + message = "2/\(nsp!),[\"\(event)\"" } else { - message = "42/\(nsp!),\(ack!)[\"\(event)\"" + message = "2/\(nsp!),\(ack!)[\"\(event)\"" } } } else { if nsp == nil { if ack == nil { - message = "45\(datas)-[\"\(event)\"" + message = "5\(datas)-[\"\(event)\"" } else { - message = "45\(datas)-\(ack!)[\"\(event)\"" + message = "5\(datas)-\(ack!)[\"\(event)\"" } } else { if ack == nil { - message = "45\(datas)-/\(nsp!),[\"\(event)\"" + message = "5\(datas)-/\(nsp!),[\"\(event)\"" } else { - message = "45\(datas)-/\(nsp!),\(ack!)[\"\(event)\"" + message = "5\(datas)-/\(nsp!),\(ack!)[\"\(event)\"" } } } @@ -110,15 +110,15 @@ class SocketEvent { if ackType == 3 { if nsp == "/" { - msg = "43\(ack)[" + msg = "3\(ack)[" } else { - msg = "43/\(nsp),\(ack)[" + msg = "3/\(nsp),\(ack)[" } } else { if nsp == "/" { - msg = "46\(binary)-\(ack)[" + msg = "6\(binary)-\(ack)[" } else { - msg = "46\(binary)-/\(nsp),\(ack)[" + msg = "6\(binary)-/\(nsp),\(ack)[" } } @@ -159,6 +159,7 @@ class SocketEvent { if message != "" { message.removeAtIndex(message.endIndex.predecessor()) } + return message + "]" } @@ -251,4 +252,4 @@ class SocketEvent { return false } -} +} \ No newline at end of file diff --git a/SwiftIO/SocketEventHandler.swift b/SwiftIO/SocketEventHandler.swift index 3fe92041..c59e92ca 100644 --- a/SwiftIO/SocketEventHandler.swift +++ b/SwiftIO/SocketEventHandler.swift @@ -23,6 +23,7 @@ // THE SOFTWARE. typealias NormalCallback = (NSArray?, AckEmitter?) -> Void +typealias AnyHandler = (event:String, items:AnyObject?) typealias AckEmitter = (AnyObject...) -> Void private func emitAckCallback(socket:SocketIOClient, num:Int, type:Int) -> AckEmitter { @@ -44,6 +45,9 @@ class SocketEventHandler { func executeCallback(_ items:NSArray? = nil, withAck ack:Int? = nil, withAckType type:Int? = nil, withSocket socket:SocketIOClient? = nil) { - callback?(items, ack != nil ? emitAckCallback(socket!, ack!, type!) : nil) + dispatch_async(dispatch_get_main_queue()) {[weak self] in + self?.callback?(items, ack != nil ? emitAckCallback(socket!, ack!, type!) : nil) + return + } } } \ No newline at end of file diff --git a/SwiftIO/SocketIOClient.swift b/SwiftIO/SocketIOClient.swift index b8350921..a37f6418 100644 --- a/SwiftIO/SocketIOClient.swift +++ b/SwiftIO/SocketIOClient.swift @@ -24,7 +24,8 @@ import Foundation -class SocketIOClient: NSObject, SRWebSocketDelegate { +class SocketIOClient { + let engine:SocketEngine! let socketURL:NSMutableString! let ackQueue = dispatch_queue_create("ackQueue".cStringUsingEncoding(NSUTF8StringEncoding), DISPATCH_QUEUE_SERIAL) @@ -32,36 +33,42 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { DISPATCH_QUEUE_SERIAL) let emitQueue = dispatch_queue_create("emitQueue".cStringUsingEncoding(NSUTF8StringEncoding), DISPATCH_QUEUE_SERIAL) - private lazy var params:[String: AnyObject] = [String: AnyObject]() + let reconnectAttempts:Int! + private lazy var params = [String: AnyObject]() private var ackHandlers = [SocketAckHandler]() + private var anyHandler:((AnyHandler) -> Void)? private var currentAck = -1 + private var currentReconnectAttempt = 0 + private var forcePolling = false private var handlers = [SocketEventHandler]() - private var lastSocketMessage:SocketEvent? + private var waitingData = [SocketEvent]() private var paramConnect = false - private var pingTimer:NSTimer! - private var secure = false + private var _secure = false + private var reconnectTimer:NSTimer? var closed = false var connected = false var connecting = false - var io:SRWebSocket? var nsp:String? var reconnects = true var reconnecting = false - var reconnectAttempts = -1 var reconnectWait = 10 + var secure:Bool { + return self._secure + } var sid:String? init(socketURL:String, opts:[String: AnyObject]? = nil) { var mutURL = RegexMutable(socketURL) if mutURL["https://"].matches().count != 0 { - self.secure = true + self._secure = true } mutURL = mutURL["http://"] ~= "" mutURL = mutURL["https://"] ~= "" self.socketURL = mutURL + self.reconnectAttempts = -1 // Set options if opts != nil { @@ -80,75 +87,66 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { if let nsp = opts!["nsp"] as? String { self.nsp = nsp } + + if let polling = opts!["forcePolling"] as? Bool { + self.forcePolling = polling + } } + + self.engine = SocketEngine(client: self, forcePolling: self.forcePolling) } // Closes the socket func close() { - self.pingTimer?.invalidate() self.closed = true self.connecting = false self.connected = false - self.io?.send("41") - self.io?.close() + self.reconnecting = false + self.engine?.close() } // Connects to the server func connect() { - self.connectWithURL(self.createConnectURL()) - } - - // Connect to the server using params - func connectWithParams(params:[String: AnyObject]) { - self.params = params - self.paramConnect = true - var endpoint = self.createConnectURL() - - for (key, value) in params { - let keyEsc = key.stringByAddingPercentEncodingWithAllowedCharacters( - NSCharacterSet.URLHostAllowedCharacterSet())! - endpoint += "&\(keyEsc)=" - - if value is String { - let valueEsc = (value as String).stringByAddingPercentEncodingWithAllowedCharacters( - NSCharacterSet.URLHostAllowedCharacterSet())! - endpoint += "\(valueEsc)" - } else { - endpoint += "\(value)" - } + if self.closed { + println("Warning! This socket was previously closed. This might be dangerous!") + self.closed = false } - self.connectWithURL(endpoint) + self.engine.open() } - private func connectWithURL(url:String) { + // Connect to the server using params + func connectWithParams(params:[String: AnyObject]) { if self.closed { - println("Warning: This socket was previvously closed. Reopening could be dangerous. Be careful.") + println("Warning! This socket was previously closed. This might be dangerous!") + self.closed = false } - self.connecting = true - self.closed = false + self.params = params + self.paramConnect = true - self.io = SRWebSocket(URL: NSURL(string: url)) - self.io?.delegate = self - self.io?.open() + self.engine.open(opts: params) } - // Creates a binary message, ready for sending - private class func createBinaryDataForSend(data:NSData) -> NSData { - var byteArray = [UInt8](count: 1, repeatedValue: 0x0) - byteArray[0] = 4 - var mutData = NSMutableData(bytes: &byteArray, length: 1) - mutData.appendData(data) - return mutData + func didConnect() { + self.closed = false + self.connected = true + self.connecting = false + self.reconnecting = false + self.currentReconnectAttempt = 0 + self.reconnectTimer?.invalidate() + self.reconnectTimer = nil + self.handleEvent("connect", data: nil, isInternalMessage: false) } - private func createConnectURL() -> String { - if self.secure { - return "wss://\(self.socketURL)/socket.io/?transport=websocket" - } else { - return "ws://\(self.socketURL)/socket.io/?transport=websocket" - } + // Server wants us to die + func didForceClose() { + self.closed = true + self.connected = false + self.reconnects = false + self.connecting = false + self.reconnecting = false + self.handleEvent("disconnect", data: "closed", isInternalMessage: true) } // Sends a message with multiple args @@ -160,11 +158,8 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } dispatch_async(self.emitQueue) {[weak self] in - if self == nil { - return - } - self?._emit(event, args) + return } } @@ -178,11 +173,8 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { self.ackHandlers.append(ackHandler) dispatch_async(self.emitQueue) {[weak self] in - if self == nil { - return - } - self?._emit(event, args, ack: true) + return } return ackHandler @@ -207,10 +199,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { hasBinary: true, withDatas: emitDatas.count, toNamespace: self.nsp, wantsAck: self.currentAck) } - self.io?.send(str) - for data in emitDatas { - self.io?.send(data) - } + self.engine?.send(str, datas: emitDatas) } else { if !ack { str = SocketEvent.createMessageForEvent(event, withArgs: items, hasBinary: false, @@ -220,7 +209,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { withDatas: 0, toNamespace: self.nsp, wantsAck: self.currentAck) } - self.io?.send(str) + self.engine?.send(str) } } @@ -243,7 +232,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { withAckType: 3, withNsp: self!.nsp!) } - self?.io?.send(str) + self?.engine?.send(str) } else { if self?.nsp == nil { str = SocketEvent.createAck(ack, withArgs: items, @@ -253,10 +242,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { withAckType: 6, withNsp: self!.nsp!, withBinary: emitDatas.count) } - self?.io?.send(str) - for data in emitDatas { - self?.io?.send(data) - } + self?.engine?.send(str, datas: emitDatas) } } } @@ -267,7 +253,14 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { if handler.ackNum != ack { return true } else { - handler.callback?(data) + if data is NSArray { + handler.callback?(data as? NSArray) + } else if data != nil { + handler.callback?([data!]) + } else { + handler.callback?(nil) + } + return false } } @@ -277,45 +270,48 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { func handleEvent(event:String, data:AnyObject?, isInternalMessage:Bool = false, wantsAck ack:Int? = nil, withAckType ackType:Int = 3) { // println("Should do event: \(event) with data: \(data)") - dispatch_async(dispatch_get_main_queue()) { - if !self.connected && !isInternalMessage { - return - } - - for handler in self.handlers { - if handler.event == event { - if data is NSArray { - if ack != nil { - handler.executeCallback(data as? NSArray, withAck: ack!, - withAckType: ackType, withSocket: self) - } else { - handler.executeCallback(data as? NSArray) - } + if !self.connected && !isInternalMessage { + return + } + + dispatch_async(dispatch_get_main_queue()) {[weak self] in + self?.anyHandler?((event, data)) + return + } + for handler in self.handlers { + if handler.event == event { + if data is NSArray { + if ack != nil { + handler.executeCallback(data as? NSArray, withAck: ack!, + withAckType: ackType, withSocket: self) } else { - - // Trying to do a ternary expression in the executeCallback method - // seemed to crash Swift - var dataArr:NSArray? = nil - - if let data:AnyObject = data { - dataArr = [data] - } - - if ack != nil { - handler.executeCallback(dataArr, withAck: ack!, - withAckType: ackType, withSocket: self) - } else { - handler.executeCallback(dataArr) - } + handler.executeCallback(data as? NSArray) + } + } else { + + // Trying to do a ternary expression in the executeCallback method + // seemed to crash Swift + var dataArr:NSArray? = nil + + if let data:AnyObject = data { + dataArr = [data] + } + + if ack != nil { + handler.executeCallback(dataArr, withAck: ack!, + withAckType: ackType, withSocket: self) + } else { + handler.executeCallback(dataArr) } } } } } - private func joinNamespace() { + // Should be removed and moved to SocketEngine + func joinNamespace() { if self.nsp != nil { - self.io?.send("40/\(self.nsp!)") + self.engine?.send("0/\(self.nsp!)") } } @@ -325,35 +321,38 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { self.handlers.append(handler) } + // Adds a handler for any event + func onAny(handler:(AnyHandler) -> Void) { + self.anyHandler = handler + } + // Opens the connection to the socket func open() { self.connect() } // Parse an NSArray looking for binary data - private class func parseArray(arr:NSArray, var placeholders:Int) -> (NSArray, Bool, [NSData]) { + private class func parseArray(arr:NSArray, var currentPlaceholder:Int) -> (NSArray, Bool, [NSData]) { var replacementArr = [AnyObject](count: arr.count, repeatedValue: 1) var hasBinary = false var arrayDatas = [NSData]() - if placeholders == -1 { - placeholders = 0 - } - for g in 0.. ([AnyObject], Bool, [NSData]) { var items = [AnyObject](count: args.count, repeatedValue: 1) - var numberOfPlaceholders = -1 + var currentPlaceholder = -1 var hasBinary = false var emitDatas = [NSData]() @@ -408,9 +408,9 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { if let dict = args[i] as? NSDictionary { // Check for binary data let (newDict, hadBinary, binaryDatas) = SocketIOClient.parseNSDictionary(dict, - placeholders: numberOfPlaceholders) + currentPlaceholder: currentPlaceholder) if hadBinary { - numberOfPlaceholders = binaryDatas.count + currentPlaceholder += binaryDatas.count emitDatas.extend(binaryDatas) hasBinary = true @@ -421,11 +421,11 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } else if let arr = args[i] as? NSArray { // arg is array, check for binary let (replace, hadData, newDatas) = SocketIOClient.parseArray(arr, - placeholders: numberOfPlaceholders) + currentPlaceholder: currentPlaceholder) if hadData { hasBinary = true - numberOfPlaceholders += emitDatas.count + currentPlaceholder += newDatas.count for data in newDatas { emitDatas.append(data) @@ -438,11 +438,10 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } else if let binaryData = args[i] as? NSData { // args is just binary hasBinary = true - let sendData = SocketIOClient.createBinaryDataForSend(binaryData) - numberOfPlaceholders++ - items[i] = ["_placeholder": true, "num": numberOfPlaceholders] - emitDatas.append(sendData) + currentPlaceholder++ + items[i] = ["_placeholder": true, "num": currentPlaceholder] + emitDatas.append(binaryData) } else { items[i] = args[i] } @@ -452,39 +451,36 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } // Parses a NSDictionary, looking for NSData objects - private class func parseNSDictionary(dict:NSDictionary, var placeholders:Int) -> (NSDictionary, Bool, [NSData]) { + private class func parseNSDictionary(dict:NSDictionary, var currentPlaceholder:Int) -> (NSDictionary, Bool, [NSData]) { var returnDict = NSMutableDictionary() var hasBinary = false - if placeholders == -1 { - placeholders = 0 - } var returnDatas = [NSData]() for (key, value) in dict { if let binaryData = value as? NSData { + currentPlaceholder++ hasBinary = true - let sendData = self.createBinaryDataForSend(binaryData) - returnDatas.append(sendData) - returnDict[key as String] = ["_placeholder": true, "num": placeholders++] + returnDatas.append(binaryData) + returnDict[key as String] = ["_placeholder": true, "num": currentPlaceholder++] } else if let arr = value as? NSArray { - let (replace, hadBinary, arrDatas) = self.parseArray(arr, placeholders: placeholders) + let (replace, hadBinary, arrDatas) = self.parseArray(arr, currentPlaceholder: currentPlaceholder) if hadBinary { hasBinary = true returnDict[key as String] = replace - placeholders += arrDatas.count + currentPlaceholder += arrDatas.count returnDatas.extend(arrDatas) } else { returnDict[key as String] = arr } } else if let dict = value as? NSDictionary { // Recursive - let (nestDict, hadBinary, nestDatas) = self.parseNSDictionary(dict, placeholders: placeholders) + let (nestDict, hadBinary, nestDatas) = self.parseNSDictionary(dict, currentPlaceholder: currentPlaceholder) if hadBinary { hasBinary = true returnDict[key as String] = nestDict - placeholders += nestDatas.count + currentPlaceholder += nestDatas.count returnDatas.extend(nestDatas) } else { returnDict[key as String] = dict @@ -498,7 +494,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } // Parses messages recieved - private func parseSocketMessage(message:AnyObject?) { + func parseSocketMessage(message:AnyObject?) { if message == nil { return } @@ -508,40 +504,33 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { if let stringMessage = message as? String { // Check for successful namepsace connect if self.nsp != nil { - if stringMessage == "40/\(self.nsp!)" { - self.handleEvent("connect", data: nil) + if stringMessage == "0/\(self.nsp!)" { + self.didConnect() return } } - /** - Begin check for socket info frame - **/ - var mutMessage = RegexMutable(stringMessage) - var setup:String! - let messageData = mutMessage["(\\d*)(\\{.*\\})?"].groups() - if messageData != nil && messageData[1] == "0" { - setup = messageData[2] - let data = setup.dataUsingEncoding(NSUTF8StringEncoding)! - var jsonError:NSError? - - if let json:AnyObject? = NSJSONSerialization.JSONObjectWithData(data, - options: nil, error: &jsonError) { - self.sid = json!["sid"] as? String - self.startPingTimer(interval: (json!["pingInterval"] as Int) / 1000) - return + if stringMessage == "0" { + if self.nsp != nil { + // Join namespace + self.joinNamespace() + return + } else { + // Don't handle as internal because something crazy could happen where + // we disconnect before it's handled + self.didConnect() + return } } - /** - End check for socket info frame - **/ + + var mutMessage = RegexMutable(stringMessage) /** Begin check for message **/ let messageGroups = mutMessage["(\\d*)\\/?(\\w*)?,?(\\d*)?(\\[.*\\])?"].groups() - if messageGroups[1].hasPrefix("42") { + if messageGroups[1].hasPrefix("2") { var mesNum = messageGroups[1] var ackNum:String var namespace:String? @@ -550,7 +539,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { if messageGroups[3] != "" { ackNum = messageGroups[3] } else { - let range = Range(start: mesNum.startIndex, end: advance(mesNum.startIndex, 2)) + let range = Range(start: mesNum.startIndex, end: advance(mesNum.startIndex, 1)) mesNum.replaceRange(range, with: "") ackNum = mesNum } @@ -615,7 +604,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } return } - } else if messageGroups[1].hasPrefix("43") { + } else if messageGroups[1].hasPrefix("3") { let arr = Array(messageGroups[1]) var ackNum:String let nsp = messageGroups[2] @@ -625,7 +614,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } if nsp == "" { - ackNum = String(arr[2...arr.count-1]) + ackNum = String(arr[1...arr.count-1]) } else { ackNum = messageGroups[3] } @@ -645,7 +634,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { // Message is binary if let binary = message as? NSData { - if self.lastSocketMessage == nil { + if self.waitingData.isEmpty { return } @@ -669,7 +658,7 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { return } - if binaryGroup[1].hasPrefix("45") { + if binaryGroup[1].hasPrefix("5") { // println(binaryGroup) var ackNum:String var event:String @@ -687,12 +676,11 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { ackNum = "" } - numberOfPlaceholders = (messageType["45"] ~= "") as String + numberOfPlaceholders = (messageType["5"] ~= "") as String event = (RegexMutable(binaryGroup[4])["\""] ~= "") as String mutMessageObject = RegexMutable(binaryGroup[5]) if namespace == "" && self.nsp != nil { - self.lastSocketMessage = nil return } @@ -709,10 +697,10 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { placeholders: numberOfPlaceholders.toInt()!, ackNum: ackNum.toInt()) } - self.lastSocketMessage = mes - } else if binaryGroup[1].hasPrefix("46") { + self.waitingData.append(mes) + } else if binaryGroup[1].hasPrefix("6") { let messageType = RegexMutable(binaryGroup[1]) - let numberOfPlaceholders = (messageType["46"] ~= "") as String + let numberOfPlaceholders = (messageType["6"] ~= "") as String var ackNum:String var nsp:String @@ -731,8 +719,10 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { let placeholdersRemoved = mutMessageObject["(\\{\"_placeholder\":true,\"num\":(\\d*)\\})"] ~= "\"~~$2\"" - self.lastSocketMessage = SocketEvent(event: "", args: placeholdersRemoved, + let event = SocketEvent(event: "", args: placeholdersRemoved, placeholders: numberOfPlaceholders.toInt()!, ackNum: ackNum.toInt(), justAck: true) + + self.waitingData.append(event) } /** End check for binary placeholders @@ -742,37 +732,42 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { // Handles binary data private func parseBinaryData(data:NSData) { - let shouldExecute = self.lastSocketMessage?.addData(data) + let shouldExecute = self.waitingData[0].addData(data) - if shouldExecute != nil && shouldExecute! { - var event = self.lastSocketMessage!.event - var parsedArgs:AnyObject? = SocketIOClient.parseData(self.lastSocketMessage!.args as? String) + if shouldExecute { + let socketEvent = self.waitingData.removeAtIndex(0) + var event = socketEvent.event + var parsedArgs:AnyObject? = SocketIOClient.parseData(socketEvent.args as? String) if let args:AnyObject = parsedArgs { - let filledInArgs:AnyObject = self.lastSocketMessage!.fillInPlaceholders(args) + let filledInArgs:AnyObject = socketEvent.fillInPlaceholders(args) - if self.lastSocketMessage!.justAck! { - self.handleAck(self.lastSocketMessage!.ack!, data: filledInArgs) + if socketEvent.justAck! { + // Should handle ack + self.handleAck(socketEvent.ack!, data: filledInArgs) return } - if self.lastSocketMessage!.ack != nil { + // Should do event + if socketEvent.ack != nil { self.handleEvent(event, data: filledInArgs, isInternalMessage: false, - wantsAck: self.lastSocketMessage!.ack!, withAckType: 6) + wantsAck: socketEvent.ack!, withAckType: 6) } else { self.handleEvent(event, data: filledInArgs) } } else { - let filledInArgs:AnyObject = self.lastSocketMessage!.fillInPlaceholders() + let filledInArgs:AnyObject = socketEvent.fillInPlaceholders() - if self.lastSocketMessage!.justAck! { - self.handleAck(self.lastSocketMessage!.ack!, data: filledInArgs) + // Should handle ack + if socketEvent.justAck! { + self.handleAck(socketEvent.ack!, data: filledInArgs) return } - if self.lastSocketMessage!.ack != nil { + // Should handle ack + if socketEvent.ack != nil { self.handleEvent(event, data: filledInArgs, isInternalMessage: false, - wantsAck: self.lastSocketMessage!.ack!, withAckType: 6) + wantsAck: socketEvent.ack!, withAckType: 6) } else { self.handleEvent(event, data: filledInArgs) } @@ -780,27 +775,19 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } } - func sendPing() { - if self.connected { - self.io?.send("2") - } - } - - // Starts the ping timer - private func startPingTimer(#interval:Int) { - dispatch_async(dispatch_get_main_queue()) { - self.pingTimer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(interval), target: self, - selector: Selector("sendPing"), userInfo: nil, repeats: true) + // Something happened while polling + func pollingDidFail(err:NSError?) { + if !self.reconnecting { + self.connected = false + self.handleEvent("reconnect", data: err?.localizedDescription, isInternalMessage: true) + self.tryReconnect() } } // We lost connection and should attempt to reestablish - private func tryReconnect(var #triesLeft:Int) { - if triesLeft != -1 && triesLeft <= 0 { - self.connecting = false - self.reconnects = false - self.reconnecting = false - self.handleEvent("disconnect", data: "Failed to reconnect", isInternalMessage: true) + @objc func tryReconnect() { + if self.reconnectAttempts != -1 && self.currentReconnectAttempt + 1 > self.reconnectAttempts { + self.didForceClose() return } else if self.connected { self.connecting = false @@ -808,26 +795,25 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { return } - // println("Trying to reconnect #\(reconnectAttempts - triesLeft)") - self.handleEvent("reconnectAttempt", data: triesLeft, isInternalMessage: true) - - let waitTime = UInt64(self.reconnectWait) * NSEC_PER_SEC - let time = dispatch_time(DISPATCH_TIME_NOW, Int64(waitTime)) - - // Wait reconnectWait seconds and then check if connected. Repeat if not - dispatch_after(time, dispatch_get_main_queue()) {[weak self] in - if self == nil || self!.connected || self!.closed { + if self.reconnectTimer == nil { + self.reconnecting = true + dispatch_async(dispatch_get_main_queue()) {[weak self] in + if self == nil { + return + } + + self?.reconnectTimer = NSTimer.scheduledTimerWithTimeInterval(Double(self!.reconnectWait), + target: self!, selector: "tryReconnect", userInfo: nil, repeats: true) return } - if triesLeft != -1 { - triesLeft = triesLeft - 1 - } - - self!.tryReconnect(triesLeft: triesLeft) + return } - self.reconnecting = true + self.handleEvent("reconnectAttempt", data: self.reconnectAttempts - self.currentReconnectAttempt, + isInternalMessage: true) + + self.currentReconnectAttempt++ if self.paramConnect { self.connectWithParams(self.params) } else { @@ -835,60 +821,28 @@ class SocketIOClient: NSObject, SRWebSocketDelegate { } } - // Called when a message is recieved - func webSocket(webSocket:SRWebSocket!, didReceiveMessage message:AnyObject?) { - dispatch_async(self.handleQueue) {[weak self] in - if self == nil { - return - } - - self?.parseSocketMessage(message) - } - } - - // Called when the socket is opened - func webSocketDidOpen(webSocket:SRWebSocket!) { - self.closed = false - self.connecting = false - self.reconnecting = false - self.connected = true - - if self.nsp != nil { - // Join namespace - self.joinNamespace() - return - } - - // Don't handle as internal because something crazy could happen where - // we disconnect before it's handled - self.handleEvent("connect", data: nil) - } - // Called when the socket is closed - func webSocket(webSocket:SRWebSocket!, didCloseWithCode code:Int, reason:String!, wasClean:Bool) { - self.pingTimer?.invalidate() + func webSocketDidCloseWithCode(code:Int, reason:String!, wasClean:Bool) { self.connected = false self.connecting = false if self.closed || !self.reconnects { - self.handleEvent("disconnect", data: reason, isInternalMessage: true) + self.didForceClose() } else { self.handleEvent("reconnect", data: reason, isInternalMessage: true) - self.tryReconnect(triesLeft: self.reconnectAttempts) - + self.tryReconnect() } } // Called when an error occurs. - func webSocket(webSocket:SRWebSocket!, didFailWithError error:NSError!) { - self.pingTimer?.invalidate() + func webSocketDidFailWithError(error:NSError!) { self.connected = false self.connecting = false self.handleEvent("error", data: error.localizedDescription, isInternalMessage: true) if self.closed || !self.reconnects { - self.handleEvent("disconnect", data: error.localizedDescription, isInternalMessage: true) + self.didForceClose() } else if !self.reconnecting { self.handleEvent("reconnect", data: error.localizedDescription, isInternalMessage: true) - self.tryReconnect(triesLeft: self.reconnectAttempts) + self.tryReconnect() } } -} +} \ No newline at end of file