« Getting Started With Portable Distributed Objects (PDOs)

Posted by Submission on December 05, 2001 [Feedback (7) & TrackBack (0)]

by H. Lally Singh

(website | email)

In the next few articles, we're going to build an Instant Messenger client and server. It won't be compatible with any other system, but it will get the basics down. Most importantly, it's a vehicle to cover a diverse range of topics:

  • Distributed Objects - Getting the client & server to talk to each other
  • Split Views, TableViews, and other GUI Tricks - An introduction to some of the less obvious Cocoa user interface components
  • Multiple Nib Files - Using more than one Nib in a program, covering fun things like that funny 'File Owner' icon in Interface Builder
  • Exception Handling - Objective-C's exception handling mechanism
  • And maybe a few Odds & Ends along the way

So, the first task we want to take care of is getting a client & server to be able to talk to each other. In case you want to follow along in the source, here's the client and the server.

Cocoa makes it easy for objects in different threads, programs, or even computers to communicate. Through a mechanism called Portable Distributed Objects, Cocoa lets you 'vend' an object for others to access outside of your thread (and thusly, process and computer).

Distributed Objects are also very easy to use! In fact Stepwise has an article on them, but we'll do one here in our own style. Hopefully you'll have a good idea of what's going on with both of our and Stepwise's article to work with.

We'll cover the general case of using Portable Distributed Objects, and then go back and show how to use them across thread and network boundaries, each of which require a little bit more code.

 

// Distributed Objects Defined

A Distributed Object is an object that other objects may access even if they're outside it's own thread.

Cocoa pulls this off with it's NSConnection class, which sets up a connection between two NSDistantObject objects which act as middlemen (or a proxy if you want to use the fancy-shmancy term) between the client and the serving object. Here's an illustration to help out:

Cocoa's Distributed Object System
How Cocoa's distributed object classes work together.

So, the object you want to distribute becomes a server of sorts, that other client objects talk to. So, the next question is, how do you 'vend' this object?

 

// Vending an Object

To vend an object, you just have to get an NSConnection and get it to vend your object for you. NSConnection will sit on an NSRunloop listening from an NSPort waiting for incoming connections. Then, NSConnection will decode the parameters given in the incoming message and pass them on to the vended object. The code for this is pretty simple:

-installServer: (NSNotification) notification
{
    NSConnection *theConnection;
    theConnection = [NSConnection defaultConnection];
    NSLog(@"Creating connection...");

    [theConnection setRootObject:self];
    if ([theConnection registerName:@"svr"] == NO) {
      NSLog(@"Failed to register name\n");
    }
    
   // insert other things you might want to do when
   // you connect to the server here...
   // note that this is where the NSNotification is used 
   // in the full version of this program.

    [theConnection retain];
    [theConnection setDelegate:self];
    NSLog(@"done.\n");    
    return self;
}

In a previous article, I mentioned the utility of NSLog and it's derivatives. Any time saved from having to use the debugger, the better I say!

Let's cover what this code does: first, it gets an NSConnection, then it sets that NSConnection's "root object" to the vended object, registers it with a name, retains the NSConnection, and then sets that vended object as the delegate. Note that this code sets itself up as the vended object. That's basically the meat of what's necessary to vend an object. Let's get into the specifics.

The first step to vend an object is to get an NSConnection that will vend it. There's always one NSConnection per thread available, and we use it here. Use theConnection = [[NSConnection alloc] init] in case you've got to vend more than one object.

The next step is to set tell the NSConnection what to vend. This is pretty straightforward: just give a pointer to setRootObject:.

The next big step is to associate that NSConnection with a name. The name is used by the client to identify what vended object it wishes to talk to. I used the extremely popular "svr" service name here, but feel free to actually give it a useful name.

