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 an echo server


In this recipe, we will be creating an echo server that will listen on port 2004. Once the connection is established, the server will echo the text received back to the client.

As we did in the earlier recipes, we will encapsulate the socket, bind, and listen steps into an Objective-C class, complete with error checking to make it easy for you to add this code to your project.

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 get started by creating a BSDSocketServer class that will greatly simplify the creation of a BSD socket server. While this recipe is focused on setting up an echo server, in the Creating a data server recipe of this chapter, you will see that the code can be modified very easily to create other types of servers.

Creating the BSDSocketServer header file

The BSDSocketServer 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 (nonatomic) int errorCode, listenfd;
-(id)initOnPort:(int)port;
-(void)echoServerListenWithDescriptor:(int)lfd;

@end

The header file of the BSDSocketServer class starts off by defining the LISTENQ constant as 1024. This constant will be the maximum number of pending connections that can be queued up at any given time before the sockets stop accepting new connection requests.

We also define the maximum length of the inbound string for the echo server, which we will set as 4096 characters.

We then define an ENUM with our five error conditions:

  • NOERROR: This determines that no errors occurred while performing the socket, bind, and listen steps

  • SOCKETERROR: This determines that the error occurred while creating the socket

  • BINDERROR: This determines that the error occurred while binding the sockaddr family of structures with the socket

  • LISTENERROR: This determines that the error occurred while preparing to listen on the socket

  • ACCEPTINGERROR: This determines that the error occurred while accepting a connection

The BSDSocketServer has two properties. The errorCode property will contain the error code if any of the functions fails, while the listenfd property will contain the socket descriptor. This descriptor can be used outside the BSDSocketServer object to create your server if you want to have your server code outside the BSDSocketServer class.

The header defines one constructor called initWithPort:, which has one parameter to define the port number to listen on. The header file also defines one method that sets up the echo server once we initialize the server within the initWithPort: constructor. As you build your own servers, you will want to add separate methods such as the echoServerListenWithDescriptor: method, to handle them while using the initWithPort: constructor to initialize the server.

Creating the BSDSocketServer implementation file

Now let's look at the BSDSocketServer implementation file. The code for this implementation file is as follows:

  #import "BSDSocketServer.h"
  #import <sys/types.h>
  #import <arpa/inet.h>
 @implementation BSDSocketServer

We begin the implementation file by importing the header files needed to implement our echo server. Let's look at the initOnPort: constructor:

-(instancetype)initOnPort:(int)port {
       self = [super init];
       if (self) {
           struct sockaddr_in servaddr;
           
           self.errorCode = NOERRROR;
           if ( (self.listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
               self.errorCode = SOCKETERROR;
           else {
memset(&servaddr, 0, sizeof(servaddr));  
servaddr.sin_family = AF_INET;
               servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
               servaddr.sin_port = htons(port);
               
               if (bind(self.listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) <0) {
                   self.errorCode = BINDERROR;
               } else {
                   
                   if ((listen (self.listenfd, LISTENQ)) <0) {
                       self.errorCode = LISTENERROR;
                   }
               }
           }
       }
       return self;
   }

The BSDSocketSever.m class has a single constructor called initWithPort:. This constructor will take a single parameter named port of type int. This port parameter is the port number that we want our server to bind to. This number can range from 0-65535; however, you will need to have the root access to bind to ports below 1024, so I recommend you to use port numbers greater than 1024.

We define a sockaddr_in structure (remember, sockaddr_in is for IPv4 and sockaddr_in6 is for IPv6) named servaddr. To begin with, we set the errorCode variable to NOERROR.

To set up a socket, we will need to call the socket(), bind(), and listen() functions. If any of these functions fail, we will want to set the errorCode variable and skip the rest of the initialization.

We use the socket() function to create our socket using the AF_INET (IPv4) and SOCK_STREAM (TCP) parameters. If you would like to use IPv6, you would change AF_INET to AF_INET6. If you would like to use UDP instead of TCP, you would change SOCK_STREAM to SOCK_DGRAM.

Prior to calling the bind() function, we need to set up a sockaddr structure that contains the IP version, interface, and port number that we will be binding the socket to. Before populating the sockaddr structure with the information, we would want to clear the memory to make sure there is no stale information that may cause our bind function to fail. We do this using the memset() function.

After we clear the memory of the sockaddr structure, we set the values. The sin_family address family is set to AF_INET, which sets the IP version to IPv4. The sin_addr.s_addr address is set using htonl(INADDR_ANY) to let the socket bind to any interface on the device. The sin_port number is set to the port number using the htons(port) function.

The htonl() and htons() functions convert the byte order of the values passed in from the host byte order to the network byte order, so the values can be properly interpreted when making network calls. If you are unsure what byte order is, you can refer to the Finding the byte order of your device recipe of this chapter.

After we have our sockaddr structure set, we use it to bind the socket to the address specified in the servaddr structure.

If our bind() function call is successful, we attempt to listen to the socket for new connections. We set the maximum number of backlog connection attempts to the LISTENQ constant, which is defined as 1024.

After we initiate the BSDSocketServer object using the initOnPort: constructor, we will have a server that is actively listening for new connections on the port, but now we need to do something when the connection comes in. That is where the echoServerListenWithDescriptor: method comes in. The echoServerListenWithDescriptor: method will listen for new connections and when one comes in, it will start a new thread to handle the connection, as shown in the following code:

   -(void)echoServerListenWithDescriptor:(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 strEchoServer:@(connfd)];
            });               
           }
       }
   }

The echoServerListenWithDescriptor: method will use the accept() function to accept incoming connections on the supplied socket descriptor.

Within the echoServerListenWithDescriptor: method, we create a for loop that will loop forever because each time a new connection is accepted, we will want to pass the 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, it will return a new socket descriptor. If there is a problem initializing the connection, the accept() function will return -1. If the connection is successfully initialized, we determine the IP address and port number from where the client is connecting and log it.

Finally, we use dispatch_async() to add our strEchoServer() method to the dispatch 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 comes in, the strEchoServer() method gets passed to the queue and then the server can go back to listening for new connections. The strEchoServer() method listens to establish connections for incoming text and then echoes that text back to the client. Refer to the following code:

  -(void)strEchoServer:(NSNumber *) sockfdNum {
      ssize_t n;
      char buf[MAXLINE];
      
      int sockfd = [sockfdNum intValue];
      while ((n=recv(sockfd, buf, MAXLINE -1,0)) > 0) {
          [self written:sockfd char:buf size:n];
          buf[n]='\0';
          NSLog(@"%s",buf);
          [[NSNotificationCenter defaultCenter] postNotificationName:@"posttext" object:[NSString stringWithCString:buf encoding:NSUTF8StringEncoding]];
          
      }
      NSLog(@"Closing Socket");
      close(sockfdNum);
  }

The strEchoServer: method has one parameter that is a socket descriptor to read from. We set up the while loop that will loop each time data comes in on the socket. When the data is received, the recv() function will put the incoming bytes into the buffer pointed to by buf. The recv() function will then return the number of bytes that are read. If the number of bytes is zero, the client is disconnected; if it is less than zero, there is an error. For the purpose of this recipe, we will close the socket if the number of bytes returned is zero or less.

As soon as the data is read from the socket, we call the written:char:size: function to write the data back to the client. This essentially is our echo server; however, we want to perform some additional steps so we can see when the data is received.

We will want to terminate the buf character array with a NULL terminator prior to converting it to NSString, so we do not get any additional garbage in our string. After we terminate the character array, we post a notification named posttext with the text from the socket. This will allow us to set an observer within our program that will receive all incoming text from the socket. In our example code, this notification will be used to display the incoming text to the screen, but it can also be used for logging or anything else we think of. If you do not want to do anything with the text that is sent, you can safely ignore the notification.

