Skip to content

Commit fde88c1

Browse files
committed
Support both v2 and v3
1 parent 21290f5 commit fde88c1

10 files changed

+219
-27
lines changed

Source/SocketIO/Client/SocketIOClient.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ open class SocketIOClient: NSObject, SocketIOClientSpec {
134134

135135
joinNamespace(withPayload: payload)
136136

137+
switch manager.version {
138+
case .three:
139+
break
140+
case .two where manager.status == .connected && nsp == "/":
141+
// We might not get a connect event for the default nsp, fire immediately
142+
didConnect(toNamespace: nsp, payload: nil)
143+
144+
return
145+
case _:
146+
break
147+
}
148+
137149
guard timeoutAfter != 0 else { return }
138150

139151
manager.handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in

Source/SocketIO/Client/SocketIOClientOption.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
import Foundation
2626
import Starscream
2727

28+
/// The socket.io version being used.
29+
public enum SocketIOVersion: Int {
30+
case two = 2
31+
case three = 3
32+
}
33+
2834
protocol ClientOption : CustomStringConvertible, Equatable {
2935
func getSocketIOOptionValue() -> Any
3036
}
@@ -99,6 +105,9 @@ public enum SocketIOClientOption : ClientOption {
99105
/// Sets an NSURLSessionDelegate for the underlying engine. Useful if you need to handle self-signed certs.
100106
case sessionDelegate(URLSessionDelegate)
101107

108+
/// The version of socket.io being used. This should match the server version. Default is 3.
109+
case version(SocketIOVersion)
110+
102111
// MARK: Properties
103112

104113
/// The description of this option.
@@ -148,6 +157,8 @@ public enum SocketIOClientOption : ClientOption {
148157
description = "sessionDelegate"
149158
case .enableSOCKSProxy:
150159
description = "enableSOCKSProxy"
160+
case .version:
161+
description = "version"
151162
}
152163

153164
return description
@@ -199,6 +210,8 @@ public enum SocketIOClientOption : ClientOption {
199210
value = delegate
200211
case let .enableSOCKSProxy(enable):
201212
value = enable
213+
case let.version(versionNum):
214+
value = versionNum
202215
}
203216

204217
return value

Source/SocketIO/Engine/SocketEngine.swift

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ open class SocketEngine:
111111
/// The url for WebSockets.
112112
public private(set) var urlWebSocket = URL(string: "http://localhost/")!
113113

114+
/// The version of engine.io being used. Default is three.
115+
public private(set) var version: SocketIOVersion = .three
116+
114117
/// If `true`, then the engine is currently in WebSockets mode.
115118
@available(*, deprecated, message: "No longer needed, if we're not polling, then we must be doing websockets")
116119
public private(set) var websocket = false
@@ -133,8 +136,14 @@ open class SocketEngine:
133136

134137
private var lastCommunication: Date?
135138
private var pingInterval: Int?
136-
private var pingTimeout = 0
139+
private var pingTimeout = 0 {
140+
didSet {
141+
pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25000))
142+
}
143+
}
137144

145+
private var pongsMissed = 0
146+
private var pongsMissedMax = 0
138147
private var probeWait = ProbeWaitQueue()
139148
private var secure = false
140149
private var certPinner: CertificatePinning?
@@ -196,8 +205,9 @@ open class SocketEngine:
196205
}
197206

198207
private func handleBase64(message: String) {
208+
let offset = version.rawValue >= 3 ? 1 : 2
199209
// binary in base64 string
200-
let noPrefix = String(message[message.index(message.startIndex, offsetBy: 1)..<message.endIndex])
210+
let noPrefix = String(message[message.index(message.startIndex, offsetBy: offset)..<message.endIndex])
201211

202212
if let data = Data(base64Encoded: noPrefix, options: .ignoreUnknownCharacters) {
203213
client?.parseEngineBinaryData(data)
@@ -278,6 +288,14 @@ open class SocketEngine:
278288
urlWebSocket.percentEncodedQuery = "transport=websocket" + queryString
279289
urlPolling.percentEncodedQuery = "transport=polling&b64=1" + queryString
280290

291+
if !urlWebSocket.percentEncodedQuery!.contains("EIO") {
292+
urlWebSocket.percentEncodedQuery = urlWebSocket.percentEncodedQuery! + engineIOParam
293+
}
294+
295+
if !urlPolling.percentEncodedQuery!.contains("EIO") {
296+
urlPolling.percentEncodedQuery = urlPolling.percentEncodedQuery! + engineIOParam
297+
}
298+
281299
return (urlPolling.url!, urlWebSocket.url!)
282300
}
283301

@@ -289,6 +307,8 @@ open class SocketEngine:
289307
includingCookies: session?.configuration.httpCookieStorage?.cookies(for: urlPollingWithSid)
290308
)
291309

