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.