Thursday, March 20, 2008

Network Communications with NSFileHandle

Okay, well, because of the NDA, I can't post code samples about how to do things using the iPhone SDK yet, but I can post regular old Cocoa code without worrying about the Apple lawyers. If that code just happens to be usable on the iPhone also... oh, well.

With the fact that so many people have downloaded the SDK, I have to think at least seven or eight of those people do not have any prior experience using Objective-C or Cocoa. Those people will have a bit of a grokkng curve. I say "grokking" instead of "learning" because the syntax of Objective-C is straightforward and relatively easy to learn. However, the approach it takes is decidedly different from the application frameworks used with Visual Basic, C#, and Java; it takes a little while to really understand the way it all fits together and start working with the frameworks rather than against it.

One of the things that I'm sure many aspiring iPhone developers will want to do in their applications is to communicate with remote servers. Now, any of the old school C programmers who want to do so will have access to BSD sockets and can do network programming just as they have always done. Because Objective-C and Cocoa build on top of C, you always have available to you everything you do when writing procedural C.

But, often, there's a better, or at least, an easier way to do things using existing Cocoa classes. Unfortunately, what that "better way" would be for this isn't quite so straightforward. One option is to use CFNetwork. CF stands for Core Foundation, and when you see something beginning with CF in the world of Apple software development, you are looking at lower level C functions that are used by the higher level libraries like Cocoa and Carbon. CFNetwork underlies many of the higher-level classes capable of communicating with remote servers. When you use NSURLConnection, or instantiate an NSString using stringWithContentsOfURL:encoding:error:, you are using classes that sit on top of CFNetwork.

And CFNetwork is great - it is robust and written to work with an existing run loop mechanism, meaning you don't have to detach threads to keep from blocking. Unfortunately, CFNetwork is conceptually hard and using it in Cocoa creates confusing, hard to maintain classes. Coercing C callbacks into an instance of a Cocoa class works, and it works well, but it just doesn't feel right in the context of Objective-C/Cocoa.

Unfortunately, there is no generic Cocoa class for accessing remote servers. There are lots of special-purpose methods and classes that sit on top of CFNetwork and let you seamlessly get information from over the network using URLs. But what can you do if you want to implement your own protocol, or access a different kind of server?

Well, there are several third party classes that you could use for this, including Dustin Voss' free AsyncSockets and the open source SmallSockets project. These are both good options if you want to use them.

Someone on the Cocoa-Dev mailing list recently pointed out that NSFileHandle is capable of handling socket communications, but you have to actually create the socket the old fashioned way and feed it to NSFileHandle using initWithFileDescriptor: to do so. Thanks to the beauty of Objective-C categories, however, you only have to do it once if you write the code generically and put it in a category.

So, thanks to the help from several people on the Cocoa-Dev list, I present a category on NSFileHandle that lets you get a conection to a remote server. You can download the category right here.

Using this category is very straight forward. You can create a new NSFileHandle that is connected to a remote server like this:
NSFileHandle *fh = [NSFileHandle fileHandleToRemoteHost:@"theremoteserver.com" onPort:80];
To send a command and get the response, you need to create a method (probably on your controller class) that will be called whenever data is available on the read stream. That method might look something like this:
-(void)process:(id)notification
{
NSFileHandle *fh = [notification object];
NSMutableData *data = [fh availableData];
NSString *dataString = [[NSString alloc]
initWithData:data
encoding:NSUTF8StringEncoding];
/* Process the data however you need to here */
[dataString release];
if (booleanVariableThatIndicatesIfWeExpectMoreData)
[fh readInBackgroundAndNotify];
}
Notice the last two lines. If we are expecting more data, we have to call readInBackgroundAndNotify again, or it won't continue to pull more data from the stream and we'll end up with only part of the server's response. If we don't expect more data, we don't have to call readInBackgroundAndNotify any more, and could also close the stream using the closeFile method of NSFileHandle. You could also leave the stream open and send more commands to it. 

If you call readInBackgroundAndNotify and there's nothing to read, your program won't be harmed, but your method won't get called again unless you send another message through to the server. So, if you have processing to do on the retrieved data once you get it all, you need to figure out in your code when you've received everything. Usually the protocol gives you a mechanism for doing that. NNTP, for example, sends a line with just a single period on it to tell you when it's done sending you data, but it leaves the connection open in case you want to send more commands through.

The nice thing here: no threads. Our application goes about its merry way, and calls us back when there's something for us to do. At this point, we've created the file handle, and we've created a method to process the responses from the server, so now we just need to send something to the remote server so that our process: method has something to wait for, which we do like this:
[fh writLine:@"HELO\r\n" withObserver:self andProcessWith:@selector(process:)];
That's all there is to it. 

As I said before, this is Cocoa code, not Cocoa Touch code, but since NSFileHandle is part of the Foundation framework and not the AppKit, it should (in theory) work unchanged in Cocoa Touch. I haven't yet tested it, but I'm going to, and as soon as the NDA lifts in June, I'll tell you if any changes are necessary to get it to work on the iPhone.

No comments:

Post a Comment