logo
Published on IPv6style (http://www.ipv6style.jp)

IPv6 Programming on Windows Tutorial

By admin
作成日時 2005-11-14 00:00
IPv6 Programming on Windows Tutorial
Part 2: Creating an IPv6-ready TCP echo client program
(2005.11.14)

Junya Kato
NTT Information Sharing Platform Laboratories



Introduction

In Part 1, we set up the development environment and created a simple sample program that resolves the IPv6 address of a given host name.  In Part 2, we will create an IPv6-ready TCP echo client program and actually run it.  For the next article, I plan to explain how to create a TCP echo server.  The TCP echo protocol (RFC862) is quite simple. First, the client sends a character string to the server and then the server sends the same character string that it received back to the client.  The objective of this article is to teach readers the basic operation of an IPv6-ready client through the creation of a TCP echo program.


IPv6-ready (protocol-independent) program

The IPv6-ready program that we will work with in this series is basically a program that supports both IPv4 and IPv6.  In general, TCP client programs that support both have a feature that automatically determines if the IPv6 and IPv4 protocols are available and connects.  In many cases, it chooses IPv6 first, and if it cannot connect with IPv6, it switches to IPv4.  There are some atypical programs that require the IPv6 protocol to run.  However, many programs that use TCP, such as TCP echo, web servers and browsers, mail (SMTP, POP, IMAP) servers, mail client, etc., support switching between IPv6 and IPv4.

The focus of this article is protocol-independent programming.  The goal is not only to support IPv6.  The readers will learn how to create a program that can run as is, or with little modification, even when a new protocol, such as IPv7, appears in the future.


IPv6-ready TCP client outline

This time, we will create a TCP echo client as a sample program.  Figure 1 shows a sample execution of a pre-compiled binary.  (see endnote 1 [0])  For information on how to compile it, please refer to Figure 4.  This is a simple program that takes a server host name as a command line argument, and when you type in a character string, the server will return (echo back) what you entered as is.  Figure 2 shows a rough program outline that supports both IPv6 and IPv4.

Figure 1: Sample execution of tcp-echo-client
C:\>tcp-echo-client.exe tcpecho.example.jp
connected
0123456789ABCD - character string entered from the keyboard
0123456789ABCD - character string received from the server
EFGHIJKLMNOPQR - character string entered from the keyboard
EFGHIJKLMNOPQR - character string received from the server
^Z - finished inputting
C:\>

Figure 2: IPv6-ready TCP echo client outline
Figure 2: IPv6-ready TCP echo client outline [0]

The important point in Figure 2 is that it creates an address information list if it receives multiple server IP addresses in block (1).  For example, if the connected server has the following two IP addresses:

  1. IPv4 address: 192.168.1.80
  2. IPv6 address: 2001:db8:1::80

the list that is created will look as follows:

address information ------------------------->  address information -------------------------> NULL
  +protocol type:            v6                  +protocol type:        IPv4
  +address:                  01:db8:1::80        +address:              192.168.1.80

In block (2), it attempts to connect to the server starting from the first item in the address information list.  If successful, it stops further attempts to connect and communicates with the first address it successfully connected to.  If it connects using the IPv6 address in the above-mentioned list, it will not attempt to connect using IPv4.  In contrast, if it fails to connect, it automatically switches to an IPv4 connection.  This function is called IPv6/IPv4 fallback and is the basic framework for many of the IPv6-ready programs that use TCP.


TCP echo client source code

Listing 2 shows the TCP echo client source code.  We will take a look at how the outline shown in Figure 2 is implemented in the source code shown in Listing 2.

•Lines 16 to 21.  The TCP echo client receives the server host name from the command line, and assigns the value to the nodename variable.

016:    /** Process command line arguments (read the server name from the command line) */
017:    if (argc != 2) {
018:        fprintf(stderr, "syntax: tcp-echo-client servername\n");
019:        exit(1);
020:    }
021:    nodename = argv[1];
•Lines 23-27. This is the initialization of WinSock. The version of WinSock used here is 2.2.

023:    /** Initialize WinSock */
024:    if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
025:        fprintf(stderr, "can not initilize WinSock\n");
026:        exit(1);
027:    }
•Lines 29 to 39. This is the process shown in block (2) of Figure 2.

