5

Background

I have been kind of teaching myself about sockets and network programming from various sample code and the classic Unix Network Programming textbook while simultaneously trying to put the knowledge to work in an app that I am working on right now. I am currently working on a part of the application which requires a simple client-server setup.

Here's how it goes right now (well, how it should go):

  1. Server publishes itself with NSNetService and creates a socket using CFSocketCreateWithNative()
  2. Client finds the server with NSNetServiceBrowser
  3. Client resolves the discovered service
  4. Server gets a callback from CFSocket, which creates a new instance of a class (MyConnection) to handle the connection. Read and write streams for the connection are attained with CFStreamCreatePairWithSocket().
  5. The client messages the server (@"hi")
  6. The server sends the data that it receives from the client back to the client (this is where my problem is)
  7. The client displays the string in a UIAlertView

Two Questions

  1. I am getting an "Operation now in progress" error when I try to have the server send the data back to the client, as annotated in the Connection code below. I believe this is because the NSOutputStream does not have space available. What is the best way to deal with this? I know it should wait for the NSStreamEventHasSpaceAvailable event, but it seems that it is not happening…
    EDIT: duh... When I was getting this error I was testing the app with the iPhone simulator only and letting it act as the server and client because I didn't have internet in my new apartment yet :P It seems to be a non-issue when using two real devices.

  2. Is it possible to make this server so that the sending and receiving of data from each connection object does not block the sending and receiving of data from other connection objects? Does each new connection object need to be on a new runloop or thread or something like that? I have fished around the apple concurrency documentation, but nothing is jumping out... The goal is to have a reply sent to the client as fast as possible, no matter how many other clients are connected to the server.
    UPDATE: In stead of allowing concurrent connections to this server I am considering just queueing the connections and handling them one at a time since the amount of data that needs to be sent to each client is very small. Is this the best decision? What if there are hundreds of clients in the queue? On second thought, this might be a bad idea because the establishment of a connection takes a second or two on a fast local network, and takes even longer with bluetooth... I would love some expert advice on this matter :)

Relevant Code

Note: APNetService and APNetServiceBrowser are analogous to NSNetService and NSNetServiceBrowser

Server Code

- (void) startServerForGroup:(NSString *)name
{
  self.groupName = name;

  NSInteger port = [self prepareListeningSocket];

  self.service = [[APNetService alloc] initWithDomain:@"local."
                                                 type:@"_example._tcp." 
                                                 name:self.groupName
                                                 port:port];
  self.service.delegate = self;
  [self.service publish];
}

- (NSInteger) prepareListeningSocket
{
  int     listenfd, err, junk, port;  
  BOOL    success;

  struct sockaddr_in addr;

  port = 0;

  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  success = (listenfd != -1);

  if (success) {
    bzero(&addr, sizeof(addr));  
    addr.sin_len    = sizeof(addr);
    addr.sin_family = AF_INET;
    addr.sin_port   = 0;
    addr.sin_addr.s_addr = INADDR_ANY;
    err = bind(listenfd, (const struct sockaddr *) &addr, sizeof(addr));
    success = (err == 0);
  }
  if (success) {
    err = listen(listenfd, 5);
    success = (err == 0);
  }
  if (success) {
    socklen_t   addrLen;

    addrLen = sizeof(addr);
    err = getsockname(listenfd, (struct sockaddr *) &addr, &addrLen);
    success = (err == 0);

    if (success) {
      assert(addrLen == sizeof(addr));
      port = ntohs(addr.sin_port);
    }
  }
  if (success) {
    CFSocketContext context = { 0,(__bridge void*) self, NULL, NULL, NULL };

    CFSocketRef socket = CFSocketCreateWithNative(
                                                  NULL, 
                                                  listenfd, 
                                                  kCFSocketAcceptCallBack, 
                                                  AcceptCallback, 
                                                  &context
                                                  );
    if (socket) {
      self.listeningSocket = socket;
      CFRelease(socket);
      success = YES;
    }

    if (success) {
      CFRunLoopSourceRef  rls;

      listenfd = -1;

      rls = CFSocketCreateRunLoopSource(NULL, self.listeningSocket, 0);
      assert(rls != NULL);

      CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);

      CFRelease(rls);
    }
  }

  if ( success ) {
    return port;
  } 
  else {
    NSLog(@"FAILED TO START SERVER");

    if (listenfd != -1) {
      junk = close(listenfd);
      assert(junk == 0);
    }
    return -1;
  }
}

#pragma mark - Callback

