Experiment 5: Socket Programming and TCP

Objectives

  1. Become familiar with the Transmission Control Protocol
  2. Observe the TCP connection handshake, flow control and disconnection exchange.
  3. Write a simple program to transfer files between computers.
  4. 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.

References

Procedure

  1. Launch putty ssh client. Log-in to the linux server account assigned by your lab instructor. Run the experiment5 script to set up the source code needed for the experiment:

    $ experiment5
    Cleaning up and setting up server source file ... done.
    

    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. Use the nano editor (other text editors also available) to view and edit the server source file:

    $ nano 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 and launch another instance of putty with the following 3 parameters: a “raw” connection type (instead of the usual ssh), the IP address and the port number where your server is running as shown below:

    Putty parameters to connect to TCP server program.

    If the connection is successful, a terminal window should open as shown below. Everything that is typed into this window will be sent using the established TCP connection to the server. As the server expects a file name first, type a file name followed by a null character (^@, where ^ means to hold the Control key while you press @). Any characters after that will go to the contents of the file. To finish sending data, start an empty new line and press ^d to close the connection.

    Putty raw terminal.

    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.

  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 to be created.

    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. Most of the initialization is similar to the client code for Experiment 4: Introduction to Socket Programming except that now 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 IP address and port. 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 (at least 1000 bytes) for sendbuf. Normally nchars will 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 use a loop:

    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).

    The following section of the server code puts the program to sleep while data is being transferred:

    // After receiving some data sleep a little to trigger some
    // flow control
    counter++;
    if (counter == 20)
      sleep(5);
    

    This code will make the server to sleep for 5 seconds after the receive buffer is read 20 times. While the server is sleeping, the client will continue sending data for a while until the receive window is full. Observe in Wireshark the packet exchange near the time when the window becomes full. To simplify finding this you may set the time reference to the first packet of the TCP conversation. Then search when the time jumps from 0 to 5 seconds.

Report

Prepare a report in pdf format with the answers to the following questions and supporting screen captures. Also include the source code of the client program developed by your group.

  1. Is there any packet exchange when the connect() function is called in this client program? Explain.
  2. Are the initial sequence numbers equal to 0? If so, why?
  3. What are the absolute initial sequence numbers?
  4. Explain packet exchange before disconnection.
  5. What was the 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.
  6. How many packets where exchanged? Hint: Use “Statistics->Summary” to show number of packets currently displayed.
  7. Explain what happens when the server window is about to become full and how is this eventually resolved.
  8. What happens if a client attempts to connect to the server while the server is busy receiving data from another client? What is the simplest change in the server code to allow that?

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(5);
      }
      fclose(outf);
      shutdown(connfd, SHUT_WR);
    }
  return 0;
}