Then, retain the NSConnection so it doesn't get deleted under your feet, and if you want, set up a delegate for it. The delegate for an NSConnection will have it's methods called whenever a new incoming connection arrives, when data is being sent, and when data is being received. These delegate callbacks allow the delegate object to authorize incoming connections, checksum or sign outgoing data, and verify incoming data (from it's checksum or digital signature). For more information on NSConnection's delegates, Apple's NSConnection documentation does a good job. We use it in the server to print out debugging messages for incoming connections.

 

// Calling a Method on a Vended Object

Here's an example:

[vendedObject doSomething];

What, you thought it would be hard? C'mon! This is Cocoa! Co-coa!

 

// Getting a Vended Object

Every server needs a client. So, let's talk about how to connect to a vended object. Here's the code from the messaging client we're building:

- initWithName:  (NSString *) clientName 
        server:  (NSString *) serverName
{
    [super init];
    m_clientName = clientName;
    [m_clientName retain];

    NSLog(@"Getting connection to server...\n");

   // create our window manager    
    m_mgr = [[ClientWinManager alloc] initWithProxy: self];

   // Connect to server
    m_server = [NSConnection
      rootProxyForConnectionWithRegisteredName:@"svr" 
      host:serverName];
   
   // check if connection worked.
    if (m_server == nil) {
      NSLog(@"couldn't connect with server\n");
    } else {
       //
       // set protocol for the remote object & then register ourselves with the 
       // messaging server.
      [m_server setProtocolForProxy:@protocol(NetServerProto)];
      if (![m_server registerClient: self withName: m_clientName]) {
         [m_server release];
         m_server = nil;
         NSLog(@"failed.. already a client with same name attached.\n");
      } else {
         NSLog(@"done\n");
      }
    }
    
    [m_server retain];
    return self;
}

This routine opens a connection to the server and registers its object as a messaging client of it. The first important line in this routine relating to distributed objects is the call to rootProxyForconnectionWithRegisteredName:host:. This method creates a new NSConnection that serves as a proxy to the remote object on the specified host with the specified registered name. This name is the same one that the server used in registerName:. Then, it tells the NSConnection the protocol that the remote object implements. This is optional, but results in a significant speedup. Finally, it calls a method on the server object that registers this messaging client.

 

// Defining the Protocols

For reasons far more important than performance, you should define the protocols that the remote objects use to communicate. I didn't plan out the protocol ahead of time, and you'll see why it was a bad idea as the messaging server & client develop (hint: sometimes method names alone can be a strong point of confusion). But, Cocoa lets you define these protocols programatically. Here's the protocols used by the messaging system:

//
// NetClientProto
//
//  A protocol for clients to follow -- mostly just to receive messages
@protocol NetClientProto
    - (BOOL) sendMessage:( in bycopy NSString *) msg 
                    from: (in bycopy NSString *) sender;
    - (BOOL) stillThere;
@end

//
// NetServerProto
//
//  A messaging server protocol
@protocol NetServerProto
- (BOOL) registerClient: (byref id)client 
              withName: (bycopy NSString *) name;
- (BOOL) unregisterClientWithName: (bycopy NSString *) name;
- (BOOL) queryClient:(bycopy NSString *) client;
- (BOOL) sendMessage:(bycopy NSString *) msg 
                from:(bycopy NSString *) sender 
                  to:(bycopy NSString *) recipient;
@end

As you can see, you define these protocols simply as a set of methods that can be implemented. Also, you have some interesting tags to help specify which parameters are passed by value or by reference (remember Programming 101?). byref specifies that a reference to the original object be sent to the implementor, and bycopy specifies that a copy of the object be created in the implementor's thread. There is also a third type of specifier (that I didn't find use for here) for methods called oneway, which tells Cocoa not to wait for the function to return. Note that this also prevents you from knowing if the method call completed successfully (or at all).

Then, you implement them:

//
// Net server
//
//  A messaging server class
@interface NetServer : NSObject <NetServerProto> {
    NSMutableDictionary * m_clients;
}

- init;
- (void) pingClients;
- (int) getNumberOfClients;
- (NSArray*) getClientNames;
- (BOOL) registerClient: (id)client withName: (NSString *)name;
- (BOOL) unregisterClientWithName: (NSString *)name;
- (BOOL) queryClient:(NSString *) client;
- (BOOL) sendMessage:(NSString *) msg 
                from: (NSString *)sender 
                  to:(NSString *) recipient; 
@end

Just declare that you implement the protocol and then write the routines like you would any other. Don't even bother specifying the bycopy & byrefs, as it's not important on the server side.

 

// Using Distributed Objects to Communicate Between Threads

Since Distributed Objects works between processes, it would be logical to assume that you could use it between threads in the same process.. Ok, it's not 100% logical, but shut up. All you have to do is specify the NSPorts yourself and the NSConnections can talk to each other, and they'll be thread-safe about it.

Here's some code to show how to do it, ripped right from the Apple Docs:

//
// In some delegate of NSApplication...
- (void)applicationDidFinishLaunching:(NSNotification *)note
{
    NSPort *port1;
    NSPort *port2;
    NSArray *portArray;

    port1 = [NSPort port];
    port2 = [NSPort port];
   
    kitConnection = [[NSConnection alloc] initWithReceivePort:port1 sendPort:port2];
    [kitConnection setRootObject:self];

    // Ports switched here.
    portArray = [NSArray arrayWithObjects:port2, port1, nil];

    [NSThread detachNewThreadSelector:@selector(connectWithPorts:)
              toTarget:[Calculator class]  
              withObject:portArray];

    return;
}

//
// In class Calculator...
+ (void)connectWithPorts:(NSArray *)portArray
{
    NSAutoreleasePool *pool;
    NSConnection *serverConnection;
    Calculator *serverObject;
   
    pool = [[NSAutoreleasePool alloc] init];
   
    serverConnection = [NSConnection
            connectionWithReceivePort:[portArray objectAtIndex:0]
            sendPort:[portArray objectAtIndex:1]];
   
    serverObject = [[self alloc] init];
    [(id)[serverConnection rootProxy] setServer:serverObject];
    [serverObject release];
   
    [[NSRunLoop currentRunLoop] run];
    [pool release];
   
    return;
}