029:    /** Perform name resolution and create ADDRINFO list */
030:    memset(&hints, 0, sizeof(hints));
031:    /* IPv6/IPv4 */
032:    hints.ai_family = AF_UNSPEC;
033:    /* Specify stream-type (TCP) socket */
034:    hints.ai_socktype = SOCK_STREAM;
035:    /* Perform name resolution and create ADDRINFO list.  ai0 is the first item of the list */
036:    if (e = getaddrinfo(nodename, servname, &hints, &ai0)) {
037:        fprintf(stderr, "%s\n", gai_strerror(e));
038:        exit(1);
039:    }

Now, I will explain the getaddrinfo( ) function, which is the most important part.  The getaddrinfo( ) function takes four arguments.  It passes in the host name as the first argument, and the service name as the second argument.  The service name here means the name given to a port number.  The typical ones are echo for port 7, http for port 80, smtp for port 25, etc.  The information on which port a service corresponds to is written in the /etc/services file on UNIX, and in the C:\WINDOWS\system32\drivers\etc\services file on Windows.  (see endnote 2 [0])

Listing 1: ADDRINFO structure members
struct addrinfo 
{
    int             ai_flags;
    int             ai_family;      /* AF_UNSPEC, AF_INET6, or AF_INET */
    int             ai_socktype;    /* SOCK_STREAM, SOCK_DGRAM */
    int             ai_protocol;    /* IPPROTO_IP, IPPROTO_TCP, IPPROTO_UDP */
    size_t          ai_addrlen;     /* Size of ai_addr */
    char            *ai_canonname;
    struct sockaddr *ai_addr;       /* Pointer to socket address*/
    struct addrinfo *ai_next;       /* Pointer to the next item in the ADDRINFO list*/
};

ai_family specifies the protocol (IPv6, IPv4, etc.) that will be used for communication.  When the protocol information is not required, specify AF_UNSEPC.  When a specific protocol is required for communication, use AF_INET6, AF_INET, etc. 

ai_socktype specifies the transport layer protocol, such as SOCK_STREAM, SOCK_DGRAM, etc.  ai_protocol is not specified in lines 29 to 39, however, the getaddrinfo( ) function automatically chooses an appropriate protocol based on the values of ai_family and ai_socktype.

For the TCP echo client that we are creating this time, we will use the protocol and port name (number) shown in the table 1.

Table 1: TCP echo client operations
Server name nodename (server name that is entered on the command line)
Service name servname (already initialized so that the servname variable refers to the character string “echo”)
Protocol that will be used Do not specify explicitly. (if IPv6 cannot connect, use IPv4)
Transport protocol that will be used TCP

So I used the following lines in order to initialize the hints information and then called the getaddrinfo( ) function, as shown in line 36.

hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

When the host and service names are resolved successfully, the getaddrinfo( ) function creates a list of address information (ADDRINFO) items for all the IP addresses that were received from the host name within the function.  The value of ai0 in the fourth argument gets initialized so that it points to the top item of the list.  Here, we will assume that the TCP echo server that was given to the first argument in the getaddrinfo( ) function has the following two IP addresses.

  1. IPv4 address: 192.168.1.80
  2. IPv6 address: 2001:db8:1::80

The information structure of the created list is shown in Figure 3.

Figure 3: ADDRINFO list structure created by the getaddrinfo( ) function
Figure 3: ADDRINFO list structure created by the getaddrinfo( ) function
[0]

In this ADDRINFO structure, which is the first item, you can see ai_family, ai_socktype, and ai_protocol are initialized to AF_INET6, SOCK_STREAM, and IPPROTO_TCP respectively.  When these three variables are passed in to the socket( ) function, it will create a socket.

  As for the item at the top in the list, the function call
    socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol) ...... (S1)
  is the same as
    socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)

