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 client


In the Creating an echo server recipe of this chapter, we created an echo server and then tested it using telnet. Creating the server was pretty fun, but testing with telnet was a kind of anti-climax; so in this recipe, we will be creating a client that we can use to connect to our echo server.

When we created the echo server, we created a BSDSocketServer class to help with the creation of our server applications. In this recipe, we will be creating a BSDSocketClient class to help with the creation of our client applications.

Getting ready

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

How to do it…

Now let's create an echo client that will communicate with our echo server:

Creating the BSDSocketClient header file

We will begin by creating the BSDSocketClient header file, as shown in the following code:

#import <Foundation/Foundation.h>
  
typedef NS_ENUM(NSUInteger, BSDClientErrorCode) {
    NOERRROR,
    SOCKETERROR,
    CONNECTERROR,
    READERROR,
    WRITEERROR
};

#define MAXLINE 4096

@interface BSDSocketClient : NSObject

@property (nonatomic) int errorCode, sockfd;

-(instancetype)initWithAddress:(NSString *)addr andPort:(int)port;
-(ssize_t) writtenToSocket:(int)sockfdNum withChar:(NSString *)vptr;
-(NSString *) recvFromSocket:(int)lsockfd withMaxChar:(int)max;

We begin the header file by defining the five error conditions that may occur while we are connecting to the server. If an error occurs, we will set the errorCode property with the appropriate code.

We then define the maximum size of the text that we can send to our server. This is really used strictly for this example; on production servers, you will not want to put a limit such as this.

The BSDSocketClient header defines two properties, errorCode and sockfd. We expose the errorCode property, so classes that use the BSDSocketClient class can check for errors, and we expose the sockfd socket descriptor in case we want to create the client protocol outside the BSDSocketClient class.

The header file also defines one constructor and two methods, which we will be exposing in the BSDSocketClient class.

The initWithAddress:andPort: constructor creates the BSDSocketClient object with the IP address and port combination for connection. The writtenToSocket:withChar: method will write data to the socket that we are connected to, and the recvFromSocket:withMaxChar: method will receive characters from the socket.

Creating the BSDSocketClient implementation file

Now we need to create the BSDSocketClient implementation file, as shown in the following code:

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

