Experiment 5: Socket Programming and TCP

Objectives

  1. Become familiar with the C socket library.

  2. Become familiar with the Transmission Control Protocol

  3. Observe the TCP connection handshake, flow control and disconnection exchange.

  4. Write a simple program to transfer files between computers.

  5. Gain more experience writing, compiling and debugging C programs.

Background

The server program used in this experiment is intended to receive and save files sent by clients. It accomplishes this by listening on one port and waiting for TCP connection requests. When a request is made the server will read data until the connection is closed. The file is transmitted as follows: first the file name ended by the NULL character. All bytes after that are assumed to be part of the file. The server creates a file with the transmitted file name and writes any data received on it.

The server can be configured to momentarily stop receiving data during the transmission to demonstrate the flow control capabilities of TCP. The server source code is included at the end of this document.

Please note that this server could be a serious security risk in a production computer. It is intended to be used just for coding practice.

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 (TCP 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. This call will initiate the handshake to establish a connection, and sets the default destination for a socket.

  4. Send (send()) or receive (recv()) data from the socket.

  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 (TCP 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.

  5. Close the socket when done.

References

Procedure

  1. Log-in to the virtual network server (eele2453.lakeheadu.ca). Use your university login account name. The password will be given by the lab instructor. In principle, only one student in the group needs to log-in. You may use the secure shell program (ssh) to login as follows: open a cmd.exe terminal, and run ssh as shown below, replacing <user id> with your university account name:

    C:\Users\engineer> ssh <user id>@eele2453.lakeheadu.ca
    

    Run the experiment5 script to set up the source code needed for the experiment:

    $ experiment5
    

    You should now have the server source code in your home directory: server-tcp.c. The source code is also included in the Appendix Section of this document.

  2. Source code editor choices: geany (IDE) or gedit (text editor), if graphic environment available. For text-only sessions, you may use nano (easiest), emacs or vi. View and edit the server source file:

    $ cd exp5
    $ gedit server-tcp.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. We’ll assume in the examples here that the port number was changed to 12345, but you must use a different number.

    Compile and run the modified server code:

    $ gcc -Wall server-tcp.c -o server
    $ ./server
    
  3. Test the server from your windows workstation. To do that, make sure that your server is running. Open a cmd.exe terminal on the Windows station and use the telnet program to connect to the server as follows:

    C:\Users\engineer> telnet eele2453.lakeheadu.ca 12345
    

    (syntax is telnet <host> <port number>). Assuming the connection is successful, you should see an incoming connection message on the server side. On the telnet window, press CTRL+] (hold the control key, and press the “]” symbol) to enter the command mode. Send some data as shown below:

    Microsoft Telnet> send myfile1.txt
    Microsoft Telnet> send This is a some content for the file
    Microsoft Telnet> close
    Microsoft Telnet> quit
    

    After the connection is closed check that the server side has received the data and created the transmitted file as expected. For the example above, the file name is myfile1.txt (note: there is a chance that the file name has extra characters at the end because we did not terminate the filename string with a null char).

  4. In the cmd terminal, repeat the steps performed in Experiment 1 to set up the compiler and a working directory.

    The objective is to write a client program to send the following file to the server: pg11.txt (download link into working directory). Other files of similar size could also be used. 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 Appendix into the editor and save into the work directory. Also set language to C in Notepad++ editor. The hints should be set as follows, since We’ll use the TCP protocol:

    hints.ai_family = AF_INET; // AF_INET, AF_INET6
    hints.ai_socktype = SOCK_STREAM; // SOCK_DGRAM
    hints.ai_protocol = IPPROTO_TCP; // IPPROTO_ICMP, IPPROTO_UDP, etc.
    

    At first you may try running the client code with minimal modifications to see if it works. At the very least you’ll have to put the correct server host name (or IP address) and port. Compile using the free MinGW compiler. 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):

    C:\Users\engineer> gcc -Wall client.c -lws2_32 -o client.exe
    client.c:13:0: warning: ignoring #pragma comment  [-Wunknown-pragmas]
     #pragma comment(lib, "Ws2_32.lib")
    
    C:\Users\engineer> client.exe
    

    If the client runs successfully, the transferred file should be created in the directory where the server is running.

    To send a complete file, you can use the following functions: fopen(), fread() and fclose(). For example to read and send block of 1000 bytes:

    FILE *inpf = fopen(filename, "rb");
    size_t nchars;
    
    // <Number of items read> = fread(<buffer pointer>,
    //             <size of item>, <number of items>, FILE *stream);
    nchars = fread(sendbuf, 1, 1000, inpf);
    
    // Send data to server
    send(sock, sendbuf, nchars, 0);
    
    fclose(inpf);
    

    Before executing this code you have to declare and allocate memory (in this example 1000 bytes) for sendbuf. In the code, the returned value for nchars would normally be 1000, except when the file has less than 1000 characters to be read. If you execute fread() many times, eventually nchars will be zero. Thus to read arbitrary file sizes you could modify part of the code shown above and use a loop as shown below:

    do {
      nchars = fread(sendbuf, 1, sendbuflen, inpf);
      send(sock, sendbuf, nchars, 0);
    } while (nchars > 0);
    

    You can use a different block size to attempt fitting the data block exactly into one TCP segment. Compile the client code and debug if necessary until it works. Check that the received file in the server is correct by opening it in a text editor.

  5. Capture the traffic generated by your client/server using Wireshark. Find out how many packets are exchanged between client and server. To isolate the TCP conversation, select the first packet and in the “Analyze” menu select “Follow TCP Stream”.

    Identify the connection handshake (SYN flag) and the initial sequence numbers for each direction. Also observe the disconnection handshake (FIN flag).