Next, ai_addr is initialized with an IPv6 socket address (SOCKADDR_IN6 type), and the size of the SOCKADDR_IN6 structure (sizeof(SOCKADDR_IN6)) is stored in ai_addrlen.  Now, the second item in the ADDRINFO list is IPv4(AF_INET).  If you take a look at ai_addr, you can see it has been initialized with an IPv4 socket address (SOCKADDR_IN type).  When you observe the inside of the SOCKADDR_IN6 type socket address shown in Figure 3, you can see an IP address and a port number are already set up in in6_addr and sin6_port.  Programmers don’t have to worry about creating socket addresses.  If you need to connect to the socket created in (S1), you can pass the ai_addr and ai_addrlen variables in to the connect( ) function as follows:

connect([socket created in (S1)], ai->au_addr, ai->ai_addrlen)

The process is the same when you process the second item of the ADDRINFO list (IPv4).

•Lines 41 to 61.  This is the process in block (3) shown in Figure 2.  It iterates through the items in the ADDRINFO structure, starting with ai0, using a for loop.  The variable ai refers to the current list item.  It creates a socket in line 46.  (see Differences between WinSock and Berkeley Sockets (1) [0] )

041:    /** Keep attempting to connect using each item in the ADDRINFO list */
042:    /** until it connects to the server successfully. */
043:    for (ai = ai0; ai; ai = ai->ai_next) {
044:        /* Attempt to create a socket.  If this fails, */
045:        /* attempt to connect using the next item in the ADDRINFO list. */
046:        s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
047:        if (s == INVALID_SOCKET)
048:            continue;
049:
050:        /* Attempt to connect to the server.  If this fails, */
051:        /* attempt to connect using the next item in the ADDRINFO list. */
052:        if (connect(s, ai->ai_addr, ai->ai_addrlen) == SOCKET_ERROR) {
053:            closesocket(s);
054:            s = INVALID_SOCKET;
055:            continue;
056:        }
057:
058:        /* Since connection was successful, quit attempts to connect here. */
059:        printf("connected\n");
060:        break;
061:    }

In lines 46 to 48, if, for some reason, it fails to create a socket, e.g. the OS doesn’t support IPv6, it attempts to connect using the following item in the ADDRINFO list (in other words, create an IPv4 socket).  Once the socket is successfully created, it attempts to connect to the server.  Since the contents of the socket address (ai_addr) have already been filled in by the getaddrinfo( ) function, it passes it in to the connect( ) function as is.  If it connects successfully using the connect( ) function in line 52, it exits the for loop using the break statement in line 60 and immediately moves onto the I/O process of sending and receiving the character string to and from the server.  (see Differences between WinSock and Berkeley Sockets (2) [0] )

If, for some reason, it fails to connect with the top item (IPv6), it attempts to connect with the next item (IPv4) of the ADDRINFO list by using the continue statement in line 55.  Before calling continue to reattempt the connection, close the socket that failed to connect and change the value of s back to INVALID_SOCKET.  (see Differences between WinSock and Berkeley Sockets (3) [0])

When we take a look at lines 41 to 61 again, there are no commands that depend on a specific protocol, such as IPv4, IPv6, etc.  For example, in the first argument of the socket( ) function, the protocol name is specified; however, the programmer does not hardcode the protocol name.  The getaddrinfo( ) function checks which protocols are available among IPv6 and IPv4, and creates ADDRINFO structures only for the protocols that are available.  It passes the value of the ai_family variable, set by the getaddrinfo( ) function, in to the first argument of the socket( ) function.  Hence, the programmer does not have to write commands that depend on a specific protocol.

•Lines 65 to 70.  When attempts to connect using all of the ADDRINFO list items (both IPv6 and IPv4) fail, it determines that it cannot connect to the server using any method and proceeds to close the program.  If the connect( ) function succeeds with either IPv6 or IPv4, this step will be skipped.