We begin the BSDSocketClient implementation file by importing the headers needed to create our client. Let's look at the initWithAddress:andPort: constructor:

 -(id)initWithAddress:(NSString *)addr andPort:(int)port {
     self = [super init];
     if (self) {
         struct sockaddr_in	servaddr;
         
         self.errorCode = NOERRROR;
         if ( (self.sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
             self.errorCode = SOCKETERROR;
         else {
             memset(&servaddr,0, sizeof(servaddr));
             servaddr.sin_family = AF_INET;
             servaddr.sin_port = htons(port);
             inet_pton(AF_INET, [addr cStringUsingEncoding:NSUTF8StringEncoding], &servaddr.sin_addr);
         
             if (connect(self.sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
                 self.errorCode = CONNECTERROR;
             }
         }
      }
     return self;
  }

The initWithAddress:andPort: constructor is used to set up the connection with the server. We define a sockaddr_in structure named servaddr. This structure will be used to define the address, port, and IP version of our connection.

If you recall, we initialized the server for the echo server by making the socket(), bind(), and listen() function calls. To initialize a client, you only need to make two function calls. These are the same socket() call you made for the server followed by a new function called connect().

We make the socket() function call 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. If there is an issue creating the socket, we will set the errorCode variable to SOCKETERROR and skip the rest of the code.

Prior to calling the connect function, we need to set up a sockaddr structure that contains the IP version, address, and port number we will be connecting to. Before populating the sockaddr structure with the information, we will want to clear the memory to make sure that 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 for the sockaddr structure, we set the values. We set the IP version to IPv4 by setting the sin_family address to AF_INET. The sin_port number is set to the port number by using the htons() function. We convert the IP address that we are connecting to from NSString to cString and use the inet_pton() function to convert the address to a network address structure that is put into servaddr.sin_addr.

After we have our sockaddr structure set, we attempt to connect to the server using the connect() function. If the connection fails, the connect() function returns -1. Let's look at the writtenToSocket:withChar: method:

-(ssize_t) writtenToSocket:(int)sockfdNum withChar:(NSString *)vptr {
      
    size_t    nleft;
    ssize_t  nwritten;
    const char  *ptr = [vptr cStringUsingEncoding:NSUTF8StringEncoding];
      
    nleft = sizeof(ptr);
    size_t n=nleft;
    while (nleft > 0) {
      if ( (nwritten = write(sockfdNum, ptr, nleft)) <= 0) {
        if (nwritten < 0 && errno == EINTR)
          nwritten = 0;
        else {
                  self.errorCode = WRITEERROR;
          return(-1);
              }
      }
          
      nleft -= nwritten;
    ptr   += nwritten;
   }
   return(n);
 }

The writtenToSocket:withChar: method is used to write the text to the server. This method has two parameters: sockfdNum, which is the socket descriptor to write to, and vptr NSString, which contains the text to send to the server.

The writtenToSocket:withChar: method uses the write() function to write the text 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 convert vptr to cString pointed to by the ptr pointer using the cStringUsingEncoding: method.

If the write() function does not send all the text to the client, the ptr pointer will be moved to point 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 the text is written. If the write function returns 0 or less, we check for errorsLet's look at the recvFromSocket:withMaxChar: method:

 -(NSString *) recvFromSocket:(int)lsockfd withMaxChar:(int)max {
     char recvline[max];
     ssize_t n;
     
     if ((n=recv(lsockfd, recvline, max -1,0)) > 0) {
         recvline[n]='\0';
         return [NSString stringWithCString:recvline encoding:NSUTF8StringEncoding];
     } else {
         self.errorCode = READERROR;
         return @"Server Terminated Prematurely";
     }
 }
   
 @end

The recvFromSocket:withMaxChar: method is used to receive characters from the server and returns an NSString representing the characters received.

When the data comes in, the recv() function will put the incoming text into the buffer pointed to by the recvline pointer. The recv() function will return the number of bytes read. If the number of bytes is zero, the client is disconnected; if it is less than zero, it means there was an error.

If we successfully received text from the client, we put a NULL terminator at the end of the text, convert it to NSString, and return it.

Using the BSDSocketClient to connect to our echo server

The downloadable code contains examples for both iOS and OS X. If you run the iOS example in the iPhone simulator, the app looks like the following screenshot:

You will type the text you wish to send in the UITextField and then click on the Send button. The text that is received back from the server, in our case Hello from Packt, is displayed below the Text Received: label.

We will look at the sendPressed: method in the iOS sample code as an example of how to use the BSDSocketClient method. This method is called when you click on the Send button. Refer to the following code:

 -(IBAction)sendPressed:(id)sender {
     NSString *str = textField.text;
     BSDSocketClient *bsdCli = [[BSDSocketClient alloc] initWithAddress:@"127.0.0.1" andPort:2004];
     if (bsdCli.errorCode == NOERRROR) {
         [bsdCli writtenToSocket:bsdCli.sockfd withChar:str];
         
         NSString *recv = [bsdCli recvFromSocket:bsdCli.sockfd withMaxChar:MAXLINE];
         textRecvLabel.text = recv;
         textField.text = @"";
         
     } else {
         NSLog(@"%@",[NSString stringWithFormat:@"Error code %d recieved.  Server was not started", bsdCli.errorCode]);
     }
 }

We begin by retrieving the text that was entered in the UITextField. This is the text that we will be sending to the echo server.

We then initialize the BSDSocketClient object with an IP address of 127.0.0.1, which is the local loopback adapter, and a port number of 2004 (this needs to be the same port that your server is listening on). If you run this on an iPhone, you will need to set the IP address to the address of the computer that is running the echo server.

Once the connection with the server is established, we call the writtenToSocket:withChar: method to write the text entered in the UITextField to the server.

Now that we have sent the text, we need to retrieve what comes back. This is done by calling the recvFromSocket:withMaxChar: method to listen to the socket and retrieve any text that comes back.

Finally, we display the text that was received from the server to the screen and clear the UITextField so that we can enter in the next text.

How it works…

When we created the BSD echo server in the Creating an echo server recipe of this chapter, we went through a three-step process to prepare the TCP server. These were the socket (create a socket), bind (bind the socket to the interface), and listen (listen for incoming connections) steps.

When we create the BSD echo client, we make the connection in a two-step process. These are the socket (create a socket just like the echo server) and connect (this connects to the server) steps. The client calls the connect() function to establish a connection with the server. If no errors occur, it means we have successfully created a connection between the server and the client.

When you create your own clients, you will want to use the initWithAddress:andPort: constructor to initiate the connection and then write your own code to handle your protocol. You can see the Create a data client recipe of this chapter when we create a data client to send an image to the server.