Book Image

iOS and OS X Network Programming Cookbook

By : Jon Hoffman
Book Image

iOS and OS X Network Programming Cookbook

By: Jon Hoffman

Overview of this book

Table of Contents (15 chapters)
iOS and OS X Network Programming Cookbook
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Creating a data server


In the Creating an echo server recipe, we created a server that accepted incoming text and echoed it back to the client. That recipe demonstrated how to send and receive text through a socket connection. Now you may be asking yourself, how do I send and receive datafiles, such as images or PDF files, through a socket connection?

Sending and receiving data over a socket connection is really not that different from sending and receiving text. You go through all the same steps to set up your sockets for sending or receiving, but at the end you get NSData instead of a character array.

For this recipe, we will be using the same BSDSocketServer class that we used in the Creating an echo server recipe of this chapter, since we can reuse the initOnPort: constructor and just add the methods to implement the protocol.

Getting ready

This recipe is compatible with both iOS and OS X. No extra frameworks or libraries are required.

How to do it…

Let's start creating our data server.

Updating the BSDSocketServer header file

We will be updating the BSDSocketServer header file that we created in the Creating an echo server recipe of this chapter. The new header file looks like the following code:

 #import <Foundation/Foundation.h>
 
 #define LISTENQ 1024
 #define MAXLINE 4096
 
 typedef NS_ENUM(NSUInteger, BSDServerErrorCode) {
    NOERROR,
    SOCKETERROR,
    BINDERROR,
    LISTENERROR,
    ACCEPTINGERROR
}; 
 @interface BSDSocketServer : NSObject
 
 @property int errorCode, listenfd;
 
 -(id)initOnPort:(int)port;
 -(void)echoServerListenWithDescriptor:(int)lfd;
 -(void)dataServerListenWithDescriptor:(int)lfd;
 
 @end

The only addition to the header file is where we added the new method that will be used to listen and process new requests for our data server. As we create new types of servers, we can reuse the initOnPort: constructor since all the sockets are set up the same way. How each type of server handles the incoming request will vary; therefore, you will need a separate method to handle each of the protocols.

Updating the BSDSocketServer implementation file

Even though we only define one new method in our header file, we really need two new methods in our implementation file. The first one is the dataServerListenWithDescriptor: method we defined in the header file; refer to the following code:

  -(void)dataServerListenWithDescriptor:(int)lfd {
      int connfd;
      socklen_t clilen;
      struct sockaddr_in cliaddr;
      char buf[MAXLINE];
      
      for (;;) {
          clilen = sizeof(cliaddr);
          if ((connfd = accept(lfd, (struct sockaddr *)&cliaddr, &clilen))<0) {
              if (errno != EINTR) {
                  self.errorCode = ACCEPTINGERROR;
                  NSLog(@"Error accepting connection");
              }
          } else {
              self.errorCode = NOERRROR;
              NSString *connStr = [NSString stringWithFormat:@"Connection from %s, port %d", inet_ntop(AF_INET, &cliaddr.sin_addr,buf, sizeof(buf)),ntohs(cliaddr.sin_port)];
              NSLog(@"%@", connStr);
      
              //Multi-threaded
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self getData:@(connfd)];
            });
      
          }
      }
  }

The dataServerListenWithDescriptor: method is almost an exact duplicate of the echoServerListenWithDescriptor: method. The dataServerListenWithDescriptor: method uses the accept() function to accept incoming connections on the supplied socket descriptor.

Within the dataServerListenWithDescriptor: method, we create a forever loop because each time a new connection is accepted, we will want to pass control of that connection to a separate thread and then come back and wait for the next connection.

The accept() function detects and initializes incoming connections on the listening socket. When a new connection is made, the accept() function will return a new socket descriptor. If there is a problem in initializing the connection, the accept function will return -1. If the connection is successfully initialized, we determine the IP address and port number that the client is connecting from and log them to the screen.

Finally, we use dispatch_async to add our getData() method to the queue. If we simply called the method directly without dispatch_async, the server would only be able to handle one incoming connection at a time. With dispatch_async, each time a new connection is established, the getData() method gets passed to the queue and the server can go back to listening for new connections.

The getData() method listens to establish connections for incoming data:

  -(void)getData:(NSNumber *) sockfdNum {
      ssize_t n;
      UInt8 buf[MAXLINE];
      NSMutableData *data = [[NSMutableData alloc] init];
      
      int sockfd = [sockfdNum intValue];
      while ((n=recv(sockfd, buf, MAXLINE -1,0)) > 0) {
          
          [data appendBytes:buf length:n];
      }
      close(sockfd);
      
      [[NSNotificationCenter defaultCenter] postNotificationName:@"postdata" object:data];
      
      NSLog(@"Done");
  }

In the strEchoServer: method that was used to retrieve text for our echo server, we used a char buf[MAXLINE] buffer to store the characters that we received. In the getData: method, we will use a UInt8 buf[MAXLINE] buffer to store our data as it comes in. We also define a NSMutableData object that holds all the data that is received.

Keep in mind that the MAXLINE constant limits the amount of data retrieved at a time and does not limit the total data. Where the MAXLINE constant is defined to be 4096, if we were receiving a file of 8000 bytes, we would receive the first 4096 bytes chunks. These first 4096 bytes would be appended to the NSMutableData object and then we would receive the next 3904 bytes, which would also be appended to the NSMutableData object, thus forming the entire file.

Once we receive all the data, we close the socket and post a notification with the name postdata. This notification can then be captured in our code so that we can do something with the incoming data once all the data is received. The iOS example expects the incoming data to be an image, so it displays the incoming data in a UIImageView.

Using the BSDSocketServer to create our data server

The downloadable code for this chapter contains samples for both iOS and OS X. Let's take a quick look at how we start the server in the iOS sample, by referring to the following code:

 -(void)startServer {
      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(newDataRecieved:) name:@"postdata" object:nil ] ;
      
      BSDSocketServer *bsdServ = [[BSDSocketServer alloc] initOnPort:2006];
      if (bsdServ.errorCode == NOERRROR) {
          [bsdServ dataServerListenWithDescriptor:bsdServ.listenfd];
          
      } else {
          NSLog(@"%@",[NSString stringWithFormat:@"Error code %d recieved.  Server was not started", bsdServ.errorCode]);
      }
  
  }
  
  -(void)newDataRecieved:(NSNotification *)notification {
      NSData *data = notification.object;
      imageView.image = [UIImage imageWithData:data];
  }

In the startSvr method, the first thing we do is set up a notification that will listen for the postdata notification. When the postdata notification is received, the listener will send the data to the newDataReceived: method to update our imageView with the data.

We initialize the BSDSocketServer object and tell it to listen on port 2006. If there are no errors while initializing the server, we call the dataServerListenWithDescriptor: method, which will listen for incoming data and process it.

How it works…

When we created the data server, we used the same initOnPort: constructor that we used for the echo server. This is because the same socket, bind, and listen steps are required for both. What we had to change were the methods that listened and processed incoming connections. When you create your own servers, you will also want to use the initOnPort: constructor and then write your own methods to handle the incoming connections.

Once we have our socket created, we can call the method that will listen on the socket. This is the dataServerListenWithDescriptor: method. This method uses the accept() function to listen for incoming connections. The accept() function will create a new socket for each incoming connection and then remove the connection from the listen queue. If you recall, we defined that the listen queue can contain up to 1024 connections before it stops accepting new ones.

The getData: method is where we actually implement our server. This method uses the recv() function to receive the incoming data. As the data comes in, we append it to the NSMutableData object until all the data is received.