Experiment 4: Introduction to Socket Programming

Objectives

  1. Become familiar with the C socket library.
  2. Write a simple program to exchange packets with a server using the UDP protocol.
  3. Observe the traffic generated by the written program.

Background

User Datagram Protocol (UDP)

The UDP is the simplest transport protocol. It provides connectionless unacknowledged best-effort service (same as the IP) with the addition of support for ports and (optionally) data error checking. UDP adds an 8-octet header to accomplish this. An IP packet containing a UDP protocol data unit has the following structure:

+---------------------+----------------+-------------------------------+
| IP Header (20)      | UDP Header (8) |    Data (variable)            |
+---------------------+----------------+-------------------------------+

In this experiment we will write, compile and run a program to send and receive individual UDP packets.

C Socket Library

(The following adapted from <http://www.linuxhowtos.org/C_C++/socket.htm>.

The POSIX C socket library uses the client-server model. One process (the client) requests a connection to the other process (the server). When the server answers the request the two processes can start exchanging information. In order to achieve this, we have to create one socket at the server that is listening for connection requests and one socket at the client to request the connection. The code to create these sockets is thus a little different in the client and the server. For example the client needs to know about the existence and the address of the server, but the server does not need to know the address of (or even the existence of) the client prior to the connection being established.

The steps involved in establishing a socket on the client side are as follows:

  1. Select active mode, server address, port number and protocol (UDP in this case) using the getaddrinfo() call.
  2. Create a socket with the socket() system call
  3. Specify the server address using the connect() system call. Since UDP is connectionless, this call will not really establish a connection, it just sets the default destination for a socket.
  4. Send (send()) or receive (recv()) data from the socket. When using UDP, data must fit in a single packet.
  5. Close the socket when done.

The steps involved in establishing a socket on the server side are as follows:

  1. Select passive mode, listening port number and protocol (UDP in this case) using the getaddrinfo() call.
  2. Create a socket with the socket() system call
  3. Bind the socket to an address using the bind() system call.
  4. Send (sendto()) or receive (recvfrom()) data from the socket. When using UDP, data must fit in a single packet.
  5. Close the socket when done.

References

Procedure

  1. Launch putty ssh client. Log-in to the linux server account assigned by your lab instructor. Open two terminals. In one of the terminals run the experiment4 script to set up the source code needed for the experiment:

    $ experiment4
    Cleaning up and setting up source files ... done.
    

    You should now have two source code files in your home directory: client.c and server.c.

  2. Use the nano editor (other text editors also available) to view and edit the source files:

    $ nano server.c
    

    Change the port number in client and server to some random number higher than 1024 (must be different from the port numbers used by other groups). If you attempt to compile and run the server “as is” it will fail as administrator privileges are required to bind ports less than 1024. Important: do not use port 3000. If several groups use the same port number you’ll have problems running the experiment. Use a port number unique for your group.

    After changes are made compile the server and client code. Run the server first in one terminal and then the client in the other terminal. To compile the source code we invoke the gcc compiler. The -Wall option tells the compiler to enable all warnings and the -o server specifies the name of the executable file:

    $ gcc -Wall server.c -o server
    $ gcc -Wall client.c -o client
    $ ls
    client  client.c  server  server.c
    $ ./server
    

    You can interrupt the server program execution with [CTRL]-C if necessary. The client program, if successful, should print something like this:

    $ ./client
    Packet sent
    Received 10 bytes: Hi there!
    

    Note that the server is only listening on the loopback interface (127.0.0.1). So the client can only connect if it is running on the same host as the server.

  3. Now modify the server code to listen on all network interfaces instead of only the loopback interface (127.0.0.1). In order to do that, change the first argument of getaddrinfo() as follows:

    s = getaddrinfo(NULL, "<your port>", &hints, &result);
    

    A NULL pointer instead of a specific address indicates that all available network interfaces should be used. Recompile and run the server program:

    $ gcc -Wall server.c -o server
    $ ./server
    
  4. Open a command window (cmd.exe) in your Windows workstation. The current directory should be set to c:\users\engineer. Run experiment1 in that window as (as you did before for Experiment 1). That script sets up the working directory and the environment variables to use the compiler. Do not close this window: you need it to compile and run the program. Type notepad++ client.c to simultaneously open the notepad++ editor and create the source code file (answer yes to create new file). Paste the code from the Sample Server and Client Source Code into the editor and save into the work directory. Also set language to C in Notepad++ editor.

    Modify the client program on your Windows workstation to to connect to the server program you have just started on the Linux computer. Windows uses the Winsock interface which is very similar to the POSIX interface described here. The main difference is that the include files are different and that the library must be initialized as shown below:

    /* Needed to compile with MinGW gcc */
    #define _WIN32_WINNT 0x0501
    
    #include <string.h>
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <iphlpapi.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    #define BUF_SIZE 500
    
    /* This is needed to tell the MS VC compiler to link the Winsock
    library */
    #pragma comment(lib, "Ws2_32.lib")
    
    int main() {
        struct addrinfo hints;
        struct addrinfo *result, *rp;
        int sfd, s;
        size_t len;
        int nread;  /* Note: no ssize_t in VC */
        char buf[BUF_SIZE];
        char *message;
        WSADATA wsaData;
        int iResult;
    
        /**** Initialize Winsock: needed for Windows */
        iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
        if (iResult != 0) {
          printf("WSAStartup failed: %d\n", iResult);
          return 1;
        }
    

    The top portion of the code must be changed as shown above and the remainder of the server code can be re-used provided that you write the correct destination address in the ``getaddrinfo()`` call. If you close the socket before exiting the program you should use closesocket().

    Compile using the free MinGW compiler and test if the communication with the server is successful. The command line is similar to the one we used in the server, but here we have to manually tell the compiler to link with the Winsock library (ws2_32):

    D:\socket>gcc -Wall client1.c -lws2_32 -o client.exe
    client.c:13:0: warning: ignoring #pragma comment  [-Wunknown-pragmas]
     #pragma comment(lib, "Ws2_32.lib")
     ^
    
    D:\socket>client
    Packet sent
    Received 10 bytes: Hi there!
    
    D:\socket>
    

    Note: the #pragma directive in the source code above is only required if you use the Microsoft .NET environment or the VC compiler from the command line. That line generates a warning if the MinGW compiler is used (as shown above) and can be safely eliminated.

  5. Capture the traffic generated by your client/server using Wireshark. Find out how many packets are exchanged between client and server and their contents.

Questions to be answered at the end of the experiment

  1. What modifications where required to the server/client programs?
  2. How many packets are exchanged in total?
  3. Is there any packet exchange when the connect() function is called in this client program?
  4. What port number is used by the client?
  5. Describe the frame/packet/segment structure up to the UDP level: how many headers, how many bytes in each and how many bytes allocated with data.
  6. How long is the Ethernet frame payload? Should any padding be used? Can that be seen in the Wireshark capture?

Sample Server and Client Source Code

The code below is very similar to the code used in this experiment and illustrates the steps to set up sockets using the UDP protocol.

Simple client program:

/* Program based on getaddrinfo(3) manual page example */

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 1500

int main()
{
    struct addrinfo hints;
    struct addrinfo *result, *rp;
    int sfd, s;
    size_t len;
    ssize_t nread;
    char buf[BUF_SIZE];
    char *message;

    /* Obtain address(es) matching host/port */

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
    hints.ai_socktype = SOCK_DGRAM; /* Datagram socket. UDP is
                                   datagram: information is sent
                                   one packet at a time. Other
                                   protocols such as TCP would use
                                   SOCK_STREAM */
    hints.ai_protocol = IPPROTO_UDP;   /* UDP protocol */

    /* Get an structure with the addresses that can be used to listen
       for packets. The first argument is the host address and the
       second is the port number. */
    s = getaddrinfo("localhost", "3000", &hints, &result);
    if (s != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
        exit(EXIT_FAILURE);
    }

    /* getaddrinfo() returns a list of address structures.
       Try each address until we successfully connect(2).
       If socket(2) (or connect(2)) fails, we (close the socket
       and) try the next address. */
    for (rp = result; rp != NULL; rp = rp->ai_next) {
        sfd = socket(rp->ai_family, rp->ai_socktype,
                     rp->ai_protocol);
        if (sfd == -1)
            continue;

        /* The default destination for send() and recv() is
           specified with connect() */
        if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
            break;                  /* Success */
    }

    if (rp == NULL) {               /* No address succeeded */
        fprintf(stderr, "Could not connect\n");
        exit(EXIT_FAILURE);
    }

    freeaddrinfo(result);           /* No longer needed */

    /* Send datagram to server read any response from server */
    message = "Hi!";
    len = strlen(message) + 1; /* +1 for terminating null byte */

    if (send(sfd, message, len, 0) != len)
      fprintf(stderr, "Error sending message\n");
    else
      printf("Packet sent\n");

    nread = recv(sfd, buf, BUF_SIZE, 0);
    if (nread > 0)
        printf("Received %ld bytes: %s\n", (long) nread, buf);

    shutdown(sfd, SHUT_RDWR);
    /* closesocket(sfd); in windows */

    exit(EXIT_SUCCESS);
}

Simple server program:

/* Program based on getaddrinfo(3) manual page example */

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>

#define BUF_SIZE 1500

int main()
{
    struct addrinfo hints;
    struct addrinfo *result, *rp;
    int sfd, s;
    struct sockaddr_storage peer_addr;
    socklen_t peer_addr_len;
    ssize_t nread;
    char buf[BUF_SIZE];
    char host[NI_MAXHOST], service[NI_MAXSERV];

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
    hints.ai_socktype = SOCK_DGRAM; /* Datagram socket. UDP is
                                   datagram: information is sent
                                   one packet at a time. Other
                                   protocols such as TCP would use
                                   SOCK_STREAM */
    hints.ai_flags = AI_PASSIVE;    /* We'll be listening */
    hints.ai_protocol = IPPROTO_UDP;   /* UDP protocol */
    hints.ai_canonname = NULL;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;

    /* Get an structure with the addresses that can be used to listen
       for packets. The first argument is the host address and the
       second is the port number. */
    s = getaddrinfo("localhost", "3000", &hints, &result);
    if (s != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
        exit(EXIT_FAILURE);
    }

    /* getaddrinfo() returns a list of address structures.  Try each
       address until we successfully bind(2).  If socket(2) (or
       bind(2)) fails we try the next address. */
    for (rp = result; rp != NULL; rp = rp->ai_next) {
        sfd = socket(rp->ai_family, rp->ai_socktype,
                rp->ai_protocol);
        if (sfd == -1)
            continue;

        if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
            break;                  /* Success */
    }

    if (rp == NULL) {               /* No address succeeded */
        fprintf(stderr, "Could not bind\n");
        exit(EXIT_FAILURE);
    }

    freeaddrinfo(result);           /* No longer needed */

    /* Read datagrams and echo them back to sender. Here we can not
       use send() or recv() because we do not know who is
       connecting. */
    peer_addr_len = sizeof(struct sockaddr_storage);
    nread = recvfrom(sfd, buf, BUF_SIZE, 0,
                 (struct sockaddr *) &peer_addr, &peer_addr_len);
    if (nread == -1)
       exit(EXIT_FAILURE); /* give up if failed request */

    /* This is the inverse of get addrinfo: give the peer address
       structure returns the address in a more readable format */
    s = getnameinfo((struct sockaddr *) &peer_addr,
                peer_addr_len, host, NI_MAXHOST,
                service, NI_MAXSERV, NI_NUMERICSERV);
    if (s == 0)
      printf("Received %ld bytes from %s:%s \t Data: %s\n",
         (long) nread, host, service, buf);
    else
      fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));

    if (sendto(sfd, buf, nread, 0,
           (struct sockaddr *) &peer_addr,
           peer_addr_len) != nread)
      fprintf(stderr, "Error sending response\n");
    else
      printf("Packet re-sent\n");

    /* Close socket */
    shutdown(sfd, SHUT_RDWR);

    exit(EXIT_SUCCESS);
}