063:    /** When attempts to connect using all of the ADDRINFO list items fail, */
064:    /** the value of s is INVALID_SOCKET. */
065:    if (s == INVALID_SOCKET) {
066:        freeaddrinfo(ai0);
067:        WSACleanup();
068:        fprintf(stderr, "can not connect server(%s)\n", nodename);
069:        exit(1);
070:    }
•Lines 72 to 89. When processing reaches this block, it means that a connection between the client and the server has been established and that sending and receiving data is possible. The details of the IPv6 and IPv4 protocols are completely hidden by the socket, and there is no protocol-dependent processing shown in the source code. The outline of the processing in this block shows that it receives the character string until EOF(^Z) is input from standard input, sends it to the server using the send( ) function, receives the echo back from the server using the recv( ) function, and displays the results on the screen.

072:    /** Process I/O between the client and the server. */
073:    /** Send the character string to the server and receive a response from the server. */
074:    while (fgets(linebuf, sizeof(linebuf), stdin) != NULL) {
075:
076:        /* Send the character string received from the standard input to the server. */
077:        if (send(s, linebuf, strlen(linebuf), 0) == SOCKET_ERROR) {
078:            fprintf(stderr, "send error\n");
079:            exit(1);
080:        }
081:
082:        /* Display the character string returned by the server. */
083:        if (recv(s, linebuf, sizeof(linebuf), 0) == SOCKET_ERROR) {
084:            fprintf(stderr, "recv error\n");
085:            exit(1);
086:        }
087:        printf(linebuf);
088:
089:    }
•Lines 91 to 93. This frees up the ADDRINFO list, which is not needed any longer. The memory allocated for the ADDRINFO list, which was created in line 36, was something that the getaddrinfo( ) function created internally and wasn’t something that the programmer secured beforehand. So, once it becomes unnecessary, the memory needs to be freed using the freeaddrinfo( ) function. After that, it cleans up WinSock using the WSACleanup( ) function and shuts down the program.

091:    freeaddrinfo(ai0);
092:    WSACleanup();
093:}

Conclusions on the TCP echo client

The important points for IPv6 support when creating a TCP echo client is the name resolution by the getaddrinfo( ) function and the creation of the ADDRINFO structure list.  The getaddrinfo( ) function loads the protocol-dependent information and the information needed to connect to the server into the ADDRINFO structure.  So, the programmer does not have to create this information.  When the system does all the work to create the protocol-dependent information, it is possible to create a protocol-independent program.  Also, by processing the ADDRINFO list sequentially until it connects, a program that automatically falls back to IPv4 if the IPv6 attempt fails can be implemented.  Next time, we will create an IPv6-ready echo server.

Listing 2: tcp-echo-client.c [0]
#include ‹winsock2.h›
#include ‹ws2tcpip.h›
#include ‹stdio.h›

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    char *nodename;
    char *servname = "echo";
    ADDRINFO hints;
    LPADDRINFO ai, ai0;
    int e;
    SOCKET s;
    char linebuf[BUFSIZ];

    /** Process command line arguments (read the server name from the command line) */
    if (argc != 2) {
	fprintf(stderr, "syntax: tcp-echo-client servername¥n");
	exit(1);
    }
    nodename = argv[1];

    /** Initialize WinSock */
    if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
	fprintf(stderr, "can not initilize WinSock¥n");
	exit(1);
    }

    /** Perform name resolution and create ADDRINFO list */
    memset(&hints, 0, sizeof(hints));
    /* IPv6/IPv4 */
    hints.ai_family = AF_UNSPEC;
    /* Specify stream-type (TCP) socket */
    hints.ai_socktype = SOCK_STREAM;
    /* Perform name resolution and create ADDRINFO list.  ai0 is the first item of the list */
    if (e = getaddrinfo(nodename, servname, &hints, &ai0)) {
	fprintf(stderr, "%s¥n", gai_strerror(e));
	exit(1);
    }

    /** Keep attempting to connect using each item in the ADDRINFO list */
    /** until it connects to the server successfully. */
    for (ai = ai0; ai; ai = ai->ai_next) {
	/* Attempt to create a socket.  If this fails, */
	/* attempt to connect using the next item in the ADDRINFO list. */
	s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
	if (s == INVALID_SOCKET)
	    continue;

	/* Attempt to connect to the server.  If this fails, */
	/* attempt to connect using the next item in the ADDRINFO list. */
	if (connect(s, ai->ai_addr, ai->ai_addrlen) == SOCKET_ERROR) {
	    closesocket(s);
	    s = INVALID_SOCKET;
	    continue;
	}

	/* Since connection was successful, quit attempts to connect here. */
	printf("connected¥n");
	break;
    }

    /** When attempts to connect using all of the ADDRINFO list items fail, */
    /** the value of s is INVALID_SOCKET. */
    if (s == INVALID_SOCKET) {
	freeaddrinfo(ai0);
	WSACleanup();
	fprintf(stderr, "can not connect server(%s)¥n", nodename);
	exit(1);
    }

    /** Process I/O between the client and the server. */
    /** Send the character string to the server and receive a response from the server. */
    while (fgets(linebuf, sizeof(linebuf), stdin) != NULL) {

	/* Send the character string received from the standard input to the server. */
	if (send(s, linebuf, strlen(linebuf), 0) == SOCKET_ERROR) {
	    fprintf(stderr, "send error¥n");
	    exit(1);
	}

	/* Display the character string returned by the server. */
	if (recv(s, linebuf, sizeof(linebuf), 0) == SOCKET_ERROR) {
	    fprintf(stderr, "recv error¥n");
	    exit(1);
	}
	printf(linebuf);

    }

    freeaddrinfo(ai0);
    WSACleanup();
}
Figure 4: How to compile tcp-echo-client.c
C:\>cl tcp-echo-client.c ws2_32.lib
Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 13.10.3077 for 80x86
Copyright (C) Microsoft Corporation 1984-2002. All rights reserved.