The code is pretty straightforward. All it does is create a new thread that vends an object of class Calculator in a new thread that acts as a server.

The first routine, applicationDidFinishLaunching:, just creates two ports, sets up its own NSConnection using them, and then sends them over to a thread it creates. Note that it switches the send & receive ports... If you don't see why, think "patch cable." Also note that since there's only one NSConnection listening on these NSPorts, it's not necessary to register a name for it. Also also note that you need a new NSRunLoop for this thread, so that's the last line this thread runs (before cleaning up after itself).

 

// Using Distributed Objects to Communicate Across the Network

TCP/IP is the language of the Internet, and we are all bound by its rules. Hence, you have to set up your NSConnection for TCP/IP. To vend objects across a network, you have to decide on a TCP port to use. A good place to look for ones already used is in /etc/services on your Mac OS X machine. It's there, but the /etc directory is hidden by default, so it's probably easiest to view /etc/services by giving the full filename manually to your favorite editor or by using Terminal.app. Once you've chosen one, you just have to create some NSSocketPorts (a subclass of NSPort) which talk over it. Here's what you have to do:

// server (init the RECEIVE port as it LISTENS for connections)
NSSocketPort  *port = [[NSSocketPort alloc] initWithTCPPort:1234];
NSConnection  *connection = [[NSConnection alloc] initWithReceivePort:port sendPort:nil]];
[connection setRootObject:whateverYourVending];
 

// client (init the SEND port as it MAKES connections)
id server;
NSSocketPort  *port = [[NSSocketPort alloc] initWithTCPPort:1234  host:@"somehost" ]];
NSConnection  *connection = [NSConnection  connectionWithReceivePort:nil sendPort:port];
server = [connection rootProxy];
[server setProtocolForProxy:@protocol(someprotocol)];

(this code courtesy of Charles Bennet, Cocoa-dev archives: http://lists.apple.com/archives/cocoa-dev/2001/Jul/10.html).

 

// Using Distributed Objects in the Messaging System

So, now you know how to distribute your objects among threads & processes. Let's talk about how this applies in our uberexample. In the messaging system, we'll use distributed objects to handle all our communication between the clients & the server. The system works as follows:

  1. Server starts up, vends its Server object.
  2. Client starts up, gets a reference to the Server object
  3. Client registers itself with the server via registerClient:withName:
  4. Client queries the Server for to look for clients in the Client's "friend list" that are also currently registered, refreshed once a second.
  5. Client initiates a conversation by calling sendMessage:from:to: on the Server object.
  6. Server calls the sendMessage:from: method on the other client's registered object.
  7. Other client displays message, user responds with another message. This remote client calls sendMessage:from:to: on the Server again to send the response
  8. Server maintains its list of the active clients by calling the stillThere: method on all the clients. If the call raises an exception (we'll talk about those in a later article), it's removed from the list of active clients.

The protocols are listed above, and this outline should fill in the blanks as how this pair of programs work together. The specifics are in further articles and in the source code. Reading source code is a good thing!

 

// Conclusion

Now that you know how to use distributed objects, you can talk between threads, across processes, and across a network with ease. We give an fairly complete example in the messaging system and show how it uses Portable Distributed Objects. Enjoy network programming the easy way!


Comments

Great article! Thanks.

I have one request though: please fix the links for the source code. That would make the article much more useful.

Thanks again.

Posted by: Travis Cripps on January 4, 2003 06:41 PM

How do I use a push button with PDOs

Posted by: gabriel on March 7, 2003 03:32 PM

I must be missing something. when I run the server and the client without modification, the client can't connect to the server. (m_server on the client is not initialized by [NSConnection rootProxyForConnectionWithRegisteredName:@"svr" host:serverName];

how does this work?

Posted by: noah sorscher on April 24, 2003 11:53 PM

This tutorial doen't work.

Posted by: Jeff on June 23, 2003 12:06 PM

Has anyone gotten this tutorial to work? I get the same error as noah sorscher :-(

I like to look at the code, but it would be great if it actually worked.

Posted by: Ian Gillespie on September 27, 2003 09:18 PM

OK, I got it to work. In the Client.m file in the - initWithName: (NSString *) clientName
server: (NSString *) serverName

method, there is a line of code like this:

m_server = [NSConnection
rootProxyForConnectionWithRegisteredName:@"svr"
host:serverName];

Change serverName to nil and it the client should be able to make a connection to ther server.

Posted by: Ian Gillespie on September 27, 2003 10:17 PM

Do the frameworks support PDO between Java and Obj-C? Say we want one or more Java servers with Obj-C clients on different hosts, is there a way?

Posted by: Brian Arthur on November 15, 2003 05:52 PM
Post a comment