| 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
|
||||||||||||||||
| 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:
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
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; 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.
Figure 3: ADDRINFO list structure created by the getaddrinfo( ) function 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] ) |
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:\> |
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.
|
[0]
[0]