tcp-echo-client.c
Microsoft (R) Incremental Linker Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:tcp-echo-client.exe
tcp-echo-client.obj
ws2_32.lib
C:\>

Differences between WinSock and Berkeley Sockets (1)

In the WinSock API, SOCKET type is used for socket.  In Berkeley Sockets on UNIX, it defined as an int, and the actual SOCKET type is just an integer.  However, on UNIX, you can use the socket and the file descriptor for file I/O interchangeably.  However, in WinSock, the file descriptor for file I/O is different from the socket.  Some Win32 APIs process them interchangeably, but it cannot send and receive data using the read( )/write( ) functions like UNIX does.  Because of this, you should not use int variables for the SOCKET type.

Also, with regards to checking the success or failure of the socket( ) function, negative value results are often used on UNIX, e.g. if (s < 0) {... }.  In POSIX, -1 will be returned when a failure occurs.  In WinSock, when the socket( ) function fails, it always returns INVALID_SOCKET.


Differences between WinSock and Berkeley Sockets (2)

In the WinSock API, the connect( ) function returns SOCKET_ERROR when a failure occurs.  Functions such as listen( ), bind( ), send( ), recv( ), etc., also return SOCKET_ERROR.  In Berkeley Sockets, -1 is returned when a failure occurs.  So, please watch out for this difference from WinSock.


Differences between WinSock and Berkeley Sockets (3)

In the WinSock API, you use the closesocket( ) function to close the socket.  In Berkeley Sockets on UNIX, you use the close( ) function, but you cannot use it in WinSock.

Endnote:

1.  Next time, we will also create a TCP echo server that runs on Windows.  However, many OSs have TCP echo servers, so you can use them to test the client.  For example, when using FreeBSD, you can enable the TCP echo server for both IPv6 and IPv4 if you uncomment the following two lines in the inetd.conf configuration:

echo   stream  tcp     nowait  root    internal
echo   stream  tcp6    nowait  root    internal

2.  For IPv4 programming, the port numbers can be resolved from the port names using the getservbyname( ) function.

Furthermore, an ADDRINFO type hints variable can be passed in to the third argument.  An ADDRINFO structure is used in order to receive the results of name resolution from the getaddrinfo( ) function.  However, it also uses it as hints that controls the getaddrinfo( ) function.  The structure for the ADDRINFO type is shown in Listing 1.

 

この記事のトラックバックURL

http://www.ipv6style.jp/trackback/629

Source URL:
http://www.ipv6style.jp/en/apps/20051114/index.shtml