diff --git a/Info.plist b/Info.plist
index 6ba5383..40bd29e 100644
--- a/Info.plist
+++ b/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 0.1.5
+ 0.1.9
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPrincipalClass
diff --git a/JFRWebSocket.h b/JFRWebSocket.h
index 8f09889..9997762 100644
--- a/JFRWebSocket.h
+++ b/JFRWebSocket.h
@@ -66,7 +66,7 @@
@property(nonatomic, readonly, nonnull) NSURL *url;
/**
- constructor to create a new websocket.
+ constructor to create a new websocket with QOS_CLASS_UTILITY dispatch queue
@param url the host you want to connect to.
@param protocols the websocket protocols you want to use (e.g. chat,superchat).
@return a newly initalized websocket.
@@ -74,9 +74,29 @@
- (nonnull instancetype)initWithURL:(nonnull NSURL *)url protocols:(nullable NSArray*)protocols;
/**
- connect to the host.
+ constructor to create a new websocket
+ @param url the host you want to connect to.
+ @param protocols the websocket protocols you want to use (e.g. chat,superchat).
+ @param callbackQueue the dispatch queue for handling callbacks
+ @return a newly initalized websocket.
+ */
+- (nonnull instancetype)initWithURLAndQueue:(nonnull NSURL *)url protocols:(nonnull NSArray*)protocols callbackQueue:(nonnull dispatch_queue_t)callbackQueue;
+
+/**
+ constructor to create a new websocket
+ @param url the host you want to connect to.
+ @param protocols the websocket protocols you want to use (e.g. chat,superchat).
+ @param callbackQueue the dispatch queue for handling callbacks
+ @param connectTimeout timeout for blocking connect
+ @return a newly initalized websocket.
+ */
+- (nonnull instancetype)initWithURL:(NSURL *)url protocols:(NSArray*)protocols callbackQueue:(dispatch_queue_t)callbackQueue connectTimeout:(NSTimeInterval)connectTimeout;
+
+/**
+ connect to the host - blocking
+ @return YES if successfully connected
*/
-- (void)connect;
+- (BOOL)connect;
/**
disconnect to the host. This sends the close Connection opcode to terminate cleanly.
diff --git a/JFRWebSocket.m b/JFRWebSocket.m
index 22d7d0a..434d6fc 100644
--- a/JFRWebSocket.m
+++ b/JFRWebSocket.m
@@ -48,7 +48,8 @@ typedef NS_ENUM(NSUInteger, JFRCloseCode) {
typedef NS_ENUM(NSUInteger, JFRInternalErrorCode) {
// 0-999 WebSocket status codes not used
- JFROutputStreamWriteError = 1
+ JFROutputStreamWriteError = 1,
+ JFRConnectTimeout = 2
};
#define kJFRInternalHTTPStatusWebSocket 101
@@ -70,7 +71,6 @@ @interface JFRWebSocket ()
@property(nonatomic, strong, null_unspecified)NSInputStream *inputStream;
@property(nonatomic, strong, null_unspecified)NSOutputStream *outputStream;
@property(nonatomic, strong, null_unspecified)NSOperationQueue *writeQueue;
-@property(nonatomic, assign)BOOL isRunLoop;
@property(nonatomic, strong, nonnull)NSMutableArray *readStack;
@property(nonatomic, strong, nonnull)NSMutableArray *inputQueue;
@property(nonatomic, strong, nullable)NSData *fragBuffer;
@@ -79,6 +79,10 @@ @interface JFRWebSocket ()
@property(nonatomic, assign)BOOL isCreated;
@property(nonatomic, assign)BOOL didDisconnect;
@property(nonatomic, assign)BOOL certValidated;
+@property(nonatomic, assign)NSTimeInterval connectTimeout;
+
+@property (strong) NSThread *wsThread;
+
@end
@@ -109,43 +113,54 @@ @interface JFRWebSocket ()
static const uint8_t JFRPayloadLenMask = 0x7F;
static const size_t JFRMaxFrameSize = 32;
+
+static const NSTimeInterval JFRDefaultConnectTimeout = 10.0;
+
@implementation JFRWebSocket
+
/////////////////////////////////////////////////////////////////////////////
//Default initializer
- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray*)protocols
+{
+ return [self initWithURL:url protocols:protocols callbackQueue:dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)];
+}
+
+/////////////////////////////////////////////////////////////////////////////
+//Initialized with custom dispatch queue
+- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray*)protocols callbackQueue:(dispatch_queue_t)callbackQueue
+{
+ return [self initWithURL:url protocols:protocols callbackQueue:callbackQueue connectTimeout:JFRDefaultConnectTimeout];
+}
+
+/////////////////////////////////////////////////////////////////////////////
+//Initialized with custom dispatch queue and connection timeout
+- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray*)protocols callbackQueue:(dispatch_queue_t)callbackQueue connectTimeout:(NSTimeInterval)connectTimeout
{
if(self = [super init]) {
self.certValidated = NO;
self.voipEnabled = NO;
self.selfSignedSSL = NO;
- self.queue = dispatch_get_main_queue();
+ self.queue = callbackQueue;
self.url = url;
self.readStack = [NSMutableArray new];
self.inputQueue = [NSMutableArray new];
self.optProtocols = protocols;
+ self.connectTimeout = connectTimeout;
}
-
return self;
}
+
/////////////////////////////////////////////////////////////////////////////
//Exposed method for connecting to URL provided in init method.
-- (void)connect {
- if(self.isCreated) {
- return;
+- (BOOL)connect {
+ @synchronized (self) {
+ if (self.wsThread != nil) {
+ return YES;
+ }
+ self.didDisconnect = NO;
+ return [self createHTTPRequest];
}
-
- __weak typeof(self) weakSelf = self;
- dispatch_async(self.queue, ^{
- weakSelf.didDisconnect = NO;
- });
-
- //everything is on a background thread.
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- weakSelf.isCreated = YES;
- [weakSelf createHTTPRequest];
- weakSelf.isCreated = NO;
- });
}
/////////////////////////////////////////////////////////////////////////////
- (void)disconnect {
@@ -153,10 +168,13 @@ - (void)disconnect {
}
/////////////////////////////////////////////////////////////////////////////
- (void)writeString:(NSString*)string {
- if(string) {
- [self dequeueWrite:[string dataUsingEncoding:NSUTF8StringEncoding]
- withCode:JFROpCodeTextFrame];
+ @autoreleasepool {
+ if(string) {
+ [self dequeueWrite:[string dataUsingEncoding:NSUTF8StringEncoding]
+ withCode:JFROpCodeTextFrame];
+ }
}
+
}
/////////////////////////////////////////////////////////////////////////////
- (void)writePing:(NSData*)data {
@@ -198,7 +216,7 @@ - (NSString *)origin;
//Uses CoreFoundation to build a HTTP request to send over TCP stream.
-- (void)createHTTPRequest {
+- (BOOL)createHTTPRequest {
CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)self.url.absoluteString, NULL);
CFStringRef requestMethod = CFSTR("GET");
CFHTTPMessageRef urlRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
@@ -254,8 +272,8 @@ - (void)createHTTPRequest {
NSLog(@"urlRequest = \"%@\"", urlRequest);
#endif
NSData *serializedRequest = (__bridge_transfer NSData *)(CFHTTPMessageCopySerializedMessage(urlRequest));
- [self initStreamsWithData:serializedRequest port:port];
CFRelease(urlRequest);
+ return [self initStreamsWithData:serializedRequest port:port];
}
/////////////////////////////////////////////////////////////////////////////
//Random String of 16 lowercase chars, SHA1 and base64 encoded.
@@ -267,47 +285,85 @@ - (NSString*)generateWebSocketKey {
}
return [[string dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0];
}
+
/////////////////////////////////////////////////////////////////////////////
-//Sets up our reader/writer for the TCP stream.
-- (void)initStreamsWithData:(NSData*)data port:(NSNumber*)port {
- CFReadStreamRef readStream = NULL;
- CFWriteStreamRef writeStream = NULL;
- CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)self.url.host, [port intValue], &readStream, &writeStream);
-
- self.inputStream = (__bridge_transfer NSInputStream *)readStream;
- self.inputStream.delegate = self;
- self.outputStream = (__bridge_transfer NSOutputStream *)writeStream;
- self.outputStream.delegate = self;
- if([self.url.scheme isEqualToString:@"wss"] || [self.url.scheme isEqualToString:@"https"]) {
- [self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
- [self.outputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
- } else {
- self.certValidated = YES; //not a https session, so no need to check SSL pinning
- }
- if(self.voipEnabled) {
- [self.inputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
- [self.outputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
- }
- if(self.selfSignedSSL) {
- NSString *chain = (__bridge_transfer NSString *)kCFStreamSSLValidatesCertificateChain;
- NSString *peerName = (__bridge_transfer NSString *)kCFStreamSSLValidatesCertificateChain;
- NSString *key = (__bridge_transfer NSString *)kCFStreamPropertySSLSettings;
- NSDictionary *settings = @{chain: [[NSNumber alloc] initWithBool:NO],
- peerName: [NSNull null]};
- [self.inputStream setProperty:settings forKey:key];
- [self.outputStream setProperty:settings forKey:key];
- }
- self.isRunLoop = YES;
+// socket i/o handler for seperate thread
+-(void)handleStream:(id)object
+{
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- [self.inputStream open];
- [self.outputStream open];
- size_t dataLen = [data length];
- [self.outputStream write:[data bytes] maxLength:dataLen];
- while (self.isRunLoop) {
+
+ while (![[NSThread currentThread] isCancelled]) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
+
+/////////////////////////////////////////////////////////////////////////////
+//Sets up our reader/writer for the TCP stream.
+- (BOOL)initStreamsWithData:(NSData*)data port:(NSNumber*)port {
+ @autoreleasepool {
+ CFReadStreamRef readStream = NULL;
+ CFWriteStreamRef writeStream = NULL;
+ CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)self.url.host, [port intValue], &readStream, &writeStream);
+
+ self.inputStream = (__bridge_transfer NSInputStream *)readStream;
+ self.inputStream.delegate = self;
+ self.outputStream = (__bridge_transfer NSOutputStream *)writeStream;
+ self.outputStream.delegate = self;
+ if([self.url.scheme isEqualToString:@"wss"] || [self.url.scheme isEqualToString:@"https"]) {
+ [self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
+ [self.outputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
+ } else {
+ self.certValidated = YES; //not a https session, so no need to check SSL pinning
+ }
+ if(self.voipEnabled) {
+ [self.inputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
+ [self.outputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
+ }
+ if(self.selfSignedSSL) {
+ NSString *chain = (__bridge_transfer NSString *)kCFStreamSSLValidatesCertificateChain;
+ NSString *peerName = (__bridge_transfer NSString *)kCFStreamSSLValidatesCertificateChain;
+ NSString *key = (__bridge_transfer NSString *)kCFStreamPropertySSLSettings;
+ NSDictionary *settings = @{chain: [[NSNumber alloc] initWithBool:NO],
+ peerName: [NSNull null]};
+ [self.inputStream setProperty:settings forKey:key];
+ [self.outputStream setProperty:settings forKey:key];
+ }
+
+ [self.inputStream open];
+ [self.outputStream open];
+ size_t dataLen = [data length];
+ [self.outputStream write:[data bytes] maxLength:dataLen];
+
+ [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+ [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+ NSDate *timeoutDate = [[NSDate date] dateByAddingTimeInterval:self.connectTimeout];
+ while (!self.isConnected) {
+ //process initial connect request synchronously
+ if ((!self.isConnected && [[NSDate date] compare:timeoutDate] == NSOrderedDescending)) {
+ [self disconnectStream:[self errorWithDetail:@"Websocket connect timeout" code:JFRConnectTimeout]];
+ return NO;
+ }
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[timeoutDate dateByAddingTimeInterval:1.0]]; // Add 1 sec to prevent race condition with beforeDate being in the past
+ }
+
+ //init worker thread
+ self.wsThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleStream:) object:nil];
+ self.wsThread.name = @"jetfire.ws.worker";
+
+ [self.inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+ [self.outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+
+ if (self.wsThread != nil) {
+ // delegate work to worker thread
+ [self.wsThread start];
+ return YES;
+ }
+ return NO;
+ }
+}
+
+
/////////////////////////////////////////////////////////////////////////////
#pragma mark - NSStreamDelegate
@@ -361,7 +417,10 @@ - (void)disconnectStream:(NSError*)error {
[self.inputStream close];
self.outputStream = nil;
self.inputStream = nil;
- self.isRunLoop = NO;
+ if (self.wsThread != nil && !self.wsThread.isCancelled) {
+ [self.wsThread cancel];
+ }
+ self.wsThread = nil;
_isConnected = NO;
self.certValidated = NO;
[self doDisconnect:error];
@@ -441,7 +500,7 @@ - (BOOL)processHTTP:(uint8_t*)buffer length:(NSInteger)bufferLen responseStatusC
_isConnected = YES;
__weak typeof(self) weakSelf = self;
dispatch_async(self.queue,^{
- if([self.delegate respondsToSelector:@selector(websocketDidConnect:)]) {
+ if([weakSelf.delegate respondsToSelector:@selector(websocketDidConnect:)]) {
[weakSelf.delegate websocketDidConnect:self];
}
if(weakSelf.onConnect) {
@@ -645,38 +704,40 @@ - (void)processExtra:(uint8_t*)buffer length:(NSInteger)bufferLen {
}
/////////////////////////////////////////////////////////////////////////////
- (BOOL)processResponse:(JFRResponse*)response {
- if(response.isFin && response.bytesLeft <= 0) {
- NSData *data = response.buffer;
- if(response.code == JFROpCodePing) {
- [self dequeueWrite:response.buffer withCode:JFROpCodePong];
- } else if(response.code == JFROpCodeTextFrame) {
- NSString *str = [[NSString alloc] initWithData:response.buffer encoding:NSUTF8StringEncoding];
- if(!str) {
- [self writeError:JFRCloseCodeEncoding];
- return NO;
- }
- __weak typeof(self) weakSelf = self;
- dispatch_async(self.queue,^{
- if([weakSelf.delegate respondsToSelector:@selector(websocket:didReceiveMessage:)]) {
- [weakSelf.delegate websocket:weakSelf didReceiveMessage:str];
- }
- if(weakSelf.onText) {
- weakSelf.onText(str);
- }
- });
- } else if(response.code == JFROpCodeBinaryFrame) {
- __weak typeof(self) weakSelf = self;
- dispatch_async(self.queue,^{
- if([weakSelf.delegate respondsToSelector:@selector(websocket:didReceiveData:)]) {
- [weakSelf.delegate websocket:weakSelf didReceiveData:data];
- }
- if(weakSelf.onData) {
- weakSelf.onData(data);
+ @autoreleasepool {
+ if(response.isFin && response.bytesLeft <= 0) {
+ NSData *data = response.buffer;
+ if(response.code == JFROpCodePing) {
+ [self dequeueWrite:response.buffer withCode:JFROpCodePong];
+ } else if(response.code == JFROpCodeTextFrame) {
+ NSString *str = [[NSString alloc] initWithData:response.buffer encoding:NSUTF8StringEncoding];
+ if(!str) {
+ [self writeError:JFRCloseCodeEncoding];
+ return NO;
}
- });
+ __weak typeof(self) weakSelf = self;
+ dispatch_async(self.queue,^{
+ if([weakSelf.delegate respondsToSelector:@selector(websocket:didReceiveMessage:)]) {
+ [weakSelf.delegate websocket:weakSelf didReceiveMessage:str];
+ }
+ if(weakSelf.onText) {
+ weakSelf.onText(str);
+ }
+ });
+ } else if(response.code == JFROpCodeBinaryFrame) {
+ __weak typeof(self) weakSelf = self;
+ dispatch_async(self.queue,^{
+ if([weakSelf.delegate respondsToSelector:@selector(websocket:didReceiveData:)]) {
+ [weakSelf.delegate websocket:weakSelf didReceiveData:data];
+ }
+ if(weakSelf.onData) {
+ weakSelf.onData(data);
+ }
+ });
+ }
+ [self.readStack removeLastObject];
+ return YES;
}
- [self.readStack removeLastObject];
- return YES;
}
return NO;
}