310+
print("ws req: \(req)")
311+
292312
ws = WebSocket(request: req, certPinner: certPinner, compressionHandler: compress ? WSCompression() : nil)
293313
ws?.callbackQueue = engineQueue
294314
ws?.delegate = self
@@ -413,6 +433,7 @@ open class SocketEngine:
413433

414434
self.sid = sid
415435
connected = true
436+
pongsMissed = 0
416437

417438
if let upgrades = json["upgrades"] as? [String] {
418439
upgradeWs = upgrades.contains("websocket")
@@ -429,26 +450,37 @@ open class SocketEngine:
429450
createWebSocketAndConnect()
430451
}
431452

453+
if version.rawValue >= 3 {
454+
checkPings()
455+
} else {
456+
sendPing()
457+
}
458+
432459
if !forceWebsockets {
433460
doPoll()
434461
}
435462

436-
checkPings()
437463
client?.engineDidOpen(reason: "Connect")
438464
}
439465

440466
private func handlePong(with message: String) {
467+
pongsMissed = 0
468+
441469
// We should upgrade
442470
if message == "3probe" {
443471
DefaultSocketLogger.Logger.log("Received probe response, should upgrade to WebSockets",
444472
type: SocketEngine.logType)
445473

446474
upgradeTransport()
447475
}
476+
477+
client?.engineDidReceivePong()
448478
}
449479

450480
private func handlePing(with message: String) {
451-
write("", withType: .pong, withData: [])
481+
if version.rawValue >= 3 {
482+
write("", withType: .pong, withData: [])
483+
}
452484

453485
client?.engineDidReceivePing()
454486
}
@@ -478,7 +510,7 @@ open class SocketEngine:
478510

479511
lastCommunication = Date()
480512

481-
client?.parseEngineBinaryData(data)
513+
client?.parseEngineBinaryData(version.rawValue >= 3 ? data : data.subdata(in: 1..<data.endIndex))
482514
}
483515

484516
/// Parses a raw engine.io packet.
@@ -489,13 +521,11 @@ open class SocketEngine:
489521

490522
DefaultSocketLogger.Logger.log("Got message: \(message)", type: SocketEngine.logType)
491523

