Creating Your Own Server: The Socket API, Part 2

5
6998
Socket programming isn't really that tough

Socket programming isn't really that tough

Earlier, we created a simple server and client program using the socket API. This time, we’ll first start with a program, and then explain what’s going on. So start up your systems, and get ready to go deeper into socket programming.

As mentioned, let’s dive straight into the code.

The IPv6 version of the server

Here’s the IPv6 version of the server we created in the previous article. There are no big changes, except the appearance of “6” in the code. Let’s name it serverin6.c:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main()
{
	int sfd, cfd;
	char ch;
	socklen_t len;
	struct sockaddr_in6 saddr, caddr;
	sfd= socket(AF_INET6, SOCK_STREAM, 0);
	saddr.sin6_family=AF_INET6;
	saddr.sin6_addr=in6addr_any;
	saddr.sin6_port=htons(1205);
	bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
	listen(sfd, 5);
	while(1) {
		printf("Waiting...n");
		len=sizeof(cfd);
		cfd=accept(sfd, (struct sockaddr *)&caddr, &len);
		if(read(cfd, &ch, 1)<0) perror("read");
		ch++;
		if(write(cfd, &ch, 1)<0) perror("write");
		close(cfd);
	}
}

Now let’s review the differences. The first is at Line 6 (sockaddr_in6) and is easily understood; for IPv6 addresses, we need to store the address, port and address family in this address structure, as we have done in lines 13, 14 and 15. At Line 14, in6addr_any is a wildcard for all IPv6 addresses, like INADDR_ANY in IPv4. There’s also a change in accept(), which can be easily understood. And here’s our server working with IPv6. You can set up your IPv6 using the following command as root:

ip -f inet6 addr add face:1f::ea54:a dev eth0

Here, -f specifies the family (inet6), and addr is for the address — we added face:1f::ea54:a (while writing IPv6 addresses, leading zeros can be dropped — the above address is actually face:001f::ea54:000a). You can give any address; and to randomise it, you can use your MAC address. The dev parameter specifies the device we are setting the address for — in this case, eth0. You can check the results using the ifconfig command.

Compile and run the server as follows:

cc serverin6.c -o serverin6
./serverin6

Before writing the client, you can see that it even works with our IPv4 client, as the IPv4 address is also pointing to the same machine. For every server, we can use telnet to test if it’s working as expected — for example:

telnet localhost 1205

Type in the character you want to send to the server and press Enter, and the server will reply with the next ASCII character, and close the connection. Remember, for IPv4, localhost is 127.0.0.1, and for IPv6, it is ::1.

Client (IPv6 version)

Now let’s write the IPv6 version client, and name it clientin6.c:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char **argv)
{
	int cfd;
	struct sockaddr_in6 addr;
	char ch;
	if(argc!=3) {
		printf("Usage: %s in6addr charactern", argv[0]);
		return -1;
	}
	if( ! inet_pton(AF_INET6, argv[1], &(addr.sin6_addr))) { /* returns 0 on error */
		printf("Invalid Addressn");
		return -1;
	}
	ch=argv[2][0];		/* Set the character to second argument */
	cfd=socket(AF_INET6, SOCK_STREAM, 0);
	addr.sin6_family=AF_INET6;
	addr.sin6_port=htons(1205);
	if(connect(cfd, (struct sockaddr *)&addr,
	sizeof(addr))<0) {
		perror("connect error");
                return -1;
	}
	if(write(cfd, &ch, 1)<0) perror("write");
	if(read(cfd, &ch, 1)<0) perror("read");
	printf("Server sent: %cn", ch);
	close(cfd);
	return 0;
}

Here, again, the changes are small and self-explanatory — just an added “6”. The client takes the IP address and characters as arguments, so to run it, just type the following:

cc clientin6.c -o clientin6
./clientin6  ::1  d

When d is sent, the server replies with e. The output will be the same as for the last examples, shown in the figures of the last article. To process the address, we have used the inet_pton() function, which we’ll look at in more detail in the next section.

Diving deeper

First, let’s look at the function we used in our client to convert the address from string to numeric. In binary, the IP address 192.168.1.23 will be 11000000 10101000 00000001 00010111 (a 32-bit string of 0’s and 1’s). It will be a 128-bit string for IPv6 (64-bit address). The human-readable addresses 192.168.1.23 or face:1f::ea54:a are the ‘presentation’ form (the p of the function), and n is for the numeric (binary) form. The function is prototyped in <arpa/inet.h> as:

int inet_pton (int family, const char *strptr, void *addrptr);

