0,0 → 1,3184 |
// |
// AsyncSocket.m |
// |
// This class is in the public domain. |
// Originally created by Dustin Voss on Wed Jan 29 2003. |
// Updated and maintained by Deusty Designs and the Mac development community. |
// |
// http://code.google.com/p/cocoaasyncsocket/ |
// |
|
#import "AsyncSocket.h" |
#import <sys/socket.h> |
#import <netinet/in.h> |
#import <arpa/inet.h> |
#import <netdb.h> |
|
#if TARGET_OS_IPHONE |
// Note: You may need to add the CFNetwork Framework to your project |
#import <CFNetwork/CFNetwork.h> |
#endif |
|
#pragma mark Declarations |
|
#define DEFAULT_PREBUFFERING YES // Whether pre-buffering is enabled by default |
|
#define READQUEUE_CAPACITY 5 // Initial capacity |
#define WRITEQUEUE_CAPACITY 5 // Initial capacity |
#define READALL_CHUNKSIZE 256 // Incremental increase in buffer size |
#define WRITE_CHUNKSIZE (1024 * 4) // Limit on size of each write pass |
|
NSString *const AsyncSocketException = @"AsyncSocketException"; |
NSString *const AsyncSocketErrorDomain = @"AsyncSocketErrorDomain"; |
|
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 |
// Mutex lock used by all instances of AsyncSocket, to protect getaddrinfo. |
// Prior to Mac OS X 10.5 this method was not thread-safe. |
static NSString *getaddrinfoLock = @"lock"; |
#endif |
|
enum AsyncSocketFlags |
{ |
kEnablePreBuffering = 1 << 0, // If set, pre-buffering is enabled |
kDidPassConnectMethod = 1 << 1, // If set, disconnection results in delegate call |
kDidCompleteOpenForRead = 1 << 2, // If set, open callback has been called for read stream |
kDidCompleteOpenForWrite = 1 << 3, // If set, open callback has been called for write stream |
kStartingReadTLS = 1 << 4, // If set, we're waiting for TLS negotiation to complete |
kStartingWriteTLS = 1 << 5, // If set, we're waiting for TLS negotiation to complete |
kForbidReadsWrites = 1 << 6, // If set, no new reads or writes are allowed |
kDisconnectAfterReads = 1 << 7, // If set, disconnect after no more reads are queued |
kDisconnectAfterWrites = 1 << 8, // If set, disconnect after no more writes are queued |
kClosingWithError = 1 << 9, // If set, the socket is being closed due to an error |
kDequeueReadScheduled = 1 << 10, // If set, a maybeDequeueRead operation is already scheduled |
kDequeueWriteScheduled = 1 << 11, // If set, a maybeDequeueWrite operation is already scheduled |
kSocketCanAcceptBytes = 1 << 12, // If set, we know socket can accept bytes. If unset, it's unknown. |
kSocketHasBytesAvailable = 1 << 13, // If set, we know socket has bytes available. If unset, it's unknown. |
}; |
|
@interface AsyncSocket (Private) |
|
// Connecting |
- (void)startConnectTimeout:(NSTimeInterval)timeout; |
- (void)endConnectTimeout; |
|
// Socket Implementation |
- (CFSocketRef)newAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr; |
- (BOOL)createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr; |
- (BOOL)attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; |
- (BOOL)configureSocketAndReturnError:(NSError **)errPtr; |
- (BOOL)connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; |
- (void)doAcceptWithSocket:(CFSocketNativeHandle)newSocket; |
- (void)doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)err; |
|
// Stream Implementation |
- (BOOL)createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr; |
- (BOOL)createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr; |
- (BOOL)attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; |
- (BOOL)configureStreamsAndReturnError:(NSError **)errPtr; |
- (BOOL)openStreamsAndReturnError:(NSError **)errPtr; |
- (void)doStreamOpen; |
- (BOOL)setSocketFromStreamsAndReturnError:(NSError **)errPtr; |
|
// Disconnect Implementation |
- (void)closeWithError:(NSError *)err; |
- (void)recoverUnreadData; |
- (void)emptyQueues; |
- (void)close; |
|
// Errors |
- (NSError *)getErrnoError; |
- (NSError *)getAbortError; |
- (NSError *)getStreamError; |
- (NSError *)getSocketError; |
- (NSError *)getConnectTimeoutError; |
- (NSError *)getReadMaxedOutError; |
- (NSError *)getReadTimeoutError; |
- (NSError *)getWriteTimeoutError; |
- (NSError *)errorFromCFStreamError:(CFStreamError)err; |
|
// Diagnostics |
- (BOOL)isSocketConnected; |
- (BOOL)areStreamsConnected; |
- (NSString *)connectedHost:(CFSocketRef)socket; |
- (UInt16)connectedPort:(CFSocketRef)socket; |
- (NSString *)localHost:(CFSocketRef)socket; |
- (UInt16)localPort:(CFSocketRef)socket; |
- (NSString *)addressHost:(CFDataRef)cfaddr; |
- (UInt16)addressPort:(CFDataRef)cfaddr; |
|
// Reading |
- (void)doBytesAvailable; |
- (void)completeCurrentRead; |
- (void)endCurrentRead; |
- (void)scheduleDequeueRead; |
- (void)maybeDequeueRead; |
- (void)doReadTimeout:(NSTimer *)timer; |
|
// Writing |
- (void)doSendBytes; |
- (void)completeCurrentWrite; |
- (void)endCurrentWrite; |
- (void)scheduleDequeueWrite; |
- (void)maybeDequeueWrite; |
- (void)maybeScheduleDisconnect; |
- (void)doWriteTimeout:(NSTimer *)timer; |
|
// Run Loop |
- (void)runLoopAddSource:(CFRunLoopSourceRef)source; |
- (void)runLoopRemoveSource:(CFRunLoopSourceRef)source; |
- (void)runLoopAddTimer:(NSTimer *)timer; |
- (void)runLoopRemoveTimer:(NSTimer *)timer; |
- (void)runLoopUnscheduleReadStream; |
- (void)runLoopUnscheduleWriteStream; |
|
// Security |
- (void)maybeStartTLS; |
- (void)onTLSHandshakeSuccessful; |
|
// Callbacks |
- (void)doCFCallback:(CFSocketCallBackType)type forSocket:(CFSocketRef)sock withAddress:(NSData *)address withData:(const void *)pData; |
- (void)doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream; |
- (void)doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream; |
|
@end |
|
static void MyCFSocketCallback(CFSocketRef, CFSocketCallBackType, CFDataRef, const void *, void *); |
static void MyCFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo); |
static void MyCFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo); |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark - |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
/** |
* The AsyncReadPacket encompasses the instructions for any given read. |
* The content of a read packet allows the code to determine if we're: |
* - reading to a certain length |
* - reading to a certain separator |
* - or simply reading the first chunk of available data |
**/ |
@interface AsyncReadPacket : NSObject |
{ |
@public |
NSMutableData *buffer; |
CFIndex bytesDone; |
NSTimeInterval timeout; |
CFIndex maxLength; |
long tag; |
NSData *term; |
BOOL readAllAvailableData; |
} |
- (id)initWithData:(NSMutableData *)d |
timeout:(NSTimeInterval)t |
tag:(long)i |
readAllAvailable:(BOOL)a |
terminator:(NSData *)e |
maxLength:(CFIndex)m; |
|
- (unsigned)readLengthForTerm; |
|
- (unsigned)prebufferReadLengthForTerm; |
- (CFIndex)searchForTermAfterPreBuffering:(CFIndex)numBytes; |
@end |
|
@implementation AsyncReadPacket |
|
- (id)initWithData:(NSMutableData *)d |
timeout:(NSTimeInterval)t |
tag:(long)i |
readAllAvailable:(BOOL)a |
terminator:(NSData *)e |
maxLength:(CFIndex)m |
{ |
if((self = [super init])) |
{ |
buffer = [d retain]; |
timeout = t; |
tag = i; |
readAllAvailableData = a; |
term = [e copy]; |
bytesDone = 0; |
maxLength = m; |
} |
return self; |
} |
|
/** |
* For read packets with a set terminator, returns the safe length of data that can be read |
* without going over a terminator, or the maxLength. |
* |
* It is assumed the terminator has not already been read. |
**/ |
- (unsigned)readLengthForTerm |
{ |
NSAssert(term != nil, @"Searching for term in data when there is no term."); |
|
// What we're going to do is look for a partial sequence of the terminator at the end of the buffer. |
// If a partial sequence occurs, then we must assume the next bytes to arrive will be the rest of the term, |
// and we can only read that amount. |
// Otherwise, we're safe to read the entire length of the term. |
|
unsigned result = [term length]; |
|
// Shortcut when term is a single byte |
if(result == 1) return result; |
|
// i = index within buffer at which to check data |
// j = length of term to check against |
|
// Note: Beware of implicit casting rules |
// This could give you -1: MAX(0, (0 - [term length] + 1)); |
|
CFIndex i = MAX(0, (CFIndex)(bytesDone - [term length] + 1)); |
CFIndex j = MIN([term length] - 1, bytesDone); |
|
while(i < bytesDone) |
{ |
const void *subBuffer = [buffer bytes] + i; |
|
if(memcmp(subBuffer, [term bytes], j) == 0) |
{ |
result = [term length] - j; |
break; |
} |
|
i++; |
j--; |
} |
|
if(maxLength > 0) |
return MIN(result, (maxLength - bytesDone)); |
else |
return result; |
} |
|
/** |
* Assuming pre-buffering is enabled, returns the amount of data that can be read |
* without going over the maxLength. |
**/ |
- (unsigned)prebufferReadLengthForTerm |
{ |
if(maxLength > 0) |
return MIN(READALL_CHUNKSIZE, (maxLength - bytesDone)); |
else |
return READALL_CHUNKSIZE; |
} |
|
/** |
* For read packets with a set terminator, scans the packet buffer for the term. |
* It is assumed the terminator had not been fully read prior to the new bytes. |
* |
* If the term is found, the number of excess bytes after the term are returned. |
* If the term is not found, this method will return -1. |
* |
* Note: A return value of zero means the term was found at the very end. |
**/ |
- (CFIndex)searchForTermAfterPreBuffering:(CFIndex)numBytes |
{ |
NSAssert(term != nil, @"Searching for term in data when there is no term."); |
|
// We try to start the search such that the first new byte read matches up with the last byte of the term. |
// We continue searching forward after this until the term no longer fits into the buffer. |
|
// Note: Beware of implicit casting rules |
// This could give you -1: MAX(0, 1 - 1 - [term length] + 1); |
|
CFIndex i = MAX(0, (CFIndex)(bytesDone - numBytes - [term length] + 1)); |
|
while(i + [term length] <= bytesDone) |
{ |
const void *subBuffer = [buffer bytes] + i; |
|
if(memcmp(subBuffer, [term bytes], [term length]) == 0) |
{ |
return bytesDone - (i + [term length]); |
} |
|
i++; |
} |
|
return -1; |
} |
|
- (void)dealloc |
{ |
[buffer release]; |
[term release]; |
[super dealloc]; |
} |
|
@end |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark - |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
/** |
* The AsyncWritePacket encompasses the instructions for any given write. |
**/ |
@interface AsyncWritePacket : NSObject |
{ |
@public |
NSData *buffer; |
CFIndex bytesDone; |
long tag; |
NSTimeInterval timeout; |
} |
- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; |
@end |
|
@implementation AsyncWritePacket |
|
- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i |
{ |
if((self = [super init])) |
{ |
buffer = [d retain]; |
timeout = t; |
tag = i; |
bytesDone = 0; |
} |
return self; |
} |
|
- (void)dealloc |
{ |
[buffer release]; |
[super dealloc]; |
} |
|
@end |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark - |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
/** |
* The AsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. |
* This class my be altered to support more than just TLS in the future. |
**/ |
@interface AsyncSpecialPacket : NSObject |
{ |
@public |
NSDictionary *tlsSettings; |
} |
- (id)initWithTLSSettings:(NSDictionary *)settings; |
@end |
|
@implementation AsyncSpecialPacket |
|
- (id)initWithTLSSettings:(NSDictionary *)settings |
{ |
if((self = [super init])) |
{ |
tlsSettings = [settings copy]; |
} |
return self; |
} |
|
- (void)dealloc |
{ |
[tlsSettings release]; |
[super dealloc]; |
} |
|
@end |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark - |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
@implementation AsyncSocket |
|
- (id)init |
{ |
return [self initWithDelegate:nil userData:0]; |
} |
|
- (id)initWithDelegate:(id)delegate |
{ |
return [self initWithDelegate:delegate userData:0]; |
} |
|
// Designated initializer. |
- (id)initWithDelegate:(id)delegate userData:(long)userData |
{ |
if((self = [super init])) |
{ |
theFlags = DEFAULT_PREBUFFERING ? kEnablePreBuffering : 0; |
theDelegate = delegate; |
theUserData = userData; |
|
theSocket4 = NULL; |
theSource4 = NULL; |
theSocket6 = NULL; |
theSource6 = NULL; |
theRunLoop = NULL; |
theReadStream = NULL; |
theWriteStream = NULL; |
|
theConnectTimer = nil; |
|
theReadQueue = [[NSMutableArray alloc] initWithCapacity:READQUEUE_CAPACITY]; |
theCurrentRead = nil; |
theReadTimer = nil; |
|
partialReadBuffer = [[NSMutableData alloc] initWithCapacity:READALL_CHUNKSIZE]; |
|
theWriteQueue = [[NSMutableArray alloc] initWithCapacity:WRITEQUEUE_CAPACITY]; |
theCurrentWrite = nil; |
theWriteTimer = nil; |
|
// Socket context |
NSAssert(sizeof(CFSocketContext) == sizeof(CFStreamClientContext), @"CFSocketContext != CFStreamClientContext"); |
theContext.version = 0; |
theContext.info = self; |
theContext.retain = nil; |
theContext.release = nil; |
theContext.copyDescription = nil; |
|
// Default run loop modes |
theRunLoopModes = [[NSArray arrayWithObject:NSDefaultRunLoopMode] retain]; |
} |
return self; |
} |
|
// The socket may been initialized in a connected state and auto-released, so this should close it down cleanly. |
- (void)dealloc |
{ |
[self close]; |
[theReadQueue release]; |
[theWriteQueue release]; |
[theRunLoopModes release]; |
[partialReadBuffer release]; |
[NSObject cancelPreviousPerformRequestsWithTarget:self]; |
[super dealloc]; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Accessors |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (long)userData |
{ |
return theUserData; |
} |
|
- (void)setUserData:(long)userData |
{ |
theUserData = userData; |
} |
|
- (id)delegate |
{ |
return theDelegate; |
} |
|
- (void)setDelegate:(id)delegate |
{ |
theDelegate = delegate; |
} |
|
- (BOOL)canSafelySetDelegate |
{ |
return ([theReadQueue count] == 0 && [theWriteQueue count] == 0 && theCurrentRead == nil && theCurrentWrite == nil); |
} |
|
- (CFSocketRef)getCFSocket |
{ |
if(theSocket4) |
return theSocket4; |
else |
return theSocket6; |
} |
|
- (CFReadStreamRef)getCFReadStream |
{ |
return theReadStream; |
} |
|
- (CFWriteStreamRef)getCFWriteStream |
{ |
return theWriteStream; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Progress |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (float)progressOfReadReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total |
{ |
// Check to make sure we're actually reading something right now, |
// and that the read packet isn't an AsyncSpecialPacket (upgrade to TLS). |
if (!theCurrentRead || ![theCurrentRead isKindOfClass:[AsyncReadPacket class]]) return NAN; |
|
// It's only possible to know the progress of our read if we're reading to a certain length |
// If we're reading to data, we of course have no idea when the data will arrive |
// If we're reading to timeout, then we have no idea when the next chunk of data will arrive. |
BOOL hasTotal = (theCurrentRead->readAllAvailableData == NO && theCurrentRead->term == nil); |
|
CFIndex d = theCurrentRead->bytesDone; |
CFIndex t = hasTotal ? [theCurrentRead->buffer length] : 0; |
if (tag != NULL) *tag = theCurrentRead->tag; |
if (done != NULL) *done = d; |
if (total != NULL) *total = t; |
float ratio = (float)d/(float)t; |
return isnan(ratio) ? 1.0F : ratio; // 0 of 0 bytes is 100% done. |
} |
|
- (float)progressOfWriteReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total |
{ |
// Check to make sure we're actually writing something right now, |
// and that the write packet isn't an AsyncSpecialPacket (upgrade to TLS). |
if (!theCurrentWrite || ![theCurrentWrite isKindOfClass:[AsyncWritePacket class]]) return NAN; |
|
CFIndex d = theCurrentWrite->bytesDone; |
CFIndex t = [theCurrentWrite->buffer length]; |
if (tag != NULL) *tag = theCurrentWrite->tag; |
if (done != NULL) *done = d; |
if (total != NULL) *total = t; |
return (float)d/(float)t; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Run Loop |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (void)runLoopAddSource:(CFRunLoopSourceRef)source |
{ |
unsigned i, count = [theRunLoopModes count]; |
for(i = 0; i < count; i++) |
{ |
CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; |
CFRunLoopAddSource(theRunLoop, source, runLoopMode); |
} |
} |
|
- (void)runLoopRemoveSource:(CFRunLoopSourceRef)source |
{ |
unsigned i, count = [theRunLoopModes count]; |
for(i = 0; i < count; i++) |
{ |
CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; |
CFRunLoopRemoveSource(theRunLoop, source, runLoopMode); |
} |
} |
|
- (void)runLoopAddTimer:(NSTimer *)timer |
{ |
unsigned i, count = [theRunLoopModes count]; |
for(i = 0; i < count; i++) |
{ |
CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; |
CFRunLoopAddTimer(theRunLoop, (CFRunLoopTimerRef)timer, runLoopMode); |
} |
} |
|
- (void)runLoopRemoveTimer:(NSTimer *)timer |
{ |
unsigned i, count = [theRunLoopModes count]; |
for(i = 0; i < count; i++) |
{ |
CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; |
CFRunLoopRemoveTimer(theRunLoop, (CFRunLoopTimerRef)timer, runLoopMode); |
} |
} |
|
- (void)runLoopUnscheduleReadStream |
{ |
unsigned i, count = [theRunLoopModes count]; |
for(i = 0; i < count; i++) |
{ |
CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; |
CFReadStreamUnscheduleFromRunLoop(theReadStream, theRunLoop, runLoopMode); |
} |
CFReadStreamSetClient(theReadStream, kCFStreamEventNone, NULL, NULL); |
} |
|
- (void)runLoopUnscheduleWriteStream |
{ |
unsigned i, count = [theRunLoopModes count]; |
for(i = 0; i < count; i++) |
{ |
CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; |
CFWriteStreamUnscheduleFromRunLoop(theWriteStream, theRunLoop, runLoopMode); |
} |
CFWriteStreamSetClient(theWriteStream, kCFStreamEventNone, NULL, NULL); |
} |
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Configuration |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
/** |
* See the header file for a full explanation of pre-buffering. |
**/ |
- (void)enablePreBuffering |
{ |
theFlags |= kEnablePreBuffering; |
} |
|
/** |
* See the header file for a full explanation of this method. |
**/ |
- (BOOL)moveToRunLoop:(NSRunLoop *)runLoop |
{ |
NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), |
@"moveToRunLoop must be called from within the current RunLoop!"); |
|
if(runLoop == nil) |
{ |
return NO; |
} |
if(theRunLoop == [runLoop getCFRunLoop]) |
{ |
return YES; |
} |
|
[NSObject cancelPreviousPerformRequestsWithTarget:self]; |
theFlags &= ~kDequeueReadScheduled; |
theFlags &= ~kDequeueWriteScheduled; |
|
if(theReadStream && theWriteStream) |
{ |
[self runLoopUnscheduleReadStream]; |
[self runLoopUnscheduleWriteStream]; |
} |
|
if(theSource4) [self runLoopRemoveSource:theSource4]; |
if(theSource6) [self runLoopRemoveSource:theSource6]; |
|
// We do not retain the timers - they get retained by the runloop when we add them as a source. |
// Since we're about to remove them as a source, we retain now, and release again below. |
[theReadTimer retain]; |
[theWriteTimer retain]; |
|
if(theReadTimer) [self runLoopRemoveTimer:theReadTimer]; |
if(theWriteTimer) [self runLoopRemoveTimer:theWriteTimer]; |
|
theRunLoop = [runLoop getCFRunLoop]; |
|
if(theReadTimer) [self runLoopAddTimer:theReadTimer]; |
if(theWriteTimer) [self runLoopAddTimer:theWriteTimer]; |
|
// Release timers since we retained them above |
[theReadTimer release]; |
[theWriteTimer release]; |
|
if(theSource4) [self runLoopAddSource:theSource4]; |
if(theSource6) [self runLoopAddSource:theSource6]; |
|
if(theReadStream && theWriteStream) |
{ |
if(![self attachStreamsToRunLoop:runLoop error:nil]) |
{ |
return NO; |
} |
} |
|
[runLoop performSelector:@selector(maybeDequeueRead) target:self argument:nil order:0 modes:theRunLoopModes]; |
[runLoop performSelector:@selector(maybeDequeueWrite) target:self argument:nil order:0 modes:theRunLoopModes]; |
[runLoop performSelector:@selector(maybeScheduleDisconnect) target:self argument:nil order:0 modes:theRunLoopModes]; |
|
return YES; |
} |
|
/** |
* See the header file for a full explanation of this method. |
**/ |
- (BOOL)setRunLoopModes:(NSArray *)runLoopModes |
{ |
NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), |
@"setRunLoopModes must be called from within the current RunLoop!"); |
|
if([runLoopModes count] == 0) |
{ |
return NO; |
} |
if([theRunLoopModes isEqualToArray:runLoopModes]) |
{ |
return YES; |
} |
|
[NSObject cancelPreviousPerformRequestsWithTarget:self]; |
theFlags &= ~kDequeueReadScheduled; |
theFlags &= ~kDequeueWriteScheduled; |
|
if(theReadStream && theWriteStream) |
{ |
[self runLoopUnscheduleReadStream]; |
[self runLoopUnscheduleWriteStream]; |
} |
|
if(theSource4) [self runLoopRemoveSource:theSource4]; |
if(theSource6) [self runLoopRemoveSource:theSource6]; |
|
// We do not retain the timers - they get retained by the runloop when we add them as a source. |
// Since we're about to remove them as a source, we retain now, and release again below. |
[theReadTimer retain]; |
[theWriteTimer retain]; |
|
if(theReadTimer) [self runLoopRemoveTimer:theReadTimer]; |
if(theWriteTimer) [self runLoopRemoveTimer:theWriteTimer]; |
|
[theRunLoopModes release]; |
theRunLoopModes = [runLoopModes copy]; |
|
if(theReadTimer) [self runLoopAddTimer:theReadTimer]; |
if(theWriteTimer) [self runLoopAddTimer:theWriteTimer]; |
|
// Release timers since we retained them above |
[theReadTimer release]; |
[theWriteTimer release]; |
|
if(theSource4) [self runLoopAddSource:theSource4]; |
if(theSource6) [self runLoopAddSource:theSource6]; |
|
if(theReadStream && theWriteStream) |
{ |
// Note: theRunLoop variable is a CFRunLoop, and NSRunLoop is NOT toll-free bridged with CFRunLoop. |
// So we cannot pass theRunLoop to the method below, which is expecting a NSRunLoop parameter. |
// Instead we pass nil, which will result in the method properly using the current run loop. |
|
if(![self attachStreamsToRunLoop:nil error:nil]) |
{ |
return NO; |
} |
} |
|
[self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; |
[self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; |
[self performSelector:@selector(maybeScheduleDisconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; |
|
return YES; |
} |
|
- (NSArray *)runLoopModes |
{ |
return [[theRunLoopModes retain] autorelease]; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Accepting |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr |
{ |
return [self acceptOnInterface:nil port:port error:errPtr]; |
} |
|
/** |
* To accept on a certain interface, pass the address to accept on. |
* To accept on any interface, pass nil or an empty string. |
* To accept only connections from localhost pass "localhost" or "loopback". |
**/ |
- (BOOL)acceptOnInterface:(NSString *)interface port:(UInt16)port error:(NSError **)errPtr |
{ |
if (theDelegate == NULL) |
{ |
[NSException raise:AsyncSocketException |
format:@"Attempting to accept without a delegate. Set a delegate first."]; |
} |
|
if (theSocket4 != NULL || theSocket6 != NULL) |
{ |
[NSException raise:AsyncSocketException |
format:@"Attempting to accept while connected or accepting connections. Disconnect first."]; |
} |
|
// Set up the listen sockaddr structs if needed. |
|
NSData *address4 = nil, *address6 = nil; |
if(interface == nil || ([interface length] == 0)) |
{ |
// Accept on ANY address |
struct sockaddr_in nativeAddr4; |
nativeAddr4.sin_len = sizeof(struct sockaddr_in); |
nativeAddr4.sin_family = AF_INET; |
nativeAddr4.sin_port = htons(port); |
nativeAddr4.sin_addr.s_addr = htonl(INADDR_ANY); |
memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); |
|
struct sockaddr_in6 nativeAddr6; |
nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); |
nativeAddr6.sin6_family = AF_INET6; |
nativeAddr6.sin6_port = htons(port); |
nativeAddr6.sin6_flowinfo = 0; |
nativeAddr6.sin6_addr = in6addr_any; |
nativeAddr6.sin6_scope_id = 0; |
|
// Wrap the native address structures for CFSocketSetAddress. |
address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; |
address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; |
} |
else if([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) |
{ |
// Accept only on LOOPBACK address |
struct sockaddr_in nativeAddr4; |
nativeAddr4.sin_len = sizeof(struct sockaddr_in); |
nativeAddr4.sin_family = AF_INET; |
nativeAddr4.sin_port = htons(port); |
nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); |
|
struct sockaddr_in6 nativeAddr6; |
nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); |
nativeAddr6.sin6_family = AF_INET6; |
nativeAddr6.sin6_port = htons(port); |
nativeAddr6.sin6_flowinfo = 0; |
nativeAddr6.sin6_addr = in6addr_loopback; |
nativeAddr6.sin6_scope_id = 0; |
|
// Wrap the native address structures for CFSocketSetAddress. |
address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; |
address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; |
} |
else |
{ |
NSString *portStr = [NSString stringWithFormat:@"%hu", port]; |
|
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 |
@synchronized (getaddrinfoLock) |
#endif |
{ |
struct addrinfo hints, *res, *res0; |
|
memset(&hints, 0, sizeof(hints)); |
hints.ai_family = PF_UNSPEC; |
hints.ai_socktype = SOCK_STREAM; |
hints.ai_protocol = IPPROTO_TCP; |
hints.ai_flags = AI_PASSIVE; |
|
int error = getaddrinfo([interface UTF8String], [portStr UTF8String], &hints, &res0); |
|
if(error) |
{ |
if(errPtr) |
{ |
NSString *errMsg = [NSString stringWithCString:gai_strerror(error) encoding:NSASCIIStringEncoding]; |
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; |
|
*errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:error userInfo:info]; |
} |
} |
|
for(res = res0; res; res = res->ai_next) |
{ |
if(!address4 && (res->ai_family == AF_INET)) |
{ |
// Found IPv4 address |
// Wrap the native address structures for CFSocketSetAddress. |
address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; |
} |
else if(!address6 && (res->ai_family == AF_INET6)) |
{ |
// Found IPv6 address |
// Wrap the native address structures for CFSocketSetAddress. |
address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; |
} |
} |
freeaddrinfo(res0); |
} |
|
if(!address4 && !address6) return NO; |
} |
|
// Create the sockets. |
|
if (address4) |
{ |
theSocket4 = [self newAcceptSocketForAddress:address4 error:errPtr]; |
if (theSocket4 == NULL) goto Failed; |
} |
|
if (address6) |
{ |
theSocket6 = [self newAcceptSocketForAddress:address6 error:errPtr]; |
|
// Note: The iPhone doesn't currently support IPv6 |
|
#if !TARGET_OS_IPHONE |
if (theSocket6 == NULL) goto Failed; |
#endif |
} |
|
// Attach the sockets to the run loop so that callback methods work |
|
[self attachSocketsToRunLoop:nil error:nil]; |
|
// Set the SO_REUSEADDR flags. |
|
int reuseOn = 1; |
if (theSocket4) setsockopt(CFSocketGetNative(theSocket4), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); |
if (theSocket6) setsockopt(CFSocketGetNative(theSocket6), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); |
|
// Set the local bindings which causes the sockets to start listening. |
|
CFSocketError err; |
if (theSocket4) |
{ |
err = CFSocketSetAddress (theSocket4, (CFDataRef)address4); |
if (err != kCFSocketSuccess) goto Failed; |
|
//NSLog(@"theSocket4: %hu", [self localPort:theSocket4]); |
} |
|
if(port == 0 && theSocket4 && theSocket6) |
{ |
// The user has passed in port 0, which means he wants to allow the kernel to choose the port for them |
// However, the kernel will choose a different port for both theSocket4 and theSocket6 |
// So we grab the port the kernel choose for theSocket4, and set it as the port for theSocket6 |
UInt16 chosenPort = [self localPort:theSocket4]; |
|
struct sockaddr_in6 *pSockAddr6 = (struct sockaddr_in6 *)[address6 bytes]; |
pSockAddr6->sin6_port = htons(chosenPort); |
} |
|
if (theSocket6) |
{ |
err = CFSocketSetAddress (theSocket6, (CFDataRef)address6); |
if (err != kCFSocketSuccess) goto Failed; |
|
//NSLog(@"theSocket6: %hu", [self localPort:theSocket6]); |
} |
|
theFlags |= kDidPassConnectMethod; |
return YES; |
|
Failed: |
if(errPtr) *errPtr = [self getSocketError]; |
if(theSocket4 != NULL) |
{ |
CFSocketInvalidate(theSocket4); |
CFRelease(theSocket4); |
theSocket4 = NULL; |
} |
if(theSocket6 != NULL) |
{ |
CFSocketInvalidate(theSocket6); |
CFRelease(theSocket6); |
theSocket6 = NULL; |
} |
return NO; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Connecting |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (BOOL)connectToHost:(NSString*)hostname onPort:(UInt16)port error:(NSError **)errPtr |
{ |
return [self connectToHost:hostname onPort:port withTimeout:-1 error:errPtr]; |
} |
|
/** |
* This method creates an initial CFReadStream and CFWriteStream to the given host on the given port. |
* The connection is then opened, and the corresponding CFSocket will be extracted after the connection succeeds. |
* |
* Thus the delegate will have access to the CFReadStream and CFWriteStream prior to connection, |
* specifically in the onSocketWillConnect: method. |
**/ |
- (BOOL)connectToHost:(NSString *)hostname |
onPort:(UInt16)port |
withTimeout:(NSTimeInterval)timeout |
error:(NSError **)errPtr |
{ |
if(theDelegate == NULL) |
{ |
[NSException raise:AsyncSocketException |
format:@"Attempting to connect without a delegate. Set a delegate first."]; |
} |
|
if(theSocket4 != NULL || theSocket6 != NULL) |
{ |
[NSException raise:AsyncSocketException |
format:@"Attempting to connect while connected or accepting connections. Disconnect first."]; |
} |
|
if(![self createStreamsToHost:hostname onPort:port error:errPtr]) goto Failed; |
if(![self attachStreamsToRunLoop:nil error:errPtr]) goto Failed; |
if(![self configureStreamsAndReturnError:errPtr]) goto Failed; |
if(![self openStreamsAndReturnError:errPtr]) goto Failed; |
|
[self startConnectTimeout:timeout]; |
theFlags |= kDidPassConnectMethod; |
|
return YES; |
|
Failed: |
[self close]; |
return NO; |
} |
|
- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr |
{ |
return [self connectToAddress:remoteAddr withTimeout:-1 error:errPtr]; |
} |
|
/** |
* This method creates an initial CFSocket to the given address. |
* The connection is then opened, and the corresponding CFReadStream and CFWriteStream will be |
* created from the low-level sockets after the connection succeeds. |
* |
* Thus the delegate will have access to the CFSocket and CFSocketNativeHandle (BSD socket) prior to connection, |
* specifically in the onSocketWillConnect: method. |
* |
* Note: The NSData parameter is expected to be a sockaddr structure. For example, an NSData object returned from |
* NSNetservice addresses method. |
* If you have an existing struct sockaddr you can convert it to an NSData object like so: |
* struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; |
* struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; |
**/ |
- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr |
{ |
if (theDelegate == NULL) |
{ |
[NSException raise:AsyncSocketException |
format:@"Attempting to connect without a delegate. Set a delegate first."]; |
} |
|
if (theSocket4 != NULL || theSocket6 != NULL) |
{ |
[NSException raise:AsyncSocketException |
format:@"Attempting to connect while connected or accepting connections. Disconnect first."]; |
} |
|
if(![self createSocketForAddress:remoteAddr error:errPtr]) goto Failed; |
if(![self attachSocketsToRunLoop:nil error:errPtr]) goto Failed; |
if(![self configureSocketAndReturnError:errPtr]) goto Failed; |
if(![self connectSocketToAddress:remoteAddr error:errPtr]) goto Failed; |
|
[self startConnectTimeout:timeout]; |
theFlags |= kDidPassConnectMethod; |
|
return YES; |
|
Failed: |
[self close]; |
return NO; |
} |
|
- (void)startConnectTimeout:(NSTimeInterval)timeout |
{ |
if(timeout >= 0.0) |
{ |
theConnectTimer = [NSTimer timerWithTimeInterval:timeout |
target:self |
selector:@selector(doConnectTimeout:) |
userInfo:nil |
repeats:NO]; |
[self runLoopAddTimer:theConnectTimer]; |
} |
} |
|
- (void)endConnectTimeout |
{ |
[theConnectTimer invalidate]; |
theConnectTimer = nil; |
} |
|
- (void)doConnectTimeout:(NSTimer *)timer |
{ |
[self endConnectTimeout]; |
[self closeWithError:[self getConnectTimeoutError]]; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Socket Implementation |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
/** |
* Creates the accept sockets. |
* Returns true if either IPv4 or IPv6 is created. |
* If either is missing, an error is returned (even though the method may return true). |
**/ |
- (CFSocketRef)newAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr |
{ |
struct sockaddr *pSockAddr = (struct sockaddr *)[addr bytes]; |
int addressFamily = pSockAddr->sa_family; |
|
CFSocketRef theSocket = CFSocketCreate(kCFAllocatorDefault, |
addressFamily, |
SOCK_STREAM, |
0, |
kCFSocketAcceptCallBack, // Callback flags |
(CFSocketCallBack)&MyCFSocketCallback, // Callback method |
&theContext); |
|
if(theSocket == NULL) |
{ |
if(errPtr) *errPtr = [self getSocketError]; |
} |
|
return theSocket; |
} |
|
- (BOOL)createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr |
{ |
struct sockaddr *pSockAddr = (struct sockaddr *)[remoteAddr bytes]; |
|
if(pSockAddr->sa_family == AF_INET) |
{ |
theSocket4 = CFSocketCreate(NULL, // Default allocator |
PF_INET, // Protocol Family |
SOCK_STREAM, // Socket Type |
IPPROTO_TCP, // Protocol |
kCFSocketConnectCallBack, // Callback flags |
(CFSocketCallBack)&MyCFSocketCallback, // Callback method |
&theContext); // Socket Context |
|
if(theSocket4 == NULL) |
{ |
if (errPtr) *errPtr = [self getSocketError]; |
return NO; |
} |
} |
else if(pSockAddr->sa_family == AF_INET6) |
{ |
theSocket6 = CFSocketCreate(NULL, // Default allocator |
PF_INET6, // Protocol Family |
SOCK_STREAM, // Socket Type |
IPPROTO_TCP, // Protocol |
kCFSocketConnectCallBack, // Callback flags |
(CFSocketCallBack)&MyCFSocketCallback, // Callback method |
&theContext); // Socket Context |
|
if(theSocket6 == NULL) |
{ |
if (errPtr) *errPtr = [self getSocketError]; |
return NO; |
} |
} |
else |
{ |
if (errPtr) *errPtr = [self getSocketError]; |
return NO; |
} |
|
return YES; |
} |
|
/** |
* Adds the CFSocket's to the run-loop so that callbacks will work properly. |
**/ |
- (BOOL)attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr |
{ |
// Get the CFRunLoop to which the socket should be attached. |
theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; |
|
if(theSocket4) |
{ |
theSource4 = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket4, 0); |
[self runLoopAddSource:theSource4]; |
} |
|
if(theSocket6) |
{ |
theSource6 = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket6, 0); |
[self runLoopAddSource:theSource6]; |
} |
|
return YES; |
} |
|
/** |
* Allows the delegate method to configure the CFSocket or CFNativeSocket as desired before we connect. |
* Note that the CFReadStream and CFWriteStream will not be available until after the connection is opened. |
**/ |
- (BOOL)configureSocketAndReturnError:(NSError **)errPtr |
{ |
// Call the delegate method for further configuration. |
if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) |
{ |
if([theDelegate onSocketWillConnect:self] == NO) |
{ |
if (errPtr) *errPtr = [self getAbortError]; |
return NO; |
} |
} |
return YES; |
} |
|
- (BOOL)connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr |
{ |
// Start connecting to the given address in the background |
// The MyCFSocketCallback method will be called when the connection succeeds or fails |
if(theSocket4) |
{ |
CFSocketError err = CFSocketConnectToAddress(theSocket4, (CFDataRef)remoteAddr, -1); |
if(err != kCFSocketSuccess) |
{ |
if (errPtr) *errPtr = [self getSocketError]; |
return NO; |
} |
} |
else if(theSocket6) |
{ |
CFSocketError err = CFSocketConnectToAddress(theSocket6, (CFDataRef)remoteAddr, -1); |
if(err != kCFSocketSuccess) |
{ |
if (errPtr) *errPtr = [self getSocketError]; |
return NO; |
} |
} |
|
return YES; |
} |
|
/** |
* Attempt to make the new socket. |
* If an error occurs, ignore this event. |
**/ |
- (void)doAcceptWithSocket:(CFSocketNativeHandle)newNative |
{ |
// New socket inherits same delegate and run loop modes. |
// Note: We use [self class] to support subclassing AsyncSocket. |
AsyncSocket *newSocket = [[[[self class] alloc] initWithDelegate:theDelegate] autorelease]; |
[newSocket setRunLoopModes:theRunLoopModes]; |
|
if(newSocket) |
{ |
if ([theDelegate respondsToSelector:@selector(onSocket:didAcceptNewSocket:)]) |
[theDelegate onSocket:self didAcceptNewSocket:newSocket]; |
|
NSRunLoop *runLoop = nil; |
if ([theDelegate respondsToSelector:@selector(onSocket:wantsRunLoopForNewSocket:)]) |
runLoop = [theDelegate onSocket:self wantsRunLoopForNewSocket:newSocket]; |
|
BOOL pass = YES; |
|
if(pass && ![newSocket createStreamsFromNative:newNative error:nil]) pass = NO; |
if(pass && ![newSocket attachStreamsToRunLoop:runLoop error:nil]) pass = NO; |
if(pass && ![newSocket configureStreamsAndReturnError:nil]) pass = NO; |
if(pass && ![newSocket openStreamsAndReturnError:nil]) pass = NO; |
|
if(pass) |
newSocket->theFlags |= kDidPassConnectMethod; |
else { |
// No NSError, but errors will still get logged from the above functions. |
[newSocket close]; |
} |
|
} |
} |
|
/** |
* Description forthcoming... |
**/ |
- (void)doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)socketError |
{ |
NSParameterAssert ((sock == theSocket4) || (sock == theSocket6)); |
|
if(socketError == kCFSocketTimeout || socketError == kCFSocketError) |
{ |
[self closeWithError:[self getSocketError]]; |
return; |
} |
|
// Get the underlying native (BSD) socket |
CFSocketNativeHandle nativeSocket = CFSocketGetNative(sock); |
|
// Setup the socket so that invalidating the socket will not close the native socket |
CFSocketSetSocketFlags(sock, 0); |
|
// Invalidate and release the CFSocket - All we need from here on out is the nativeSocket |
// Note: If we don't invalidate the socket (leaving the native socket open) |
// then theReadStream and theWriteStream won't function properly. |
// Specifically, their callbacks won't work, with the exception of kCFStreamEventOpenCompleted. |
// I'm not entirely sure why this is, but I'm guessing that events on the socket fire to the CFSocket we created, |
// as opposed to the CFReadStream/CFWriteStream. |
|
CFSocketInvalidate(sock); |
CFRelease(sock); |
theSocket4 = NULL; |
theSocket6 = NULL; |
|
NSError *err; |
BOOL pass = YES; |
|
if(pass && ![self createStreamsFromNative:nativeSocket error:&err]) pass = NO; |
if(pass && ![self attachStreamsToRunLoop:nil error:&err]) pass = NO; |
if(pass && ![self openStreamsAndReturnError:&err]) pass = NO; |
|
if(!pass) |
{ |
[self closeWithError:err]; |
} |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Stream Implementation |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
/** |
* Creates the CFReadStream and CFWriteStream from the given native socket. |
* The CFSocket may be extracted from either stream after the streams have been opened. |
* |
* Note: The given native socket must already be connected! |
**/ |
- (BOOL)createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr |
{ |
// Create the socket & streams. |
CFStreamCreatePairWithSocket(kCFAllocatorDefault, native, &theReadStream, &theWriteStream); |
if (theReadStream == NULL || theWriteStream == NULL) |
{ |
NSError *err = [self getStreamError]; |
|
NSLog (@"AsyncSocket %p couldn't create streams from accepted socket: %@", self, err); |
|
if (errPtr) *errPtr = err; |
return NO; |
} |
|
// Ensure the CF & BSD socket is closed when the streams are closed. |
CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); |
CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); |
|
return YES; |
} |
|
/** |
* Creates the CFReadStream and CFWriteStream from the given hostname and port number. |
* The CFSocket may be extracted from either stream after the streams have been opened. |
**/ |
- (BOOL)createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr |
{ |
// Create the socket & streams. |
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (CFStringRef)hostname, port, &theReadStream, &theWriteStream); |
if (theReadStream == NULL || theWriteStream == NULL) |
{ |
if (errPtr) *errPtr = [self getStreamError]; |
return NO; |
} |
|
// Ensure the CF & BSD socket is closed when the streams are closed. |
CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); |
CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); |
|
return YES; |
} |
|
- (BOOL)attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr |
{ |
// Get the CFRunLoop to which the socket should be attached. |
theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; |
|
// Setup read stream callbacks |
|
CFOptionFlags readStreamEvents = kCFStreamEventHasBytesAvailable | |
kCFStreamEventErrorOccurred | |
kCFStreamEventEndEncountered | |
kCFStreamEventOpenCompleted; |
|
if (!CFReadStreamSetClient(theReadStream, |
readStreamEvents, |
(CFReadStreamClientCallBack)&MyCFReadStreamCallback, |
(CFStreamClientContext *)(&theContext))) |
{ |
NSError *err = [self getStreamError]; |
|
NSLog (@"AsyncSocket %p couldn't attach read stream to run-loop,", self); |
NSLog (@"Error: %@", err); |
|
if (errPtr) *errPtr = err; |
return NO; |
} |
|
// Setup write stream callbacks |
|
CFOptionFlags writeStreamEvents = kCFStreamEventCanAcceptBytes | |
kCFStreamEventErrorOccurred | |
kCFStreamEventEndEncountered | |
kCFStreamEventOpenCompleted; |
|
if (!CFWriteStreamSetClient (theWriteStream, |
writeStreamEvents, |
(CFWriteStreamClientCallBack)&MyCFWriteStreamCallback, |
(CFStreamClientContext *)(&theContext))) |
{ |
NSError *err = [self getStreamError]; |
|
NSLog (@"AsyncSocket %p couldn't attach write stream to run-loop,", self); |
NSLog (@"Error: %@", err); |
|
if (errPtr) *errPtr = err; |
return NO; |
} |
|
// Add read and write streams to run loop |
|
unsigned i, count = [theRunLoopModes count]; |
for(i = 0; i < count; i++) |
{ |
CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; |
CFReadStreamScheduleWithRunLoop(theReadStream, theRunLoop, runLoopMode); |
CFWriteStreamScheduleWithRunLoop(theWriteStream, theRunLoop, runLoopMode); |
} |
|
return YES; |
} |
|
/** |
* Allows the delegate method to configure the CFReadStream and/or CFWriteStream as desired before we connect. |
* Note that the CFSocket and CFNativeSocket will not be available until after the connection is opened. |
**/ |
- (BOOL)configureStreamsAndReturnError:(NSError **)errPtr |
{ |
// Call the delegate method for further configuration. |
if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) |
{ |
if([theDelegate onSocketWillConnect:self] == NO) |
{ |
if (errPtr) *errPtr = [self getAbortError]; |
return NO; |
} |
} |
return YES; |
} |
|
- (BOOL)openStreamsAndReturnError:(NSError **)errPtr |
{ |
BOOL pass = YES; |
|
if(pass && !CFReadStreamOpen (theReadStream)) |
{ |
NSLog (@"AsyncSocket %p couldn't open read stream,", self); |
pass = NO; |
} |
|
if(pass && !CFWriteStreamOpen (theWriteStream)) |
{ |
NSLog (@"AsyncSocket %p couldn't open write stream,", self); |
pass = NO; |
} |
|
if(!pass) |
{ |
if (errPtr) *errPtr = [self getStreamError]; |
} |
|
return pass; |
} |
|
/** |
* Called when read or write streams open. |
* When the socket is connected and both streams are open, consider the AsyncSocket instance to be ready. |
**/ |
- (void)doStreamOpen |
{ |
NSError *err = nil; |
if ((theFlags & kDidCompleteOpenForRead) && (theFlags & kDidCompleteOpenForWrite)) |
{ |
// Get the socket. |
if (![self setSocketFromStreamsAndReturnError: &err]) |
{ |
NSLog (@"AsyncSocket %p couldn't get socket from streams, %@. Disconnecting.", self, err); |
[self closeWithError:err]; |
return; |
} |
|
// Stop the connection attempt timeout timer |
[self endConnectTimeout]; |
|
if ([theDelegate respondsToSelector:@selector(onSocket:didConnectToHost:port:)]) |
{ |
[theDelegate onSocket:self didConnectToHost:[self connectedHost] port:[self connectedPort]]; |
} |
|
// Immediately deal with any already-queued requests. |
[self maybeDequeueRead]; |
[self maybeDequeueWrite]; |
} |
} |
|
- (BOOL)setSocketFromStreamsAndReturnError:(NSError **)errPtr |
{ |
// Get the CFSocketNativeHandle from theReadStream |
CFSocketNativeHandle native; |
CFDataRef nativeProp = CFReadStreamCopyProperty(theReadStream, kCFStreamPropertySocketNativeHandle); |
if(nativeProp == NULL) |
{ |
if (errPtr) *errPtr = [self getStreamError]; |
return NO; |
} |
|
CFDataGetBytes(nativeProp, CFRangeMake(0, CFDataGetLength(nativeProp)), (UInt8 *)&native); |
CFRelease(nativeProp); |
|
CFSocketRef theSocket = CFSocketCreateWithNative(kCFAllocatorDefault, native, 0, NULL, NULL); |
if(theSocket == NULL) |
{ |
if (errPtr) *errPtr = [self getSocketError]; |
return NO; |
} |
|
// Determine whether the connection was IPv4 or IPv6 |
CFDataRef peeraddr = CFSocketCopyPeerAddress(theSocket); |
if(peeraddr == NULL) |
{ |
NSLog(@"AsyncSocket couldn't determine IP version of socket"); |
|
CFRelease(theSocket); |
|
if (errPtr) *errPtr = [self getSocketError]; |
return NO; |
} |
struct sockaddr *sa = (struct sockaddr *)CFDataGetBytePtr(peeraddr); |
|
if(sa->sa_family == AF_INET) |
{ |
theSocket4 = theSocket; |
} |
else |
{ |
theSocket6 = theSocket; |
} |
|
CFRelease(peeraddr); |
|
return YES; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Disconnect Implementation |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
// Sends error message and disconnects |
- (void)closeWithError:(NSError *)err |
{ |
theFlags |= kClosingWithError; |
|
if (theFlags & kDidPassConnectMethod) |
{ |
// Try to salvage what data we can. |
[self recoverUnreadData]; |
|
// Let the delegate know, so it can try to recover if it likes. |
if ([theDelegate respondsToSelector:@selector(onSocket:willDisconnectWithError:)]) |
{ |
[theDelegate onSocket:self willDisconnectWithError:err]; |
} |
} |
[self close]; |
} |
|
// Prepare partially read data for recovery. |
- (void)recoverUnreadData |
{ |
if(theCurrentRead != nil) |
{ |
// We never finished the current read. |
// Check to see if it's a normal read packet (not AsyncSpecialPacket) and if it had read anything yet. |
|
if(([theCurrentRead isKindOfClass:[AsyncReadPacket class]]) && (theCurrentRead->bytesDone > 0)) |
{ |
// We need to move its data into the front of the partial read buffer. |
|
[partialReadBuffer replaceBytesInRange:NSMakeRange(0, 0) |
withBytes:[theCurrentRead->buffer bytes] |
length:theCurrentRead->bytesDone]; |
} |
} |
|
[self emptyQueues]; |
} |
|
- (void)emptyQueues |
{ |
if (theCurrentRead != nil) [self endCurrentRead]; |
if (theCurrentWrite != nil) [self endCurrentWrite]; |
|
[theReadQueue removeAllObjects]; |
[theWriteQueue removeAllObjects]; |
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueRead) object:nil]; |
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueWrite) object:nil]; |
|
theFlags &= ~kDequeueReadScheduled; |
theFlags &= ~kDequeueWriteScheduled; |
} |
|
/** |
* Disconnects. This is called for both error and clean disconnections. |
**/ |
- (void)close |
{ |
// Empty queues |
[self emptyQueues]; |
|
// Clear partialReadBuffer (pre-buffer and also unreadData buffer in case of error) |
[partialReadBuffer replaceBytesInRange:NSMakeRange(0, [partialReadBuffer length]) withBytes:NULL length:0]; |
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(disconnect) object:nil]; |
|
// Stop the connection attempt timeout timer |
if (theConnectTimer != nil) |
{ |
[self endConnectTimeout]; |
} |
|
// Close streams. |
if (theReadStream != NULL) |
{ |
[self runLoopUnscheduleReadStream]; |
CFReadStreamClose(theReadStream); |
CFRelease(theReadStream); |
theReadStream = NULL; |
} |
if (theWriteStream != NULL) |
{ |
[self runLoopUnscheduleWriteStream]; |
CFWriteStreamClose(theWriteStream); |
CFRelease(theWriteStream); |
theWriteStream = NULL; |
} |
|
// Close sockets. |
if (theSocket4 != NULL) |
{ |
CFSocketInvalidate (theSocket4); |
CFRelease (theSocket4); |
theSocket4 = NULL; |
} |
if (theSocket6 != NULL) |
{ |
CFSocketInvalidate (theSocket6); |
CFRelease (theSocket6); |
theSocket6 = NULL; |
} |
if (theSource4 != NULL) |
{ |
[self runLoopRemoveSource:theSource4]; |
CFRelease (theSource4); |
theSource4 = NULL; |
} |
if (theSource6 != NULL) |
{ |
[self runLoopRemoveSource:theSource6]; |
CFRelease (theSource6); |
theSource6 = NULL; |
} |
theRunLoop = NULL; |
|
// If the client has passed the connect/accept method, then the connection has at least begun. |
// Notify delegate that it is now ending. |
BOOL shouldCallDelegate = (theFlags & kDidPassConnectMethod); |
|
// Clear all flags (except the pre-buffering flag, which should remain as is) |
theFlags &= kEnablePreBuffering; |
|
if (shouldCallDelegate) |
{ |
if ([theDelegate respondsToSelector: @selector(onSocketDidDisconnect:)]) |
{ |
[theDelegate onSocketDidDisconnect:self]; |
} |
} |
|
// Do not access any instance variables after calling onSocketDidDisconnect. |
// This gives the delegate freedom to release us without returning here and crashing. |
} |
|
/** |
* Disconnects immediately. Any pending reads or writes are dropped. |
**/ |
- (void)disconnect |
{ |
[self close]; |
} |
|
/** |
* Diconnects after all pending reads have completed. |
**/ |
- (void)disconnectAfterReading |
{ |
theFlags |= (kForbidReadsWrites | kDisconnectAfterReads); |
|
[self maybeScheduleDisconnect]; |
} |
|
/** |
* Disconnects after all pending writes have completed. |
**/ |
- (void)disconnectAfterWriting |
{ |
theFlags |= (kForbidReadsWrites | kDisconnectAfterWrites); |
|
[self maybeScheduleDisconnect]; |
} |
|
/** |
* Disconnects after all pending reads and writes have completed. |
**/ |
- (void)disconnectAfterReadingAndWriting |
{ |
theFlags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); |
|
[self maybeScheduleDisconnect]; |
} |
|
/** |
* Schedules a call to disconnect if possible. |
* That is, if all writes have completed, and we're set to disconnect after writing, |
* or if all reads have completed, and we're set to disconnect after reading. |
**/ |
- (void)maybeScheduleDisconnect |
{ |
BOOL shouldDisconnect = NO; |
|
if(theFlags & kDisconnectAfterReads) |
{ |
if(([theReadQueue count] == 0) && (theCurrentRead == nil)) |
{ |
if(theFlags & kDisconnectAfterWrites) |
{ |
if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) |
{ |
shouldDisconnect = YES; |
} |
} |
else |
{ |
shouldDisconnect = YES; |
} |
} |
} |
else if(theFlags & kDisconnectAfterWrites) |
{ |
if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) |
{ |
shouldDisconnect = YES; |
} |
} |
|
if(shouldDisconnect) |
{ |
[self performSelector:@selector(disconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; |
} |
} |
|
/** |
* In the event of an error, this method may be called during onSocket:willDisconnectWithError: to read |
* any data that's left on the socket. |
**/ |
- (NSData *)unreadData |
{ |
// Ensure this method will only return data in the event of an error |
if(!(theFlags & kClosingWithError)) return nil; |
|
if(theReadStream == NULL) return nil; |
|
CFIndex totalBytesRead = [partialReadBuffer length]; |
BOOL error = NO; |
while(!error && CFReadStreamHasBytesAvailable(theReadStream)) |
{ |
[partialReadBuffer increaseLengthBy:READALL_CHUNKSIZE]; |
|
// Number of bytes to read is space left in packet buffer. |
CFIndex bytesToRead = [partialReadBuffer length] - totalBytesRead; |
|
// Read data into packet buffer |
UInt8 *packetbuf = (UInt8 *)( [partialReadBuffer mutableBytes] + totalBytesRead ); |
CFIndex bytesRead = CFReadStreamRead(theReadStream, packetbuf, bytesToRead); |
|
// Check results |
if(bytesRead < 0) |
{ |
error = YES; |
} |
else |
{ |
totalBytesRead += bytesRead; |
} |
} |
|
[partialReadBuffer setLength:totalBytesRead]; |
|
return partialReadBuffer; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Errors |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
/** |
* Returns a standard error object for the current errno value. |
* Errno is used for low-level BSD socket errors. |
**/ |
- (NSError *)getErrnoError |
{ |
NSString *errorMsg = [NSString stringWithUTF8String:strerror(errno)]; |
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errorMsg forKey:NSLocalizedDescriptionKey]; |
|
return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; |
} |
|
/** |
* Returns a standard error message for a CFSocket error. |
* Unfortunately, CFSocket offers no feedback on its errors. |
**/ |
- (NSError *)getSocketError |
{ |
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCFSocketError", |
@"AsyncSocket", [NSBundle mainBundle], |
@"General CFSocket error", nil); |
|
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; |
|
return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCFSocketError userInfo:info]; |
} |
|
- (NSError *)getStreamError |
{ |
CFStreamError err; |
if (theReadStream != NULL) |
{ |
err = CFReadStreamGetError (theReadStream); |
if (err.error != 0) return [self errorFromCFStreamError: err]; |
} |
|
if (theWriteStream != NULL) |
{ |
err = CFWriteStreamGetError (theWriteStream); |
if (err.error != 0) return [self errorFromCFStreamError: err]; |
} |
|
return nil; |
} |
|
/** |
* Returns a standard AsyncSocket abort error. |
**/ |
- (NSError *)getAbortError |
{ |
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCanceledError", |
@"AsyncSocket", [NSBundle mainBundle], |
@"Connection canceled", nil); |
|
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; |
|
return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCanceledError userInfo:info]; |
} |
|
/** |
* Returns a standard AsyncSocket connect timeout error. |
**/ |
- (NSError *)getConnectTimeoutError |
{ |
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketConnectTimeoutError", |
@"AsyncSocket", [NSBundle mainBundle], |
@"Attempt to connect to host timed out", nil); |
|
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; |
|
return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketConnectTimeoutError userInfo:info]; |
} |
|
/** |
* Returns a standard AsyncSocket maxed out error. |
**/ |
- (NSError *)getReadMaxedOutError |
{ |
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadMaxedOutError", |
@"AsyncSocket", [NSBundle mainBundle], |
@"Read operation reached set maximum length", nil); |
|
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; |
|
return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadMaxedOutError userInfo:info]; |
} |
|
/** |
* Returns a standard AsyncSocket read timeout error. |
**/ |
- (NSError *)getReadTimeoutError |
{ |
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadTimeoutError", |
@"AsyncSocket", [NSBundle mainBundle], |
@"Read operation timed out", nil); |
|
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; |
|
return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadTimeoutError userInfo:info]; |
} |
|
/** |
* Returns a standard AsyncSocket write timeout error. |
**/ |
- (NSError *)getWriteTimeoutError |
{ |
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketWriteTimeoutError", |
@"AsyncSocket", [NSBundle mainBundle], |
@"Write operation timed out", nil); |
|
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; |
|
return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketWriteTimeoutError userInfo:info]; |
} |
|
- (NSError *)errorFromCFStreamError:(CFStreamError)err |
{ |
if (err.domain == 0 && err.error == 0) return nil; |
|
// Can't use switch; these constants aren't int literals. |
NSString *domain = @"CFStreamError (unlisted domain)"; |
NSString *message = nil; |
|
if(err.domain == kCFStreamErrorDomainPOSIX) { |
domain = NSPOSIXErrorDomain; |
} |
else if(err.domain == kCFStreamErrorDomainMacOSStatus) { |
domain = NSOSStatusErrorDomain; |
} |
else if(err.domain == kCFStreamErrorDomainMach) { |
domain = NSMachErrorDomain; |
} |
else if(err.domain == kCFStreamErrorDomainNetDB) |
{ |
domain = @"kCFStreamErrorDomainNetDB"; |
message = [NSString stringWithCString:gai_strerror(err.error) encoding:NSASCIIStringEncoding]; |
} |
else if(err.domain == kCFStreamErrorDomainNetServices) { |
domain = @"kCFStreamErrorDomainNetServices"; |
} |
else if(err.domain == kCFStreamErrorDomainSOCKS) { |
domain = @"kCFStreamErrorDomainSOCKS"; |
} |
else if(err.domain == kCFStreamErrorDomainSystemConfiguration) { |
domain = @"kCFStreamErrorDomainSystemConfiguration"; |
} |
else if(err.domain == kCFStreamErrorDomainSSL) { |
domain = @"kCFStreamErrorDomainSSL"; |
} |
|
NSDictionary *info = nil; |
if(message != nil) |
{ |
info = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey]; |
} |
return [NSError errorWithDomain:domain code:err.error userInfo:info]; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Diagnostics |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (BOOL)isConnected |
{ |
return [self isSocketConnected] && [self areStreamsConnected]; |
} |
|
- (NSString *)connectedHost |
{ |
if(theSocket4) |
return [self connectedHost:theSocket4]; |
else |
return [self connectedHost:theSocket6]; |
} |
|
- (UInt16)connectedPort |
{ |
if(theSocket4) |
return [self connectedPort:theSocket4]; |
else |
return [self connectedPort:theSocket6]; |
} |
|
- (NSString *)localHost |
{ |
if(theSocket4) |
return [self localHost:theSocket4]; |
else |
return [self localHost:theSocket6]; |
} |
|
- (UInt16)localPort |
{ |
if(theSocket4) |
return [self localPort:theSocket4]; |
else |
return [self localPort:theSocket6]; |
} |
|
- (NSString *)connectedHost:(CFSocketRef)theSocket |
{ |
if (theSocket == NULL) return nil; |
|
CFDataRef peeraddr; |
NSString *peerstr = nil; |
|
if((peeraddr = CFSocketCopyPeerAddress(theSocket))) |
{ |
peerstr = [self addressHost:peeraddr]; |
CFRelease (peeraddr); |
} |
|
return peerstr; |
} |
|
- (UInt16)connectedPort:(CFSocketRef)theSocket |
{ |
if (theSocket == NULL) return 0; |
|
CFDataRef peeraddr; |
UInt16 peerport = 0; |
|
if((peeraddr = CFSocketCopyPeerAddress(theSocket))) |
{ |
peerport = [self addressPort:peeraddr]; |
CFRelease (peeraddr); |
} |
|
return peerport; |
} |
|
- (NSString *)localHost:(CFSocketRef)theSocket |
{ |
if (theSocket == NULL) return nil; |
|
CFDataRef selfaddr; |
NSString *selfstr = nil; |
|
if((selfaddr = CFSocketCopyAddress(theSocket))) |
{ |
selfstr = [self addressHost:selfaddr]; |
CFRelease (selfaddr); |
} |
|
return selfstr; |
} |
|
- (UInt16)localPort:(CFSocketRef)theSocket |
{ |
if (theSocket == NULL) return 0; |
|
CFDataRef selfaddr; |
UInt16 selfport = 0; |
|
if ((selfaddr = CFSocketCopyAddress(theSocket))) |
{ |
selfport = [self addressPort:selfaddr]; |
CFRelease (selfaddr); |
} |
|
return selfport; |
} |
|
- (NSString *)addressHost:(CFDataRef)cfaddr |
{ |
if (cfaddr == NULL) return nil; |
|
char addrBuf[ MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) ]; |
struct sockaddr *pSockAddr = (struct sockaddr *) CFDataGetBytePtr (cfaddr); |
struct sockaddr_in *pSockAddrV4 = (struct sockaddr_in *)pSockAddr; |
struct sockaddr_in6 *pSockAddrV6 = (struct sockaddr_in6 *)pSockAddr; |
|
const void *pAddr = (pSockAddr->sa_family == AF_INET) ? |
(void *)(&(pSockAddrV4->sin_addr)) : |
(void *)(&(pSockAddrV6->sin6_addr)); |
|
const char *pStr = inet_ntop (pSockAddr->sa_family, pAddr, addrBuf, sizeof(addrBuf)); |
if (pStr == NULL) [NSException raise: NSInternalInconsistencyException |
format: @"Cannot convert address to string."]; |
|
return [NSString stringWithCString:pStr encoding:NSASCIIStringEncoding]; |
} |
|
- (UInt16)addressPort:(CFDataRef)cfaddr |
{ |
if (cfaddr == NULL) return 0; |
|
struct sockaddr_in *pAddr = (struct sockaddr_in *) CFDataGetBytePtr (cfaddr); |
return ntohs (pAddr->sin_port); |
} |
|
- (NSData *)connectedAddress |
{ |
CFSocketRef theSocket; |
|
if (theSocket4) |
theSocket = theSocket4; |
else |
theSocket = theSocket6; |
|
if (theSocket == NULL) return nil; |
|
CFDataRef peeraddr = CFSocketCopyPeerAddress(theSocket); |
|
if (peeraddr == NULL) return nil; |
|
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 |
NSData *result = [NSData dataWithBytes:CFDataGetBytePtr(peeraddr) length:CFDataGetLength(peeraddr)]; |
CFRelease(peeraddr); |
return result; |
#else |
return [(NSData *)NSMakeCollectable(peeraddr) autorelease]; |
#endif |
} |
|
- (NSData *)localAddress |
{ |
CFSocketRef theSocket; |
|
if (theSocket4) |
theSocket = theSocket4; |
else |
theSocket = theSocket6; |
|
if (theSocket == NULL) return nil; |
|
CFDataRef selfaddr = CFSocketCopyAddress(theSocket); |
|
if (selfaddr == NULL) return nil; |
|
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 |
NSData *result = [NSData dataWithBytes:CFDataGetBytePtr(selfaddr) length:CFDataGetLength(selfaddr)]; |
CFRelease(selfaddr); |
return result; |
#else |
return [(NSData *)NSMakeCollectable(selfaddr) autorelease]; |
#endif |
} |
|
- (BOOL)isIPv4 |
{ |
return (theSocket4 != NULL); |
} |
|
- (BOOL)isIPv6 |
{ |
return (theSocket6 != NULL); |
} |
|
- (BOOL)isSocketConnected |
{ |
if(theSocket4 != NULL) |
return CFSocketIsValid(theSocket4); |
else if(theSocket6 != NULL) |
return CFSocketIsValid(theSocket6); |
else |
return NO; |
} |
|
- (BOOL)areStreamsConnected |
{ |
CFStreamStatus s; |
|
if (theReadStream != NULL) |
{ |
s = CFReadStreamGetStatus (theReadStream); |
if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusReading || s == kCFStreamStatusError) ) |
return NO; |
} |
else return NO; |
|
if (theWriteStream != NULL) |
{ |
s = CFWriteStreamGetStatus (theWriteStream); |
if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusWriting || s == kCFStreamStatusError) ) |
return NO; |
} |
else return NO; |
|
return YES; |
} |
|
- (NSString *)description |
{ |
static const char *statstr[] = {"not open","opening","open","reading","writing","at end","closed","has error"}; |
CFStreamStatus rs = (theReadStream != NULL) ? CFReadStreamGetStatus(theReadStream) : 0; |
CFStreamStatus ws = (theWriteStream != NULL) ? CFWriteStreamGetStatus(theWriteStream) : 0; |
|
NSString *peerstr, *selfstr; |
CFDataRef peeraddr4 = NULL, peeraddr6 = NULL, selfaddr4 = NULL, selfaddr6 = NULL; |
|
if (theSocket4 || theSocket6) |
{ |
if (theSocket4) peeraddr4 = CFSocketCopyPeerAddress(theSocket4); |
if (theSocket6) peeraddr6 = CFSocketCopyPeerAddress(theSocket6); |
|
if(theSocket4 && theSocket6) |
{ |
peerstr = [NSString stringWithFormat: @"%@/%@ %u", |
[self addressHost:peeraddr4], [self addressHost:peeraddr6], [self addressPort:peeraddr4]]; |
} |
else if(theSocket4) |
{ |
peerstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:peeraddr4], [self addressPort:peeraddr4]]; |
} |
else |
{ |
peerstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:peeraddr6], [self addressPort:peeraddr6]]; |
} |
|
if(peeraddr4) CFRelease(peeraddr4); |
if(peeraddr6) CFRelease(peeraddr6); |
peeraddr4 = NULL; |
peeraddr6 = NULL; |
} |
else peerstr = @"nowhere"; |
|
if (theSocket4 || theSocket6) |
{ |
if (theSocket4) selfaddr4 = CFSocketCopyAddress (theSocket4); |
if (theSocket6) selfaddr6 = CFSocketCopyAddress (theSocket6); |
|
if (theSocket4 && theSocket6) |
{ |
selfstr = [NSString stringWithFormat: @"%@/%@ %u", |
[self addressHost:selfaddr4], [self addressHost:selfaddr6], [self addressPort:selfaddr4]]; |
} |
else if (theSocket4) |
{ |
selfstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:selfaddr4], [self addressPort:selfaddr4]]; |
} |
else |
{ |
selfstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:selfaddr6], [self addressPort:selfaddr6]]; |
} |
|
if(selfaddr4) CFRelease(selfaddr4); |
if(selfaddr6) CFRelease(selfaddr6); |
selfaddr4 = NULL; |
selfaddr6 = NULL; |
} |
else selfstr = @"nowhere"; |
|
NSMutableString *ms = [[NSMutableString alloc] initWithCapacity:150]; |
|
[ms appendString:[NSString stringWithFormat:@"<AsyncSocket %p", self]]; |
[ms appendString:[NSString stringWithFormat:@" local %@ remote %@ ", selfstr, peerstr]]; |
|
unsigned readQueueCount = (unsigned)[theReadQueue count]; |
unsigned writeQueueCount = (unsigned)[theWriteQueue count]; |
|
[ms appendString:[NSString stringWithFormat:@"has queued %u reads %u writes, ", readQueueCount, writeQueueCount]]; |
|
if (theCurrentRead == nil) |
[ms appendString: @"no current read, "]; |
else |
{ |
int percentDone; |
if ([theCurrentRead->buffer length] != 0) |
percentDone = (float)theCurrentRead->bytesDone / |
(float)[theCurrentRead->buffer length] * 100.0F; |
else |
percentDone = 100.0F; |
|
[ms appendString: [NSString stringWithFormat:@"currently read %u bytes (%d%% done), ", |
(unsigned int)[theCurrentRead->buffer length], |
theCurrentRead->bytesDone ? percentDone : 0]]; |
} |
|
if (theCurrentWrite == nil) |
[ms appendString: @"no current write, "]; |
else |
{ |
int percentDone; |
if ([theCurrentWrite->buffer length] != 0) |
percentDone = (float)theCurrentWrite->bytesDone / |
(float)[theCurrentWrite->buffer length] * 100.0F; |
else |
percentDone = 100.0F; |
|
[ms appendString: [NSString stringWithFormat:@"currently written %u (%d%%), ", |
(unsigned int)[theCurrentWrite->buffer length], |
theCurrentWrite->bytesDone ? percentDone : 0]]; |
} |
|
[ms appendString:[NSString stringWithFormat:@"read stream %p %s, ", theReadStream, statstr[rs]]]; |
[ms appendString:[NSString stringWithFormat:@"write stream %p %s", theWriteStream, statstr[ws]]]; |
|
if(theFlags & kDisconnectAfterReads) |
{ |
if(theFlags & kDisconnectAfterWrites) |
[ms appendString: @", will disconnect after reads & writes"]; |
else |
[ms appendString: @", will disconnect after reads"]; |
} |
else if(theFlags & kDisconnectAfterWrites) |
{ |
[ms appendString: @", will disconnect after writes"]; |
} |
|
if (![self isConnected]) [ms appendString: @", not connected"]; |
|
[ms appendString:@">"]; |
|
return [ms autorelease]; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Reading |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (void)readDataToLength:(CFIndex)length withTimeout:(NSTimeInterval)timeout tag:(long)tag |
{ |
if(length == 0) return; |
if(theFlags & kForbidReadsWrites) return; |
|
NSMutableData *buffer = [[NSMutableData alloc] initWithLength:length]; |
AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer |
timeout:timeout |
tag:tag |
readAllAvailable:NO |
terminator:nil |
maxLength:length]; |
|
[theReadQueue addObject:packet]; |
[self scheduleDequeueRead]; |
|
[packet release]; |
[buffer release]; |
} |
|
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag |
{ |
[self readDataToData:data withTimeout:timeout maxLength:-1 tag:tag]; |
} |
|
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(CFIndex)length tag:(long)tag |
{ |
if(data == nil || [data length] == 0) return; |
if(length >= 0 && length < [data length]) return; |
if(theFlags & kForbidReadsWrites) return; |
|
NSMutableData *buffer = [[NSMutableData alloc] initWithLength:0]; |
AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer |
timeout:timeout |
tag:tag |
readAllAvailable:NO |
terminator:data |
maxLength:length]; |
|
[theReadQueue addObject:packet]; |
[self scheduleDequeueRead]; |
|
[packet release]; |
[buffer release]; |
} |
|
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag |
{ |
if (theFlags & kForbidReadsWrites) return; |
|
NSMutableData *buffer = [[NSMutableData alloc] initWithLength:0]; |
AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer |
timeout:timeout |
tag:tag |
readAllAvailable:YES |
terminator:nil |
maxLength:-1]; |
|
[theReadQueue addObject:packet]; |
[self scheduleDequeueRead]; |
|
[packet release]; |
[buffer release]; |
} |
|
/** |
* Puts a maybeDequeueRead on the run loop. |
* An assumption here is that selectors will be performed consecutively within their priority. |
**/ |
- (void)scheduleDequeueRead |
{ |
if((theFlags & kDequeueReadScheduled) == 0) |
{ |
theFlags |= kDequeueReadScheduled; |
[self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; |
} |
} |
|
/** |
* This method starts a new read, if needed. |
* It is called when a user requests a read, |
* or when a stream opens that may have requested reads sitting in the queue, etc. |
**/ |
- (void)maybeDequeueRead |
{ |
// Unset the flag indicating a call to this method is scheduled |
theFlags &= ~kDequeueReadScheduled; |
|
// If we're not currently processing a read AND we have an available read stream |
if((theCurrentRead == nil) && (theReadStream != NULL)) |
{ |
if([theReadQueue count] > 0) |
{ |
// Dequeue the next object in the write queue |
theCurrentRead = [[theReadQueue objectAtIndex:0] retain]; |
[theReadQueue removeObjectAtIndex:0]; |
|
if([theCurrentRead isKindOfClass:[AsyncSpecialPacket class]]) |
{ |
// Attempt to start TLS |
theFlags |= kStartingReadTLS; |
|
// This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are both set |
[self maybeStartTLS]; |
} |
else |
{ |
// Start time-out timer |
if(theCurrentRead->timeout >= 0.0) |
{ |
theReadTimer = [NSTimer timerWithTimeInterval:theCurrentRead->timeout |
target:self |
selector:@selector(doReadTimeout:) |
userInfo:nil |
repeats:NO]; |
[self runLoopAddTimer:theReadTimer]; |
} |
|
// Immediately read, if possible |
[self doBytesAvailable]; |
} |
} |
else if(theFlags & kDisconnectAfterReads) |
{ |
if(theFlags & kDisconnectAfterWrites) |
{ |
if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) |
{ |
[self disconnect]; |
} |
} |
else |
{ |
[self disconnect]; |
} |
} |
} |
} |
|
/** |
* Call this method in doBytesAvailable instead of CFReadStreamHasBytesAvailable(). |
* This method supports pre-buffering properly as well as the kSocketHasBytesAvailable flag. |
**/ |
- (BOOL)hasBytesAvailable |
{ |
if ((theFlags & kSocketHasBytesAvailable) || ([partialReadBuffer length] > 0)) |
{ |
return YES; |
} |
else |
{ |
return CFReadStreamHasBytesAvailable(theReadStream); |
} |
} |
|
/** |
* Call this method in doBytesAvailable instead of CFReadStreamRead(). |
* This method support pre-buffering properly. |
**/ |
- (CFIndex)readIntoBuffer:(UInt8 *)buffer maxLength:(CFIndex)length |
{ |
if([partialReadBuffer length] > 0) |
{ |
// Determine the maximum amount of data to read |
CFIndex bytesToRead = MIN(length, [partialReadBuffer length]); |
|
// Copy the bytes from the buffer |
memcpy(buffer, [partialReadBuffer bytes], bytesToRead); |
|
// Remove the copied bytes from the buffer |
[partialReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToRead) withBytes:NULL length:0]; |
|
return bytesToRead; |
} |
else |
{ |
// Unset the "has-bytes-available" flag |
theFlags &= ~kSocketHasBytesAvailable; |
|
return CFReadStreamRead(theReadStream, buffer, length); |
} |
} |
|
/** |
* This method is called when a new read is taken from the read queue or when new data becomes available on the stream. |
**/ |
- (void)doBytesAvailable |
{ |
// If data is available on the stream, but there is no read request, then we don't need to process the data yet. |
// Also, if there is a read request, but no read stream setup yet, we can't process any data yet. |
if((theCurrentRead != nil) && (theReadStream != NULL)) |
{ |
// Note: This method is not called if theCurrentRead is an AsyncSpecialPacket (startTLS packet) |
|
CFIndex totalBytesRead = 0; |
|
BOOL done = NO; |
BOOL socketError = NO; |
BOOL maxoutError = NO; |
|
while(!done && !socketError && !maxoutError && [self hasBytesAvailable]) |
{ |
BOOL didPreBuffer = NO; |
|
// There are 3 types of read packets: |
// |
// 1) Read a specific length of data. |
// 2) Read all available data. |
// 3) Read up to a particular terminator. |
|
if(theCurrentRead->readAllAvailableData == YES) |
{ |
// We're reading all available data. |
// |
// Make sure there is at least READALL_CHUNKSIZE bytes available. |
// We don't want to increase the buffer any more than this or we'll waste space. |
// With prebuffering it's possible to read in a small chunk on the first read. |
|
unsigned buffInc = READALL_CHUNKSIZE - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); |
[theCurrentRead->buffer increaseLengthBy:buffInc]; |
} |
else if(theCurrentRead->term != nil) |
{ |
// We're reading up to a terminator. |
// |
// We may only want to read a few bytes. |
// Just enough to ensure we don't go past our term or over our max limit. |
// Unless pre-buffering is enabled, in which case we may want to read in a larger chunk. |
|
// If we already have data pre-buffered, we obviously don't want to pre-buffer it again. |
// So in this case we'll just read as usual. |
|
if(([partialReadBuffer length] > 0) || !(theFlags & kEnablePreBuffering)) |
{ |
unsigned maxToRead = [theCurrentRead readLengthForTerm]; |
|
unsigned bufInc = maxToRead - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); |
[theCurrentRead->buffer increaseLengthBy:bufInc]; |
} |
else |
{ |
didPreBuffer = YES; |
unsigned maxToRead = [theCurrentRead prebufferReadLengthForTerm]; |
|
unsigned buffInc = maxToRead - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); |
[theCurrentRead->buffer increaseLengthBy:buffInc]; |
|
} |
} |
|
// Number of bytes to read is space left in packet buffer. |
CFIndex bytesToRead = [theCurrentRead->buffer length] - theCurrentRead->bytesDone; |
|
// Read data into packet buffer |
UInt8 *subBuffer = (UInt8 *)([theCurrentRead->buffer mutableBytes] + theCurrentRead->bytesDone); |
CFIndex bytesRead = [self readIntoBuffer:subBuffer maxLength:bytesToRead]; |
|
// Check results |
if(bytesRead < 0) |
{ |
socketError = YES; |
} |
else |
{ |
// Update total amount read for the current read |
theCurrentRead->bytesDone += bytesRead; |
|
// Update total amount read in this method invocation |
totalBytesRead += bytesRead; |
} |
|
// Is packet done? |
if(theCurrentRead->readAllAvailableData != YES) |
{ |
if(theCurrentRead->term != nil) |
{ |
if(didPreBuffer) |
{ |
// Search for the terminating sequence within the big chunk we just read. |
CFIndex overflow = [theCurrentRead searchForTermAfterPreBuffering:bytesRead]; |
|
if(overflow > 0) |
{ |
// Copy excess data into partialReadBuffer |
NSMutableData *buffer = theCurrentRead->buffer; |
const void *overflowBuffer = [buffer bytes] + theCurrentRead->bytesDone - overflow; |
|
[partialReadBuffer appendBytes:overflowBuffer length:overflow]; |
|
// Update the bytesDone variable. |
// Note: The completeCurrentRead method will trim the buffer for us. |
theCurrentRead->bytesDone -= overflow; |
} |
|
done = (overflow >= 0); |
} |
else |
{ |
// Search for the terminating sequence at the end of the buffer |
int termlen = [theCurrentRead->term length]; |
if(theCurrentRead->bytesDone >= termlen) |
{ |
const void *buf = [theCurrentRead->buffer bytes] + (theCurrentRead->bytesDone - termlen); |
const void *seq = [theCurrentRead->term bytes]; |
done = (memcmp (buf, seq, termlen) == 0); |
} |
} |
|
if(!done && theCurrentRead->maxLength >= 0 && theCurrentRead->bytesDone >= theCurrentRead->maxLength) |
{ |
// There's a set maxLength, and we've reached that maxLength without completing the read |
maxoutError = YES; |
} |
} |
else |
{ |
// Done when (sized) buffer is full. |
done = ([theCurrentRead->buffer length] == theCurrentRead->bytesDone); |
} |
} |
// else readAllAvailable doesn't end until all readable is read. |
} |
|
if(theCurrentRead->readAllAvailableData && theCurrentRead->bytesDone > 0) |
{ |
// Ran out of bytes, so the "read-all-available-data" type packet is done |
done = YES; |
} |
|
if(done) |
{ |
[self completeCurrentRead]; |
if (!socketError) [self scheduleDequeueRead]; |
} |
else if(totalBytesRead > 0) |
{ |
// We're not done with the readToLength or readToData yet, but we have read in some bytes |
if ([theDelegate respondsToSelector:@selector(onSocket:didReadPartialDataOfLength:tag:)]) |
{ |
[theDelegate onSocket:self didReadPartialDataOfLength:totalBytesRead tag:theCurrentRead->tag]; |
} |
} |
|
if(socketError) |
{ |
CFStreamError err = CFReadStreamGetError(theReadStream); |
[self closeWithError:[self errorFromCFStreamError:err]]; |
return; |
} |
if(maxoutError) |
{ |
[self closeWithError:[self getReadMaxedOutError]]; |
return; |
} |
} |
} |
|
// Ends current read and calls delegate. |
- (void)completeCurrentRead |
{ |
NSAssert(theCurrentRead, @"Trying to complete current read when there is no current read."); |
|
[theCurrentRead->buffer setLength:theCurrentRead->bytesDone]; |
if([theDelegate respondsToSelector:@selector(onSocket:didReadData:withTag:)]) |
{ |
[theDelegate onSocket:self didReadData:theCurrentRead->buffer withTag:theCurrentRead->tag]; |
} |
|
if (theCurrentRead != nil) [self endCurrentRead]; // Caller may have disconnected. |
} |
|
// Ends current read. |
- (void)endCurrentRead |
{ |
NSAssert(theCurrentRead, @"Trying to end current read when there is no current read."); |
|
[theReadTimer invalidate]; |
theReadTimer = nil; |
|
[theCurrentRead release]; |
theCurrentRead = nil; |
} |
|
- (void)doReadTimeout:(NSTimer *)timer |
{ |
NSTimeInterval timeoutExtension = 0.0; |
|
if([theDelegate respondsToSelector:@selector(onSocket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) |
{ |
timeoutExtension = [theDelegate onSocket:self shouldTimeoutReadWithTag:theCurrentRead->tag |
elapsed:theCurrentRead->timeout |
bytesDone:theCurrentRead->bytesDone]; |
} |
|
if(timeoutExtension > 0.0) |
{ |
theCurrentRead->timeout += timeoutExtension; |
|
theReadTimer = [NSTimer timerWithTimeInterval:timeoutExtension |
target:self |
selector:@selector(doReadTimeout:) |
userInfo:nil |
repeats:NO]; |
[self runLoopAddTimer:theReadTimer]; |
} |
else |
{ |
// Do not call endCurrentRead here. |
// We must allow the delegate access to any partial read in the unreadData method. |
|
[self closeWithError:[self getReadTimeoutError]]; |
} |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Writing |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag |
{ |
if (data == nil || [data length] == 0) return; |
if (theFlags & kForbidReadsWrites) return; |
|
AsyncWritePacket *packet = [[AsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; |
|
[theWriteQueue addObject:packet]; |
[self scheduleDequeueWrite]; |
|
[packet release]; |
} |
|
- (void)scheduleDequeueWrite |
{ |
if((theFlags & kDequeueWriteScheduled) == 0) |
{ |
theFlags |= kDequeueWriteScheduled; |
[self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; |
} |
} |
|
/** |
* Conditionally starts a new write. |
* |
* IF there is not another write in process |
* AND there is a write queued |
* AND we have a write stream available |
* |
* This method also handles auto-disconnect post read/write completion. |
**/ |
- (void)maybeDequeueWrite |
{ |
// Unset the flag indicating a call to this method is scheduled |
theFlags &= ~kDequeueWriteScheduled; |
|
// If we're not currently processing a write AND we have an available write stream |
if((theCurrentWrite == nil) && (theWriteStream != NULL)) |
{ |
if([theWriteQueue count] > 0) |
{ |
// Dequeue the next object in the write queue |
theCurrentWrite = [[theWriteQueue objectAtIndex:0] retain]; |
[theWriteQueue removeObjectAtIndex:0]; |
|
if([theCurrentWrite isKindOfClass:[AsyncSpecialPacket class]]) |
{ |
// Attempt to start TLS |
theFlags |= kStartingWriteTLS; |
|
// This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are both set |
[self maybeStartTLS]; |
} |
else |
{ |
// Start time-out timer |
if(theCurrentWrite->timeout >= 0.0) |
{ |
theWriteTimer = [NSTimer timerWithTimeInterval:theCurrentWrite->timeout |
target:self |
selector:@selector(doWriteTimeout:) |
userInfo:nil |
repeats:NO]; |
[self runLoopAddTimer:theWriteTimer]; |
} |
|
// Immediately write, if possible |
[self doSendBytes]; |
} |
} |
else if(theFlags & kDisconnectAfterWrites) |
{ |
if(theFlags & kDisconnectAfterReads) |
{ |
if(([theReadQueue count] == 0) && (theCurrentRead == nil)) |
{ |
[self disconnect]; |
} |
} |
else |
{ |
[self disconnect]; |
} |
} |
} |
} |
|
/** |
* Call this method in doSendBytes instead of CFWriteStreamCanAcceptBytes(). |
* This method supports the kSocketCanAcceptBytes flag. |
**/ |
- (BOOL)canAcceptBytes |
{ |
if (theFlags & kSocketCanAcceptBytes) |
{ |
return YES; |
} |
else |
{ |
return CFWriteStreamCanAcceptBytes(theWriteStream); |
} |
} |
|
- (void)doSendBytes |
{ |
if((theCurrentWrite != nil) && (theWriteStream != NULL)) |
{ |
// Note: This method is not called if theCurrentWrite is an AsyncSpecialPacket (startTLS packet) |
|
CFIndex totalBytesWritten = 0; |
|
BOOL done = NO; |
BOOL error = NO; |
|
while (!done && !error && [self canAcceptBytes]) |
{ |
// Figure out what to write. |
CFIndex bytesRemaining = [theCurrentWrite->buffer length] - theCurrentWrite->bytesDone; |
CFIndex bytesToWrite = (bytesRemaining < WRITE_CHUNKSIZE) ? bytesRemaining : WRITE_CHUNKSIZE; |
UInt8 *writestart = (UInt8 *)([theCurrentWrite->buffer bytes] + theCurrentWrite->bytesDone); |
|
// Write. |
CFIndex bytesWritten = CFWriteStreamWrite(theWriteStream, writestart, bytesToWrite); |
|
// Unset the "can accept bytes" flag |
theFlags &= ~kSocketCanAcceptBytes; |
|
// Check results |
if (bytesWritten < 0) |
{ |
error = YES; |
} |
else |
{ |
// Update total amount read for the current write |
theCurrentWrite->bytesDone += bytesWritten; |
|
// Update total amount written in this method invocation |
totalBytesWritten += bytesWritten; |
|
// Is packet done? |
done = ([theCurrentWrite->buffer length] == theCurrentWrite->bytesDone); |
} |
} |
|
if(done) |
{ |
[self completeCurrentWrite]; |
[self scheduleDequeueWrite]; |
} |
else if(error) |
{ |
CFStreamError err = CFWriteStreamGetError(theWriteStream); |
[self closeWithError:[self errorFromCFStreamError:err]]; |
return; |
} |
else |
{ |
// We're not done with the entire write, but we have written some bytes |
if ([theDelegate respondsToSelector:@selector(onSocket:didWritePartialDataOfLength:tag:)]) |
{ |
[theDelegate onSocket:self didWritePartialDataOfLength:totalBytesWritten tag:theCurrentWrite->tag]; |
} |
} |
} |
} |
|
// Ends current write and calls delegate. |
- (void)completeCurrentWrite |
{ |
NSAssert(theCurrentWrite, @"Trying to complete current write when there is no current write."); |
|
if ([theDelegate respondsToSelector:@selector(onSocket:didWriteDataWithTag:)]) |
{ |
[theDelegate onSocket:self didWriteDataWithTag:theCurrentWrite->tag]; |
} |
|
if (theCurrentWrite != nil) [self endCurrentWrite]; // Caller may have disconnected. |
} |
|
// Ends current write. |
- (void)endCurrentWrite |
{ |
NSAssert(theCurrentWrite, @"Trying to complete current write when there is no current write."); |
|
[theWriteTimer invalidate]; |
theWriteTimer = nil; |
|
[theCurrentWrite release]; |
theCurrentWrite = nil; |
} |
|
- (void)doWriteTimeout:(NSTimer *)timer |
{ |
NSTimeInterval timeoutExtension = 0.0; |
|
if([theDelegate respondsToSelector:@selector(onSocket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) |
{ |
timeoutExtension = [theDelegate onSocket:self shouldTimeoutWriteWithTag:theCurrentWrite->tag |
elapsed:theCurrentWrite->timeout |
bytesDone:theCurrentWrite->bytesDone]; |
} |
|
if(timeoutExtension > 0.0) |
{ |
theCurrentWrite->timeout += timeoutExtension; |
|
theWriteTimer = [NSTimer timerWithTimeInterval:timeoutExtension |
target:self |
selector:@selector(doWriteTimeout:) |
userInfo:nil |
repeats:NO]; |
[self runLoopAddTimer:theWriteTimer]; |
} |
else |
{ |
[self closeWithError:[self getWriteTimeoutError]]; |
} |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Security |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (void)startTLS:(NSDictionary *)tlsSettings |
{ |
if(tlsSettings == nil) |
{ |
// Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, |
// but causes problems if we later try to fetch the remote host's certificate. |
// |
// To be exact, it causes the following to return NULL instead of the normal result: |
// CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) |
// |
// So we use an empty dictionary instead, which works perfectly. |
|
tlsSettings = [NSDictionary dictionary]; |
} |
|
AsyncSpecialPacket *packet = [[AsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; |
|
[theReadQueue addObject:packet]; |
[self scheduleDequeueRead]; |
|
[theWriteQueue addObject:packet]; |
[self scheduleDequeueWrite]; |
|
[packet release]; |
} |
|
- (void)maybeStartTLS |
{ |
// We can't start TLS until: |
// - All queued reads prior to the user calling StartTLS are complete |
// - All queued writes prior to the user calling StartTLS are complete |
// |
// We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set |
|
if((theFlags & kStartingReadTLS) && (theFlags & kStartingWriteTLS)) |
{ |
AsyncSpecialPacket *tlsPacket = (AsyncSpecialPacket *)theCurrentRead; |
|
BOOL didStartOnReadStream = CFReadStreamSetProperty(theReadStream, kCFStreamPropertySSLSettings, |
(CFDictionaryRef)tlsPacket->tlsSettings); |
BOOL didStartOnWriteStream = CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertySSLSettings, |
(CFDictionaryRef)tlsPacket->tlsSettings); |
|
if(!didStartOnReadStream || !didStartOnWriteStream) |
{ |
[self closeWithError:[self getSocketError]]; |
} |
} |
} |
|
- (void)onTLSHandshakeSuccessful |
{ |
if((theFlags & kStartingReadTLS) && (theFlags & kStartingWriteTLS)) |
{ |
theFlags &= ~kStartingReadTLS; |
theFlags &= ~kStartingWriteTLS; |
|
if([theDelegate respondsToSelector:@selector(onSocketDidSecure:)]) |
{ |
[theDelegate onSocketDidSecure:self]; |
} |
|
[self endCurrentRead]; |
[self endCurrentWrite]; |
|
[self scheduleDequeueRead]; |
[self scheduleDequeueWrite]; |
} |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark CF Callbacks |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
- (void)doCFSocketCallback:(CFSocketCallBackType)type |
forSocket:(CFSocketRef)sock |
withAddress:(NSData *)address |
withData:(const void *)pData |
{ |
NSParameterAssert ((sock == theSocket4) || (sock == theSocket6)); |
|
switch (type) |
{ |
case kCFSocketConnectCallBack: |
// The data argument is either NULL or a pointer to an SInt32 error code, if the connect failed. |
if(pData) |
[self doSocketOpen:sock withCFSocketError:kCFSocketError]; |
else |
[self doSocketOpen:sock withCFSocketError:kCFSocketSuccess]; |
break; |
case kCFSocketAcceptCallBack: |
[self doAcceptWithSocket: *((CFSocketNativeHandle *)pData)]; |
break; |
default: |
NSLog (@"AsyncSocket %p received unexpected CFSocketCallBackType %d.", self, type); |
break; |
} |
} |
|
- (void)doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream |
{ |
NSParameterAssert(theReadStream != NULL); |
|
CFStreamError err; |
switch (type) |
{ |
case kCFStreamEventOpenCompleted: |
theFlags |= kDidCompleteOpenForRead; |
[self doStreamOpen]; |
break; |
case kCFStreamEventHasBytesAvailable: |
if(theFlags & kStartingReadTLS) { |
[self onTLSHandshakeSuccessful]; |
} |
else { |
theFlags |= kSocketHasBytesAvailable; |
[self doBytesAvailable]; |
} |
break; |
case kCFStreamEventErrorOccurred: |
case kCFStreamEventEndEncountered: |
err = CFReadStreamGetError (theReadStream); |
[self closeWithError: [self errorFromCFStreamError:err]]; |
break; |
default: |
NSLog (@"AsyncSocket %p received unexpected CFReadStream callback, CFStreamEventType %d.", self, type); |
} |
} |
|
- (void)doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream |
{ |
NSParameterAssert(theWriteStream != NULL); |
|
CFStreamError err; |
switch (type) |
{ |
case kCFStreamEventOpenCompleted: |
theFlags |= kDidCompleteOpenForWrite; |
[self doStreamOpen]; |
break; |
case kCFStreamEventCanAcceptBytes: |
if(theFlags & kStartingWriteTLS) { |
[self onTLSHandshakeSuccessful]; |
} |
else { |
theFlags |= kSocketCanAcceptBytes; |
[self doSendBytes]; |
} |
break; |
case kCFStreamEventErrorOccurred: |
case kCFStreamEventEndEncountered: |
err = CFWriteStreamGetError (theWriteStream); |
[self closeWithError: [self errorFromCFStreamError:err]]; |
break; |
default: |
NSLog (@"AsyncSocket %p received unexpected CFWriteStream callback, CFStreamEventType %d.", self, type); |
} |
} |
|
/** |
* This is the callback we setup for CFSocket. |
* This method does nothing but forward the call to it's Objective-C counterpart |
**/ |
static void MyCFSocketCallback (CFSocketRef sref, CFSocketCallBackType type, CFDataRef address, const void *pData, void *pInfo) |
{ |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
|
AsyncSocket *theSocket = [[(AsyncSocket *)pInfo retain] autorelease]; |
[theSocket doCFSocketCallback:type forSocket:sref withAddress:(NSData *)address withData:pData]; |
|
[pool release]; |
} |
|
/** |
* This is the callback we setup for CFReadStream. |
* This method does nothing but forward the call to it's Objective-C counterpart |
**/ |
static void MyCFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) |
{ |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
|
AsyncSocket *theSocket = [[(AsyncSocket *)pInfo retain] autorelease]; |
[theSocket doCFReadStreamCallback:type forStream:stream]; |
|
[pool release]; |
} |
|
/** |
* This is the callback we setup for CFWriteStream. |
* This method does nothing but forward the call to it's Objective-C counterpart |
**/ |
static void MyCFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) |
{ |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
|
AsyncSocket *theSocket = [[(AsyncSocket *)pInfo retain] autorelease]; |
[theSocket doCFWriteStreamCallback:type forStream:stream]; |
|
[pool release]; |
} |
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
#pragma mark Class Methods |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
// Return line separators. |
+ (NSData *)CRLFData |
{ |
return [NSData dataWithBytes:"\x0D\x0A" length:2]; |
} |
|
+ (NSData *)CRData |
{ |
return [NSData dataWithBytes:"\x0D" length:1]; |
} |
|
+ (NSData *)LFData |
{ |
return [NSData dataWithBytes:"\x0A" length:1]; |
} |
|
+ (NSData *)ZeroData |
{ |
return [NSData dataWithBytes:"" length:1]; |
} |
|
@end |