492-
let reader = SocketStringReader(message: message)
493-
494-
if message.hasPrefix("b") {
524+
if message.hasPrefix(version.rawValue >= 3 ? "b" : "b4") {
495525
return handleBase64(message: message)
496526
}
497527

498-
guard let type = SocketEnginePacketType(rawValue: Int(reader.currentCharacter) ?? -1) else {
528+
guard let type = SocketEnginePacketType(rawValue: message.first?.wholeNumberValue ?? -1) else {
499529
checkAndHandleEngineError(message)
500530

501531
return
@@ -536,6 +566,34 @@ open class SocketEngine:
536566
waitingForPost = false
537567
}
538568

569+
private func sendPing() {
570+
guard connected, let pingInterval = pingInterval else {
571+
print("not connected \(self.connected) or no ping interval \(self.pingInterval ?? -222)")
572+
return
573+
}
574+
575+
// Server is not responding
576+
if pongsMissed > pongsMissedMax {
577+
closeOutEngine(reason: "Ping timeout")
578+
return
579+
}
580+
581+
pongsMissed += 1
582+
write("", withType: .ping, withData: [], completion: nil)
583+
584+
engineQueue.asyncAfter(deadline: .now() + .milliseconds(pingInterval)) {[weak self, id = self.sid] in
585+
// Make sure not to ping old connections
586+
guard let this = self, this.sid == id else {
587+
print("wrong ping?")
588+
return
589+
}
590+
591+
this.sendPing()
592+
}
593+
594+
client?.engineDidSendPing()
595+
}
596+
539597
/// Called when the engine should set/update its configs from a given configuration.
540598
///
541599
/// parameter config: The `SocketIOClientConfiguration` that should be used to set/update configs.
@@ -570,6 +628,8 @@ open class SocketEngine:
570628
self.compress = true
571629
case .enableSOCKSProxy:
572630
self.enableSOCKSProxy = true
631+
case let .version(num):
632+
version = num
573633
default:
574634
continue
575635
}

Source/SocketIO/Engine/SocketEngineClient.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,16 @@ import Foundation
4444
/// - parameter reason: The reason the engine opened.
4545
func engineDidOpen(reason: String)
4646

47-
/// Called when the engine receives a ping message.
47+
/// Called when the engine receives a ping message. Only called in socket.io >3.
4848
func engineDidReceivePing()
4949

50-
/// Called when the engine sends a pong to the server.
50+
/// Called when the engine receives a pong message. Only called in socket.io 2.
51+
func engineDidReceivePong()
52+
53+
/// Called when the engine sends a ping to the server. Only called in socket.io 2.
54+
func engineDidSendPing()
55+
56+
/// Called when the engine sends a pong to the server. Only called in socket.io >3.
5157
func engineDidSendPong()
5258

5359
/// Called when the engine has a message that must be parsed.

Source/SocketIO/Engine/SocketEnginePollable.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,15 @@ extension SocketEnginePollable {
7979
postWait.removeAll(keepingCapacity: true)
8080
}
8181

82-
let postStr = postWait.lazy.map({ $0.msg }).joined(separator: "\u{1e}")
82+
var postStr = ""
83+
84+
if version.rawValue >= 3 {
85+
postStr = postWait.lazy.map({ $0.msg }).joined(separator: "\u{1e}")
86+
} else {
87+
for packet in postWait {
88+
postStr += "\(packet.msg.utf16.count):\(packet.msg)"
89+
}
90+
}
8391

8492
DefaultSocketLogger.Logger.log("Created POST string: \(postStr)", type: "SocketEnginePolling")
8593

@@ -195,10 +203,29 @@ extension SocketEnginePollable {
195203

196204
DefaultSocketLogger.Logger.log("Got poll message: \(str)", type: "SocketEnginePolling")
197205

198-
let records = str.components(separatedBy: "\u{1e}")
206+
if version.rawValue >= 3 {
207+
let records = str.components(separatedBy: "\u{1e}")
208+
209+
for record in records {
210+
parseEngineMessage(record)
211+
}
212+
} else {
213+
guard str.count != 1 else {
214+
parseEngineMessage(str)
215+
216+
return
217+
}
218+
219+
var reader = SocketStringReader(message: str)
199220

200-
for record in records {
201-
parseEngineMessage(record)
221+
while reader.hasNext {
222+
if let n = Int(reader.readUntilOccurence(of: ":")) {
223+
parseEngineMessage(reader.read(count: n))
224+
} else {
225+
parseEngineMessage(str)
226+
break
227+
}
228+
}
202229
}
203230
}
204231

Source/SocketIO/Engine/SocketEngineSpec.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ public protocol SocketEngineSpec: class {
8181
/// The url for WebSockets.
8282
var urlWebSocket: URL { get }
8383

84+
/// The version of engine.io being used. Default is three.
85+
var version: SocketIOVersion { get }
86+
8487
/// If `true`, then the engine is currently in WebSockets mode.
8588
@available(*, deprecated, message: "No longer needed, if we're not polling, then we must be doing websockets")
8689
var websocket: Bool { get }
@@ -142,17 +145,35 @@ public protocol SocketEngineSpec: class {
142145
}
143146

144147
extension SocketEngineSpec {
148+
var engineIOParam: String {
149+
switch version {
150+
case .two:
151+
return "&EIO=3"
152+
case .three:
153+
return "&EIO=4"
154+
}
155+
}
156+
145157
var urlPollingWithSid: URL {
146158
var com = URLComponents(url: urlPolling, resolvingAgainstBaseURL: false)!
147159
com.percentEncodedQuery = com.percentEncodedQuery! + "&sid=\(sid.urlEncode()!)"
148160

161+
if !com.percentEncodedQuery!.contains("EIO") {
162+
com.percentEncodedQuery = com.percentEncodedQuery! + engineIOParam
163+
}
164+
149165
return com.url!
150166
}
151167

152168
var urlWebSocketWithSid: URL {
153169
var com = URLComponents(url: urlWebSocket, resolvingAgainstBaseURL: false)!
154170
com.percentEncodedQuery = com.percentEncodedQuery! + (sid == "" ? "" : "&sid=\(sid.urlEncode()!)")
155171

172+
if !com.percentEncodedQuery!.contains("EIO") {
173+
com.percentEncodedQuery = com.percentEncodedQuery! + engineIOParam
174+
}
175+
176+
156177
return com.url!
157178
}
158179

@@ -172,10 +193,12 @@ extension SocketEngineSpec {
172193
}
173194

174195
func createBinaryDataForSend(using data: Data) -> Either<Data, String> {
196+
let prefixB64 = version.rawValue >= 3 ? "b" : "b4"
197+
175198
if polling {
176-
return .right("b" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)))
199+
return .right(prefixB64 + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)))
177200
} else {
178-
return .left(data)
201+
return .left(version.rawValue >= 3 ? data : Data([0x4]) + data)
179202
}
180203
}
181204

0 commit comments

Comments
 (0)