I'm currently trying to run my whole network stuff in a background thread, as it currently blocks the main thread, when the server is not reachable (i.e.).
I'm currently creating the network connection through the following code. Is there a simple way to run this in a new background thread?
An how can I throw back the received message to the main thread? An how can I send messages through the background thread?
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)ipAdress, port, &readStream, &writeStream);
inputStream = (__bridge NSInputStream *)readStream;
outputStream = (__bridge NSOutputStream *)writeStream;
[inputStream setDelegate:self];
[outputStream setDelegate:self];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
here is a tutorial that does exactly what you're talking about. although it focuses more on audio streaming, but the principles are exactly the same (ie in terms of spawning a worker thread, having it talk with the parent thread etc etc).
the idea is simple.. you create a new thread and have it handle the streaming work, and then you schedule the stream reader with the run loop that belongs to the thread you just created. The stream will have callbacks that will be fired when ceratain events happen (ie you get some data, the connect times out etc).. in the callback methods you can alert or communicate with the mainthread (which is the thread that handles the UI).
Here is some code to point you in the right direction, but if you download the code from the above tutorial and follow through.. you'll get it:
// create a new thread
internalThread =
[[NSThread alloc]
initWithTarget:self
selector:#selector(startInternal)
object:nil];
[internalThread start];
// creating a stream inside the 'startInternal' thread*
stream = CFReadStreamCreateForHTTPRequest(NULL, message);
// open stream
CFReadStreamOpen(stream)
// set callback functions
// ie say: if there are bites available in the stream, fire a callback etc
CFStreamClientContext context = {0, self, NULL, NULL, NULL};
CFReadStreamSetClient(
stream,
kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered,
ASReadStreamCallBack,
&context);
// schedule stream in current thread runloop, so that we DON'T block the mainthread
CFReadStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
// create the callback function to handle reading from stream
// NOTE: see where else in the code this function is named (ie CFReadStreamSetClient)
static void ASReadStreamCallBack
(
CFReadStreamRef aStream,
CFStreamEventType eventType,
void* inClientInfo
)
{
//handle events you registered above
// ie
if (eventType == kCFStreamEventHasBytesAvailable) {
// handle network data here..
..
// if something goes wrong, create an alert and run it through the main thread:
UIAlertView *alert = [
[[UIAlertView alloc]
initWithTitle:title
message:message
delegate:self
cancelButtonTitle:NSLocalizedString(#"OK", #"")
otherButtonTitles: nil]
autorelease];
[alert
performSelector:#selector(show)
onThread:[NSThread mainThread]
withObject:nil
waitUntilDone:NO];
}
}
Related
I'm trying to update an old Mac OS program I wrote in ASOC (mostly Applescript, but some ObjC objects for things like web service access). I used a synchronous connection:
NSData *resultsData = [NSURLConnection sendSynchronousRequest: req returningResponse: &response error: &err];
The server credentials were embedded in the URL. This worked fine for me since the program really could not continue to do anything while the data was being fetched. A change to the server authentication method however has forced the need for changes to this application. I have tried all the usual workarounds with a NSURLCredential but that still does not work with this service.
So it looks like I will need to change to the asynchronous call:
[[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
I have this working with the appropriate delegate methods, most importantly:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
Although I'd love to just use some form of delay loop to check for when the data has finished loading (essentially making it synchronous again), I have not found a way to do this that does not actually block the connection.
I am able to use a NSTimer to wait for the data before continuing:
set theJobListTimer to current application's NSTimer's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(0.05, me, "jobListTimerFired:", "", true)
on jobListTimerFired_(theTimer)
(jobListData as list) & count of jobListData
if count of jobListData ≠ 0 then
log "jobListTimerFired_ done"
tell theTimer to invalidate()
setUpJobList(jobListData)
end if
end jobListTimerFired_
but this is clumsy and does not work while I'm in a modal dialog:
set buttonReturned to current application's NSApp's runModalForWindow_(collectionWindow)
(I have a drop down in the dialog that needs to be updated with the results of the web service call). Right now, the delegate methods are blocked until the modal is dismissed.
Is there no simple way to emulate the synchronous call using the async methods?
Trying to use semaphore, I changed code to:
- (void) startConnection:(int)reqType :(NSMutableURLRequest *)request {
requestType = [NSNumber numberWithInt:reqType];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// This could be any block that is run asynchronously
void (^myBlock)(void) = ^(void) {
self.connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
myBlock();
if (self.connection) {
// create an object to hold the received data
self.receivedData = [NSMutableData data];
NSLog(#"connection started %#", requestType);
}
dispatch_time_t timeOut = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC);
dispatch_semaphore_wait(semaphore, timeOut);
dispatch_release(semaphore);
semaphore = NULL;
}
then in the connection handler:
- (void) connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"connectionDidFinishLoading %#", requestType);
NSString *returnData = [[NSString alloc] initWithData:receivedData
encoding:NSUTF8StringEncoding] ;
// NSLog(#"connectionDidFinishLoading %#", returnData);
[self handleData:requestType :returnData];
[self terminate];
if(semaphore) {
dispatch_semaphore_signal(semaphore);
}
}
However, the connectionDidFinishLoading handler (and for that matter the didReceiveResponse and didReceiveData handlers) do not get called until after the 10 second dispatch timeout. What am I missing here?
You can use dispatch_semaphore_wait to make any asynchronous API into a synchronous one again.
Here's an example:
__block BOOL accessGranted = NO;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// This could be any block that is run asynchronously
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
accessGranted = granted;
if(semaphore) {
dispatch_semaphore_signal(semaphore);
}
});
// This will block until the semaphore has been signaled
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
semaphore = NULL;
return accessGranted;
Found the answer here:
iOS, NSURLConnection: Delegate Callbacks on Different Thread?
I knew the connection was running on a different thread and tried various other while loops to wait for it to finish. But this was REALLY the magic line:
while(!self->finished]){
//This line below is the magic!
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
I've got the following code, which I know is being run:
ReadDelegate * del = [[ReadDelegate alloc] init];
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)(#"server.com"), port, &readStream, &writeStream);
NSInputStream * readSock = (__bridge_transfer NSInputStream*)readStream;
[readSock setDelegate:del];
[readSock scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
NSOutputStream * writeSock = (__bridge_transfer NSOutputStream*)writeStream;
[writeSock scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
NSLog(#"Open socket");
[readSock open];
[writeSock open];
[writeSock write:(uint8_t*)("request\0\0\0") maxLength:10];
while (YES) {
//I'm skipping over inconsequential stuff
}
NSLog(#"finished reading");
[readSock close];
[writeSock close];
return [del getMessage];
My ReadDelegate class is declared like #interface ReadDelegate : NSObject <NSStreamDelegate> and includes a - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode. That particular function just has a print statement in it to see if it's ever being called. It's not.
I know for a fact that the connection is being opened because my server is receiving the "request\0\0\0" message and the server is sending the file (I have tests in other environments which can receive the file just fine).
However, as mentioned, the ReadDelegate object declared in the beginning (del) never receives the stream message, even once (to say the stream is open or whatever).
Why is the delegate not being called?
It looks like your stream doesn't receive events because of your while loop.
Every new event from a stream can be handled in a new iteration of run loop. But the new iteration can not be started because the current one never finishes.
I have a TCP connection class which uses the NSStreamDelegate and works fine. It receives messages and respond to them in any matter. In some cases it should open a second connection. This is a second class which is quite similar to the first one.
On open, the first class should wait until the streams report open state:
- (BOOL)connectDataConnection {
__block BOOL connected = YES;
_dataConnection = [JWTCPConnection connectionWithInputStream:(__bridge NSInputStream *)readStream and outputStream:(__bridge NSOutputStream *)writeStream];
[_dataConnection openWithTimeoutBlock:^{
connected = NO;
}];
return connected;
}
// JWTCPConnection
- (id)initWithInputStream:(NSInputStream *)inStream andOutputStream:(NSOutputStream *)outStream {
if (self = [super init]) {
_iStream = inStream;
_oStream = outStream;
[_iStream setDelegate:self];
[_oStream setDelegate:self];
[_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] foreMode:NSRunLoopCommonModes];
[_oStream scheduleInRunLoop:[NSRunLoop currentRunLoop] foreMode:NSRunLoopCommonModes];
}
return self;
}
- (void)openWithTimeoutBlock:(void (^)())timeoutBlock {
_timeoutBlock = timeoutBlock;
float seconds = 5.0;
dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC);
dispatch_queue_t dispatchQueue = dispatch_queue_create("com.company.app", 0);
dispatch_async(dispatchQueue, ^{
dispatch_after(, dispatchTime, dispatchQueue, ^{
if (_timeoutBlock) {
_timeoutBlock();
[self close];
}
dispatch_semaphore_signal(_connectionSemaphore);
});
});
_connectionSemaphore = dispatch_semaphore_create(0);
[_iStream open];
[_oStream open];
dispatch_semaphore_wait(_connectionSemaphore, DISPATCH_TIME_FOREVER);
}
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
NSLog(#"a stream handels event...");
}
My problem is, that the stream delegate method -stream:handleEvent: does not get called.
I use almost the identical code in my first class, which works. Even when I remove the dispatch_semaphore_wait(); call, the delegate method doesn't fire.
In the case I don't wait, I can write to the stream. But I have to implement a timeout in an asynchronous environment (of the first class).
I call the -openWithTimeoutBlock: method inside the -stream:handleEvent: method of the first class. Could that interrupt the second classes NSStreamDelegate?
Any ideas how to fix that?
I'm learning Cocoa...
I tried different way to do that but I'm still in the black...
I have this method in my implementation:
- (void)closeStream:(NSStream *)theStream {
[theStream close];
[theStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
How can I call it from an IBAction in my #synthetize?
- (IBAction)connect:(id)sender {
if ([[connectNOK stringValue] isEqualToString:#"Disconnected"]) {
[connectButton setTitle:#"Disconnect"];
NSString * hostFromField = [hostField stringValue];
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)hostFromField, [portField intValue], &readStream, &writeStream);
inputStream = (NSInputStream *)readStream;
outputStream = (NSOutputStream *)writeStream;
[inputStream setDelegate:self];
[outputStream setDelegate:self];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
} else {
[connectButton setTitle:#"Connect"];
// I want to call this method here
}
}
If the closeStream: method is defined in the same class than the connect: method, you'll have to use:
[ self closeStream: someStream ];
Where someStream is the NSStream object you need to pass.
The self keyword refers to the current instance of the class.
If you don't know that, or what it means, I suggest you take a look at the Objective-C language basics before trying to do/code anything, and maybe after, the complete language reference.
EDIT:
I can see in your code that your connect: method «toggles» the connection based on the value of a button label.
This is not a very good design, for you to know, but you'll have other problems here.
I guess you want to close the input and output streams, if necessary.
The problem is, when the connect method is called a second time, the inputStream and outputStream variables are not accessible anymore, as they are local variables.
You probably need to store them as instance variables instead, so you can refer to them later on.
Once again, it seems you really should start by reading some documentation about programming principles, as well as some Object-Oriented programming principles.
Don't try to go too fast. Knowledge is the key to everything, so start by reading the documentation I mentioned before.
i'm trying to establish an FTP connection within an app. i want to upload several files to a FTP server, all files in one directory. So at first i want to create the remote directory.
- (void) createRemoteDir {
NSURL *destinationDirURL = [NSURL URLWithString: uploadDir];
CFWriteStreamRef writeStreamRef = CFWriteStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) destinationDirURL);
assert(writeStreamRef != NULL);
ftpStream = (__bridge_transfer NSOutputStream *) writeStreamRef;
BOOL success = [ftpStream setProperty: ftpUser forKey: (id)kCFStreamPropertyFTPUserName];
if (success) {
NSLog(#"\tsuccessfully set the user name");
}
success = [ftpStream setProperty: ftpPass forKey: (id)kCFStreamPropertyFTPPassword];
if (success) {
NSLog(#"\tsuccessfully set the password");
}
ftpStream.delegate = self;
[ftpStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// open stream
[ftpStream open];
}
This code doesn't work when executed in a background thread using the following call:
[self performSelectorInBackground: #selector(createRemoteDir) withObject: nil];
my guess is that the (background-threads) runloop isn't running?
If i send the message inside the main thread the uploading just works fine:
[self createRemoteDir];
as the runloop of the main thread is up and running.
but fairly large files are going to be uploaded; so i want to put that workload in a background thread.
but how and where do i set up the NSRunLoop, so that the whole uploading happens in a background thread? Apples documentation on NSRunLoops (especially how to start them without using a timer/input source, as in this case) didn't help me out.
I found/created a solution that at least works for me.
with the above method (createRemoteDir), the following code applied and worked for me:
NSError *error;
createdDirectory = FALSE;
/*
only 'prepares' the stream for upload
- doesn't actually upload anything until the runloop of this background thread is run
*/
[self createRemoteDir];
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
do {
if(![currentRunLoop runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]]) {
// log error if the runloop invocation failed
error = [[NSError alloc] initWithDomain: #"org.mJae.FTPUploadTrial"
code: 23
userInfo: nil];
}
} while (!createdDirectory && !error);
// close stream, remove from runloop
[ftpStream close];
[ftpStream removeFromRunLoop: [NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode];
if (error) {
// handle error
}
It runs in a background thread and creates the directory on the ftp server.
I like it more than other examples where runloops are only run for an assumed small interval, say 1second.
[NSDate distantFuture]
is a date in the futur (several centuries, according to Apples documentation). But that's good as the "break-condition" is handled by my class property createdDirectory - or the occurance of an error while starting the runloop.
I can't explain why it works without me explicitly attaching an input source to the runloop (NSTimer or NSPort), but my guess is, it is sufficient that the NSOutputStream is scheduled in the runloop of the background thread (see createRemoteDir).
You could also try to use a dispatch_async call to perform your createRemoteDir in the background. It's much simpler to use and you won't have to worry about managing extra threads.
Here's what the code would look like:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self createRemoteDir];
});