This month, we are starting a series on network programming. This area of programming is enormous, not only because of the sheer amount of information that is needed to successfully develop network applications, but also because of the number of applications currently being developed with networking in mind. With network speeds increasing, more and more applications have a “network” version of some sort. For example, Quicken can automatically update your account information from some banks, computer games can be played with other people on the Internet, and so on.
When diving into a topic as huge as network programming, it is easy to get lost in the details of every specific parameter for each and every function call. Those of you who have been reading Compile Time for a few months now have probably noticed that we thoroughly describe each part of the simple examples given for the topic at hand. If we were to do this for network programming, we wouldn’t be able to discuss more than five lines of code in any given article. Instead, we will take a slightly different approach, waving our hands at some things (to be described in later articles) in order to demonstrate concrete examples as quickly as possible.
Client-Server Basics
This month we’ll look at two-way network communication, which permits two processes to send data to each other interactively. We have seen this idea before in the context of two processes running on a single machine, in which case we could use anonymous pipes to transfer the information. (For more information about anonymous pipe files, see the January 2001 Compile Time,http://www.linux-mag.com/2001-01/compile_01.html.) Using anonymous pipes, one process can read from a pipe file while the other one writes to it, allowing the processes to communicate one way. With two pipes, the two processes can communicate information back and forth. (Only one process may write to a given pipe.)
In network programming, the processes can (and usually do) reside on different machines, each of which is called a node. The method of communication from one node to the other is called a socket. Once the connection between nodes is established, one node can write to its socket, and the other node can read that information from its socket (just like the two ends of the pipe). Unlike pipes, each node can read and write to its socket, removing the necessity of having two channels for two-way communication.
The type of network programming we will discuss this month is based on TCP/IP (Transmission Control Protocol/ Internet Protocol). A communication channel based upon TCP traveling over IP is often referred to as a “stateful” connection between two processes. “Stateful” means that TCP handles certain functions, such as making sure that all the packets that were sent were actually received, that no packets were corrupted during transmission, and that they are reassembled on the receiving end in the same order they were sent.
There are other protocols that can travel over IP, such as UDP (User Datagram Protocol), that do not maintain a “stateful” connection (or channel). UDP simply sends and receives packets, with no error checking, no acknowledgement that anything was received, and no sense of the order in which the packets should be reassembled. For a list of all the other protocols available to you, check out the /etc/protocols file. The constants defining the various protocol (and address) families are in <bits/socket.h>.
The node that initiates the TCP/IP connection is called the client. The other node, which waits for a connection to be made, is called the server.
Establishing the TCP/IP Connection
Let’s now look at how we can use sockets to establish a connection between two nodes. The job of the server is to open its end of the socket, pick a port to communicate on, and then wait for a client to connect. The job of the client is to open its end of the socket and then to connect to the server through the port the server chose. A list of all standard TCP and UDP port numbers, along with the associated service names, is also in the /etc/services file.
The function to create a socket is:
int socket (int domain, int type, int protocol);
This function is defined in <sys/socket.h>. We’ll discuss the meaning of the different parameters in a future article, but they are basically used to indicate what type of communication will travel over this socket. For our purposes, we will use the constant PF_INET for domain, SOCK_ STREAM for type, and 0 for protocol. These constants are defined in <bits/socket.h>. The return value from socket() is -1 on error and a socket descriptor on success.
Once the client has successfully created a socket, it must connect to the server before communication can take place. This is done with a call to the conveniently named function connect(). Its prototype (also listed in <sys/socket.h>) is:
intconnect (int sockfd, const struct sockaddr*
serv_addr, socklen_t addrlen);
It takes in three arguments. The first argument, sockfd, is the socket descriptor as returned from a call to the socket() function. The second and third arguments deal with the server the client wishes to connect to. The serv_addr parameter is a pointer to a struct sockaddr, the data structure that keeps the necessary information about the server. This struct is defined in <bits/socket.h> and is basically equivalent to struct sockaddr_in, which is defined in <linux/in.h> (shown in Figure One.
Figure One: The sockaddr_in and in_addr Structs Defined in <linux/in.h>
1 struct sockaddr_in {
2 sa_family_t sin_family; /* Address family */
3 unsigned short int sin_port; /* Port number */
4 struct in_addr sin_addr; /* Internet address */
5
6 /* Pad to size of struct sockaddr. */
7 unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
8 sizeof(unsigned short int) - sizeof(struct in_addr)];
9 };
10
11 struct in_addr {
12 __u32 s_addr;
13 };
|
Its first parameter, sin_family, is a constant. The second parameter, sin_port, gets the port number in a specified byte order. The third parameter, sin_addr, is itself a structure of type struct in_addr.
That structin_addr leads us to ask, “How do we get the server’s address into the structsockaddr parameter to pass to connect()?” There are two specialized functions, inet_ aton() and htons(), which do the hauling and lifting to fill in the sockaddr_instruct. Theinet_aton() function converts a dotted IP address (like “209.81.9.15″) and stores it into the sin_addr structure passed to it. The htons() function takes the port number and converts it from the local host’s byte order into the network’s byte order.
The Client and Server
Once the client has successfully created a socket and connected to a server, it may begin to communicate through the socket using normal read and write calls.
Figure Two: A Toy Client Program for the Server Program in Figure Three
1 #include <sys/socket.h>
2 #include <resolv.h>
3 #include <sys/types.h>
4
5 int main ()
6 {
7 int sockfd;
8 struct sockaddr_in addr;
9 char buf[10];
10 /* host’s IP address is “1.1.1.1″; port = 9999 */
11 /* Create the socket. */
12 sockfd = socket (PF_INET, SOCK_STREAM, 0);
13
14 /* Clear out the address struct. */
15 bzero (&addr, sizeof (addr));
16
17 /* Initialize the values of the address struct including
18 port number and IP address. */
19 addr.sin_family = AF_INET;
20 addr.sin_port = htons (9999);
21 inet_aton (”1.1.1.1″, &addr.sin_addr);
22
23 /* Connect to the server. */
24 connect (sockfd, &addr, sizeof (addr));
25
26 /* Write a message to the server. */
27 write (sockfd, “Hi server”, 9);
28
29 /* Wait for a few seconds, and then read from the server. */
30 sleep (3);
31 read (sockfd, buf, 9);
32
33 /* Terminate the string read from the server and print it out. */
34 buf[9] = ‘ |