Once the client closes the connection, we will want to close the socket on our end. The close() function at the end of the strEchoServer: method does this for us if the number of bytes returned from the recv() function is zero or less:

   -(ssize_t) written:(int)sockfdNum char:(const void *)vptr size:(size_t)n {
       
     size_t    nleft;
     ssize_t    nwritten;
     const char  *ptr;
       
     ptr = vptr;
     nleft = n;
     while (nleft > 0) {
       if ( (nwritten = write(sockfdNum, ptr, nleft)) <= 0) {
         if (nwritten < 0 && errno == EINTR)
           nwritten = 0;    /* and call write() again */
         else
           return -1;      /* error */
       }
           
       nleft -= nwritten;
       ptr   += nwritten;
     }
     return(n);
   }
   
   @end

The written:char:size: method is used to write the text back to the client and has three parameters. These parameters are: sockfdNum, which is the socket descriptor to write to; the vptr pointer, which points to the text to be written; and n, which is the length of the text to be written.

The written:char:size: method uses the write() function to write the text back to the client. This method returns the number of bytes written, which may be less than the total number of bytes you told it to write. When that happens, we will need to make multiple write calls until everything is written back to the client.

We set ptr to point to the beginning of the text to send back and then set nleft to the size of the text to write. If the write function does not send all the text to the client, ptr will be moved to point to where we will begin the next write from and nleft will be set to the number of remaining bytes to write. The while loop will continue to loop until all text is written back to the client. If the write function returns a number less than 0, it means that there was a problem writing to the socket, so we return -1.

Using the BSDSocketServer class to start the echo server

The following code will start our server and can be used on both the iOS and OS X platforms:

 BSDSocketServer *bsdServ = [[BSDSocketServer alloc] initOnPort:2004];
 if (bsdServ.errorCode == NOERRROR) {
     [bsdServ echoServerListenWithDescriptor:bsdServ.listenfd];
         
 } else {
     NSLog(@"%@",NSString stringWithFormat:@"Error code %d recieved.  Server was not started", bsdServ.errorCode]);
 }

We begin by initializing our BSDSocketServer object by setting the port number for our server. In this example, we use port 2004. We then verify that we did not have any issues initializing our server and if everything was good, we call the echo server listener method.

When you create your own server, you will want to keep the initWithPort: constructor to establish the connection and then create your protocol in a separate method such as the echoServerListenWithDescriptor: method shown in this recipe. You will see an example of this in the Creating a data server recipe of this chapter.

The downloadable code contains sample projects for both iOS and OS X.

Tip

Once you download the code, you can start the server and test it using the following telnet command:

telnet localhost 2004

Once telnet makes the connection, type any text and press the Enter key. Once you press the Enter key, the text you typed in will be echoed back to you.

The following screenshot shows how the telnet session will work with our echo server:

How it works…

When you create a server using BSD sockets, you need to call the socket(), bind(), and listen() methods in that order:

  • int socket(int domain, int type, int protocol): This function returns an integer descriptor that can be used to identify the socket in all future function calls.

  • int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen): This function will bind the network interface and port combination to the socket. We will need to create a sockaddr structure with the IP version, network interfaces, and the port number to bind the socket prior to calling the bind() function.

  • int listen(int sockfd, int backlog): This function begins listening to the socket for any incoming connections.

The socket, bind, and listen steps described are the normal steps needed to prepare a TCP server and to create a listening descriptor. The listening descriptor will be used to accept incoming connections. Once we have the listening descriptor, we can then wait for incoming connections and respond to them.

When you create your own servers, you will want to use the initOnPort: constructor to initiate the server, but write separate functions to handle the incoming requests. You will see this in the Creating a data server recipe when we create a data server to receive images from a client.

Once we have our socket created, we can call the method that will listen on the socket (the echoServerListenWithDescriptor: 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 strEchoServer: function is where we actually implement our echo server. This method uses the recv() function to receive the incoming data (in our case, incoming text) from an open socket. Once the text is received, we call the written:char:size: method to write the data back to the client.