This function can convert both IPv4 and IPv6 addresses from string form to binary as needed by the machine. The first argument is, of course, the family: AF_INET or AF_INET6. The *strptr argument is the address in string form, and *addrptr will be the destination where the numeric address will be stored; this must be a structure (sin_addr for IPv4, and sin6_addr for IPv6). The function returns 1 on success and 0 on error. The next function is:

const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

This function does exactly the reverse of the previous case; it converts the numeric (addrptr) and stores in the presentation (strptr). The last argument is len, the size/length of the destination variable, to avoid overflow. For ease, netinet/in.h defines the size constants, as follows:

#define INET_ADDRSTRLEN	    16
#define INET6_ADDRSTRLEN	46

There are other functions available for the same purpose — inet_aton() and inet_ntoa() — but only for IPv4; therefore, we aren’t using them.

Now let’s look at the functions to convert bytes between the network protocol stack and host to get machine-independent code. Machine architecture is either big-endian or little-endian, i.e., how the machine stores data. The high-order byte at a higher address and low-order byte at a lower address is little-endian. Conversely, high-order byte at a lower address, and low-order byte at a higher address will be big-endian.

For example, if we have a 2-byte integer storing 0x1234 at address 0x0200, it will occupy two bytes: 0x0200 and 0x0201. If, at 0x0200 we have 0x12, and at 0x0201 we have 0x32, the machine is little-endian. If the values are stored the opposite way, the machine is big-endian. The book Unix Network Programming, by W Richard Stevens, gives a program to find out what machine you have. Let’s look at the functions now, defined in netinet/in.h:

uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);

These two functions return 16-bit and 32-bit values respectively; htons() stands for the host to network short, htonl() for long. They convert from host byte-ordering to network-stack byte-ordering. The functions ntohs() and ntohl() do the reverse (from network byte-ordering to host byte-ordering).

These functions are particularly useful when we ask the system to provide us the information of the host running a client or server. Now let’s try to extract some information from the client. Just add these lines to the server:

char address[INET6_ADDRSTRLEN]; /* at declaration section */
printf("Connected to client %s at port %dn", inet_ntop(AF_INET6, &caddr.sin6_addr,
buff, sizeof(buff)), ntohs(caddr.sin6_port)); /* after the call to accept() */

You can see the output after these code changes in Figure 1. The code itself is clear—we have used the functions we just understood, above.

Figure 1: Server running
Figure 1: Server running

A little exercise

Now put this code in the proper section of the server program, to make it do something more interesting than what we did earlier:

if(read(cfd, &ch, 1)<0) perror("read");
while( ch != EOF) {
        if((ch>='a' && ch<='z') || (ch>='A' && ch<='Z'))
        ch^=0x20;    /* EXORing 6th bit will result in change in case */
        if(write(cfd, &ch, 1)<0) perror("write");
        if(read(cfd, &ch, 1)<0) perror("read");
}

Yes, you’ve got it right: add it after the call to accept(). Now, if you’re as sleepy as I am right now, then just telnet to the server — or go on and write a client; great, that’s the spirit.

Well, before closing, let’s look at the output of a telnet session. Run telnet ::1 1205 in your shell, and start typing. See Figure 2 for a sample output.

Figure 2: Telnetting
Figure 2: Telnetting

On hitting Ctrl+D, the server will close the connection, and wait for a new one. And for those lazy fellas who didn’t want to write their own client’s :P, you just needed to put this into your client, instead of only write() and read() calls, for the result shown in Figure 3:

while(1) {
      ch=getchar();
      if(write(cfd, &ch, 1)<0) perror("write");
      if(read(cfd, &ch, 1)<0) perror("read");
      printf("%c", ch);
}

Figure 3: Client running
Figure 3: Client running

Compile and run your client.

To close, use Ctrl+D to send EOF to the server, and the server will close the connection to the client, instead of shutting the server down using Ctrl-C. Or you can also close the connection from the client; we’ll handle this problem later, using signal handlers. Now, I think I should try to get some rest. Good night, and FOSS ROCKS!

5 COMMENTS

  1. […] good beginning, right? In the next article we’ll rewrite both these programs for IPv6, and move further to UDP. And yes, FOSS rocks! Feature image credit: Emilian Robert Vicol. Reused […]

  2. After adding this code to serverin6.c, I get a compile error stating that buff does not exist. It looks like you forgot to declare it…

    char address[INET6_ADDRSTRLEN]; /* at declaration section */

    printf(“Connected to client %s at port %dn”, inet_ntop(AF_INET6, &caddr.sin6_addr,

    buff, sizeof(buff)), ntohs(caddr.sin6_port)); /* after the call to accept() */

  3. “Before writing the client, you can see that it even works with our IPv4 client, as the IPv4 address is also pointing to the same machine.” — I don’t think so… they’re on different ports…

LEAVE A REPLY

Please enter your comment!
Please enter your name here