Questions to be answered at the end of the Experiment

  1. Is there any packet exchange when the connect() function is called in this client program? Explain.

  2. What port number is used by the client?

  3. Show in a diagram the frame/packet/segment structure corresponding to the first data segment sent from client to server. Show how many bytes in each header and how many bytes allocated with data.

  4. Are the initial sequence numbers really equal to 0?

  5. What are the absolute initial sequence numbers?

  6. Explain packet exchange before disconnection.

  7. What was the exact size of the transmitted file? Explain the relation between the file size and the difference between initial and final SEQ number from client to server.

  8. How many packets where exchanged? Hint: Use “Statistics->Summary” to show number of packets currently displayed.

  9. What is the simplest change in the server code to allow serving more than one client?

Appendix

Client template code is shown below. Note that you are expected to transmit a real file, not the strings used in this example:

/* 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>

/* This is needed to tell the MS VC compiler to link the Winsock
library */
#pragma comment(lib, "Ws2_32.lib")

int main() {

  int iResult;
  struct addrinfo *result = NULL, *rp = NULL, hints;
  int sock = -1;
  WSADATA wsaData;
  char *filename, *contents;

  /**** Initialize Winsock: needed for Windows */
  iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
  if (iResult != 0) {
    printf("WSAStartup failed: %d\n", iResult);
    return 1;
  }

  memset(&hints, 0, sizeof(struct addrinfo));
  hints.ai_family = AF_INET; // AF_INET, AF_INET6
  hints.ai_socktype = SOCK_STREAM; // SOCK_DGRAM
  hints.ai_protocol = IPPROTO_TCP; // IPPROTO_ICMP, IPPROTO_UDP, etc.

  /* Resolve the server address and port */
  iResult = getaddrinfo("192.168.10.16", "22", &hints, &result);
  if (iResult != 0) {
    printf("getaddrinfo failed: %d\n", iResult);
    return 1;
  }

  // Attempt to connect to the first address returned by
  // the call to getaddrinfo (getaddrinfo returns a linked list)
  for (rp = result; rp != NULL; rp = rp->ai_next) {
        sock = socket(rp->ai_family, rp->ai_socktype,
                     rp->ai_protocol);
        if (sock == -1)
            continue;
        /* Connect with server */
        if (connect(sock, rp->ai_addr, rp->ai_addrlen) != -1)
            break;                  /* Success */
    }

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

  // Send file name terminated by NULL
  filename = "myfile.txt";
  send(sock, filename, strlen(filename) + 1, 0);

  // Send file contents
  contents = "This is the file content.\n\n";
  send(sock, contents, strlen(contents), 0);

  // shutdown the send half of the connection since no more data will be sent
  iResult = shutdown(sock, SD_SEND);
  if (iResult == SOCKET_ERROR) {
    printf("shutdown failed: %d\n", WSAGetLastError());
    closesocket(sock);
    WSACleanup();
    return 1;
  }

  // cleanup
  closesocket(sock);
  WSACleanup();

  return 0;
}

