Performing a network address resolution
Most applications will eventually need to convert host/service names to sockaddr
structures and sockaddr
structures to host/service names. The BSD Socket Library has two functions to assist with these conversions:
Getaddrinfo()
: This is a function that will return information about a given host/service name. The results are returned in anaddrinfo
structure.Getnameinfo()
: This is a function that will return the host and service names, given asockaddr
structure.
The getaddrinfo()
and getnameinfo()
functions make the gethostbyname()
, gethostbyaddr()
, and getservbyport()
functions obsolete. One of the main advantages that the getaddrinfo()
and getnameinfo()
functions has over the obsolete functions is that they are compatible with both IPv4 and IPv6 addresses.
In this recipe, we will encapsulate getaddrinfo()
and getnameinfo()
into an Objective-C class. This class will not hide most of the complexity of the two functions; however, it will save you from having to worry about NSString
to character array conversions and will also handle the memory management of the addrinfo
structures for you.
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 with the AddrInfo
class.
Creating the AddrInfo header file
The header file for the AddrInfo
class looks like the following:
#import <Foundation/Foundation.h> @interface AddrInfo : NSObject @property (nonatomic, strong) NSString *hostname, *service; @property (nonatomic) struct addrinfo *results; @property (nonatomic) struct sockaddr *sa; @property (nonatomic, readonly) int errorCode; -(void)addrWithHostname:(NSString*)lHostname Service:(NSString *)lService andHints:(struct addrinfo*)lHints; -(void)nameWithSockaddr:(struct sockaddr *)saddr; -(NSString *)errorString; @end
The addrinfo
header file defines four properties. The hostname
, service
, and results
properties will contain the results of the address resolution queries, and the errorCode
property will contain any error code that is returned.
We are also defining three methods in our header file. The addrWithHostname:Service:andHints:
method, which takes supplied hostname
, service
, and hints
(we will discuss the hints
structure when we discuss how to use the AddrInfo
class) and populates the results
property using the getaddrinfo()
function. The nameWithSockaddr:
method, which takes supplied sockaddr
and populates the hostname
and service
properties using the
getnameinfo()
function. If there is an error with either of the methods, the
errorCode
property is set to the returned error code.
The errorString
method takes the error code from the errorCode
property and returns a string that tells what the error code is.
Creating the AddrInfo implementation file
To create the AddrInfo
implementation file, we use the following code:
#import "AddrInfo.h" #import <netdb.h> #import <netinet/in.h> #import <netinet6/in6.h> @implementation AddrInfo -(instancetype)init { self = [super init]; if (self) { [self setVars]; } return self; }
We begin the implementation file by importing the headers that are needed. We also define an init
constructor for our class that uses the setVars
method to reset our properties to default values. Let's look at the addrWithHostname:Service:andHints:
method:
-(void)addrWithHostname:(NSString*)lHostname Service:(NSString *)lService andHints:(struct addrinfo*)lHints { [self setVars]; self.hostname = lHostname; self.service = lService; struct addrinfo *res; _errorCode = getaddrinfo([_hostname UTF8String], [_service UTF8String], lHints, &res); self.results = res; }
The addrWithHostname:Service:andHints:
method will retrieve the addresses for a given hostname. We start off by resetting the properties to default values using the setVars
method. We then set the hostname
and service
properties with the values passed to the method.
Since the getaddrinfo()
function expects character arrays for hostname
and service
, we need to convert our NSString
values to character arrays. This is done by using the UTF8String
method of the NSString
class. We also pass the addrinfo hints
structure and the address of the res addrinfo
structure. The results of the getaddrinfo()
function are put into the errorCode
property. If the getaddrinfo()
function call was successful, errorCode
will be equal to 0
.
When the getaddrinfo()
function returns, the res
structure contains the results that we use to set the results
property:
-(void)nameWithSockaddr:(struct sockaddr *)saddr { [self setVars]; char host[1024]; char serv[20]; _errorCode = getnameinfo(saddr, sizeof saddr, host, sizeof host, serv, sizeof serv, 0); self.hostname = [NSString stringWithUTF8String:host]; self.service = [NSString stringWithUTF8String:serv]; }
The nameWithSockaddr:
method will retrieve the names associated with a given IP address. We start this method by calling the setVars
method to initialize the object's properties. We then define the two character arrays that will contain the results of the
getnameinfo()
function call.
The getnameinfo()
function will take the address information from the saddr sockaddr
structure, perform a lookup for the host/service name, and put the results into the host
and serv
character arrays. If the getnameinfo()
function was successful, it will return 0
, otherwise it will return -1
.
Finally, we convert the host
and serv
character arrays to NSStrings
and put the values into the hostname
and service
properties:
-(void)setVars { self.hostname = @""; self.service = @""; self.results = @""; _errorCode = 0; }
The setVars
method simply sets all the method's NSString
properties to empty strings and the errorcode
property to 0
. This gives us a well-defined starting point for the method properties to make sure they do not contain stale information. Let's look at the errorString:
method:
-(NSString *)errorString { return [NSString stringWithCString:gai_strerror(_errorCode) encoding:NSASCIIStringEncoding]; }
The errorStiring
method uses the gai_strerror()
function to convert the error code from either the getnameinfo()
or getaddrinfo()
function calls to an actual error method that can tell us what went wrong; let's look at the setResults
: method:
-(void)setResults:(struct addrinfo *)lResults { freeaddrinfo(self.results); _results = lResults; }
We create the setResults:
method because we need to call the freeaddrinfo()
function to release the results before setting the new results. This will avoid memory leaks in our application.
Using the AddrInfo class to perform the address/hostname resolution
In the following sample code, we will show how to get the hostname www.packtpub.com to list the IP addresses and then convert those IP addresses back to the hostnames:
struct addrinfo *res; struct addrinfo hints; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM;
We begin our address/hostname resolution code by setting up two addrinfo
structures. The res
structure will be used as a temporary store when we loop though the linked list of results that are returned to us from the addrWithHostname:Service:andHints:
method. The hints
structure will store the hints that we are going to pass to the addrWithHostname:Service:andHints:
method to let the method know what type of addresses we are looking for.
Whenever you create a new structure that you plan on setting the values for, you should always use the memset()
function to clear the memory of the structure. This will ensure that there is nothing in the memory that will corrupt the structure.
We set ai_family
to AF_UNSPEC
and ai_socktype
to SOCK_STREAM
. This tells the getaddrinfo()
function that we are looking for any IP version (IPv4 or IPv6) but limiting our socket type to socket streams (these settings are used when we want to make a TCP connection). We could set the ai_family
to AF_INET4
to limit the results to only IPv4 results, or set it to AF_INET6
for only IPv6 results. Let's look at how we would initiate the AddrInfo object:
AddrInfo *ai = [[AddrInfo alloc] init]; [ai addrWithHostname:@"www.packtpub.com" Service:@"443" andHints:&hints]; if (ai.errorCode != 0) { NSLog(@"Error in getaddrinfo(): %@",[ai getErrorString]); return -1; }
We now initiate our AddrInfo
object and call the addrWithHostname:Service:andHints:
method. For our example, we are requesting an address lookup for the www.packtpub.com hostname. The service we are requesting is port 443
, which is HTTPS, and we are also supplying our hints
structure, which specifies the type of addresses we are looking for.
The code then checks to see if we have any errors; if so, it logs them and exits. Depending on what your application does, you will probably want to catch the error and display a message to the user. Let's loop though the addresses and display the results:
struct addrinfo *results = ai.results; for (res = results; res!= NULL; res = res->ai_next) { void *addr; NSString *ipver = @""; char ipstr[INET6_ADDRSTRLEN]; if (res->ai_family == AF_INET) { struct sockaddr_in *ipv4 = (struct sockaddr_in *)res->ai_addr; addr = &(ipv4->sin_addr); ipver = @"IPv4"; } else if (res->ai_family == AF_INET6){ struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)res->ai_addr; addr = &(ipv6->sin6_addr); ipver = @"IPv6"; } else { continue; } inet_ntop(res->ai_family, addr, ipstr,sizeof ipstr); NSLog(@" %@ %s", ipver, ipstr); AddrInfo *ai2 = [[AddrInfo alloc] init]; [ai2 getNameWithSockaddr:res->ai_addr]; if (ai2.errorCode ==0) NSLog(@"--%@ %@",ai2.hostname, ai2.service); } freeaddrinfo(results);
If there are no errors, we loop though the results. After we initialize the variables, we check to see if the address family is AF_INET
(IPv4 address). If so, we create a sockaddr_in
structure, retrieve the address from the sin_addr
variable, and set ipver
to IPv4
.
If the address family was not AF_INET
, we check to see if the address family is AF_INET6
(IPv6 address). If so, we create a sockaddr_in6
structure, retrieve the address from the sin_addr6
variable, and set ipver
to IPv6
.
If the address family is neither AF_INET
nor AF_INET6
, we continue the for
loop without logging the address.
The inet_ntop()
function converts the address from binary to text form so that we can display it. The NSLog
line will display the IP version followed by the IP address.
Now that we have retrieved the IP address, we will need to send it back to the hostname. For this, we take the sockaddr
from our results
structure and send it to the nameWithSockaddr:
method of the AddrInfo
class. When the nameWithSockaddr:
method completes, it will populate the hostname
and service
properties of the AddrInfo
object.
Finally, we use the freeaddrinfo()
function to release the results in order to prevent any memory leaks.
How it works…
In this recipe, we used the
getaddrinfo()
and
getnameinfo()
functions to get the IP address and hostname. These functions are provided as part of the standard POSIX API.
While these functions are black-box functions, there is really nothing magical about them. Internally, these functions call lower-level functions to send our requests to the appropriate DNS server to perform the resolution.