// Called by CFSocket when someone connects to the listening socket
static void AcceptCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
{
  MyServer *  obj;

  obj = (__bridge MyServer *) info;

  assert(s == obj->_listeningSocket);

  MyConnection *newCon = [[MyConnection alloc] initWithFileDescriptor:*(int*)data];

  [newCon startReceive];

    //add the new connection object to the servers mutable array of connections
  [obj.connections addObject:newCon];

}

Connection Code

- (void) startReceive
{
  CFReadStreamRef     readStream;
  CFWriteStreamRef    writeStream;

  CFStreamCreatePairWithSocket(NULL, self.fd, &readStream, &writeStream);

  self.inputStream =  (__bridge_transfer NSInputStream *) readStream;
  self.outputStream = (__bridge_transfer NSOutputStream*) writeStream;

  [self.inputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];
  [self.outputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];

  self.inputStream.delegate = self;
  self.outputStream.delegate = self;


  [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

  [self.inputStream open];
  [self.outputStream open];
}


#pragma mark - NSStreamDelegate

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
  switch (eventCode) {

    case NSStreamEventHasBytesAvailable: {
      NSInteger       bytesRead;
      uint8_t         buffer[32768];

      bytesRead = [self.inputStream read:buffer maxLength:sizeof(buffer)];
      if (bytesRead == -1)...
      else if (bytesRead == 0)...
      else {
                NSData  *data = [NSData dataWithBytes:buffer length:bytesRead];
                [self didReceiveData:data];
      }
    } break;
    case NSStreamEventHasSpaceAvailable: {
      self.space = YES;
    } break;

    . . .
  }
}

- (void) didReceiveData:(NSData *)data
{
  if (self.space) 
    NSLog(@"SPACE");
  else
    NSLog(@"NO SPACE"); //this gets printed

  NSInteger i = [self.outputStream write:data.bytes maxLength:data.length];

  if (i < 0) {
    printf("%s",strerror(errno)); //"Operation now in progress" error
  }  
}

Client Code

#pragma mark - APNetServiceBrowserDelegate

- (void) browser:(APNetServiceBrowser *)browser didAddService:(APNetService *)service moreComing:(BOOL)moreComing
{
        //omitting checks that determine which server to connect to, if multiple

    service.delegate = self;
    [service resolveWithTimeout:20]; 
}


#pragma mark - APNetServiceDelegate

- (void) netServiceDidResolveAddress:(APNetService *)service
{    
  NSInputStream   *input; 
  NSOutputStream  *output;

  [service getInputStream:&input outputStream:&output];

  self.inputStream = input;
  self.outputStream = output;

  self.inputStream.delegate = self;
  self.outputStream.delegate = self;

  [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
  [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

  [self.inputStream open];
  [self.outputStream open];
}


#pragma mark - NSStreamDelegate

- (void) stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    switch (eventCode) {      

    case NSStreamEventHasBytesAvailable: {
      NSInteger       bytesRead;
      uint8_t         buffer[32768];

      bytesRead = [self.inputStream read:buffer maxLength:sizeof(buffer)];

      if (bytesRead == -1) NSLog(@"Error reading data");
      else if (bytesRead == 0) NSLog(@"no bytes read");
      else {
                NSData  *data = [NSData dataWithBytes:buffer length:bytesRead];
                [self didReceiveData:data];
      }
    } break;

    case NSStreamEventHasSpaceAvailable: {
      if (!self.isWaitingForReply) {
        [self sendHelloMessage];
      }
    } break;
        //omitted other NSStreamEvents    
  }
}

- (void) sendHelloMessage
{
  NSData *d = [NSKeyedArchiver archivedDataWithRootObject:@"hi"];

  [self.outputStream write:d.bytes maxLength:d.length];
  self.isWaiting = YES;
}


- (void) didReceiveData:(NSData *)data
{
  NSString  *string = [NSKeyedUnarchiver unarchiveObjectWithData:data];

  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Message" 
                                                  message:string
                                                 delegate:self
                                        cancelButtonTitle:@"OK"
                                        otherButtonTitles:nil];
  [alert show];
}
RyanM
  • 4,474
  • 4
  • 37
  • 44

1 Answers1

0

As from what I see, you want to make a server that can handle many connections very fast. A good starting point documentation mat be the c10k page : http://www.kegel.com/c10k.html. And yes queuing may be a good idea, it will consume much less resources than forking and will respond faster. However that also mean that your computations stays relatively small so that you are still able to reply fast. A good start maybe http://libevent.org/, check how it did it, it's design for this kind of use.

good luck.

Fred
  • 484
  • 8
  • 16