Experiment 4: 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. Launch the remote desktop client (or optionally a putty ssh client). Log-in to the virtual network server (at5030-eng2453server.lakeheadu.ca). The account name is : group<n>, where <n> is the number assigned by the lab instructor. Only one student in the group should log-in. Run the experiment4 script to set up the source code needed for the experiment:

    $ experiment4
    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 gedit editor (if graphic environment available) or the nano editor for text-mode only to view and edit the server source file:

    $ 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 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: Socket Programming and TCP 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 preparation and questions

  1. Prepare a formal report summarizing this experiment in pdf format and submit it to the lab instructor. Report writing rules:

    • One report per group
    • All students are responsible for the contents of the report, but one student in the group must coordinate, write and submit the report for the experiment. Each student in a group must prepare at least one of the five reports in the term.
    • Clearly state in the report cover the name of all students in the group and indicate who prepared the report
  2. Include supporting screen captures and the source code of the client program developed by your group.

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

  4. What port number is used by the client?

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

  6. Are the initial sequence numbers equal to 0? If so, why?

  7. What are the absolute initial sequence numbers?

  8. Explain packet exchange before disconnection.

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

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

  11. Explain what happens when the server window is about to become full and how is this eventually resolved.

  12. 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;
}