Server code (port number is not valid):

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

#define DEFAULT_BUFLEN 1200

int main(void)
{
  int iResult, p1;
  struct addrinfo *result = NULL,
    hints;
  int recvbuflen = DEFAULT_BUFLEN;
  char recvbuf[DEFAULT_BUFLEN];

  memset(&hints, 0, sizeof(struct addrinfo));
  hints.ai_family = AF_INET; // AF_INET, AF_INET6
  hints.ai_socktype = SOCK_STREAM; // SOCK_DGRAM
  hints.ai_protocol = IPPROTO_TCP; // IPPROTO_ICMP, IPPROTO_UDP, etc.
  hints.ai_flags = AI_PASSIVE; // needed to indicate that the address
                               // will be used with bind()

  /* Resolve the server address and port */
  iResult = getaddrinfo(NULL, "200", &hints, &result);
  if (iResult != 0) {
    printf("getaddrinfo failed: %d\n", iResult);
    return 1;
  }

  // Look what was returned
  int listenfd = 0;
  struct addrinfo *rp;
  for (rp = result; rp != NULL; rp = rp->ai_next)
    {
      listenfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
      if (listenfd == -1)
   continue;
      else
   printf("socket retrieve success\n");
      if (bind(listenfd, rp->ai_addr, rp->ai_addrlen))
   printf("Bind failed!\n");
      else
   break;
    }
  if (rp == NULL) {               /* No address succeeded */
    fprintf(stderr, "Could not connect\n");
    return -1;
  }

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

  if(listen(listenfd, 5) == -1){
    printf("Failed to listen\n");
    return -1;
  }

  int connfd = 0;
  struct sockaddr_in client_addr;
  unsigned int cal;
  cal = sizeof(client_addr);
  int i;
  // Receive file data and print to screen (for debugging purposes
  // only one client per run for now)
  for (i=0; i < 1; i++)
    {
      connfd = accept(listenfd, (struct sockaddr*)&client_addr , &cal);
      char host[NI_MAXHOST], service[NI_MAXSERV];
      int s = getnameinfo((struct sockaddr *) &client_addr,
                     cal, host, NI_MAXHOST,
                     service, NI_MAXSERV, NI_NUMERICSERV);
      if (s == 0)
        printf("\nIncoming connection from %s:%s\n", host, service);
      else
        fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));
      p1 = 0;
      // Receive data until client closes the connection
      // First string must be file name
      iResult = recv(connfd, recvbuf, recvbuflen-1, 0);
      if (iResult > 0)
        {
          printf("*** Bytes received: %d\n", iResult);
          // Search for first NULL
          p1 = strlen(recvbuf);
          if (p1 < recvbuflen) {
            printf("File Name (%d chars) = %s\n", p1, recvbuf);
          }
          else {
            recvbuf[recvbuflen - 1] = 0;
            printf("Protocol error = %s\n", recvbuf);
            shutdown(connfd, SHUT_RDWR);
            continue;
          }
        }
      else if (iResult == 0)
        printf("Connection closed\n");
      else
        printf("recv failed\n");
      // The remaining bytes in recvbuf must be part of the file
      FILE *outf = fopen(recvbuf, "wb");
      fwrite(recvbuf + p1 + 1, 1, iResult - p1 -1, outf);
      // Keep receiving until connection closed
      int counter = 0;
      while (iResult > 0) {
        iResult = recv(connfd, recvbuf, recvbuflen-1, 0);
        if (iResult > 0)
          {
            printf("*** Bytes received: %d\n", iResult);
            fwrite(recvbuf, 1, iResult, outf);
          }
        else if (iResult == 0)
          printf("Connection closed\n");
        else
          printf("recv failed\n");

        // After receiving some data sleep a little to trigger some
        // flow control
        counter++;
        if (counter == 20)
          sleep(1);
      }
      fclose(outf);
      shutdown(connfd, SHUT_WR);
    }
  return 0;
}