CS360 Lecture notes -- Sockets

  • James S. Plank
  • Directory: /home/plank/cs360/notes/Sockets
  • Last updated: Wed Apr 13 11:32:21 EDT 2016
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Sockets/lecture.html
    This lecture focuses on programming with sockets, as implemented by the program sockettome.c (name provided by Harrison "Jay" Cross, a student in my CS360 class in 2011).

    Sockets are the most general way of performing interprocess communication (usually abbreviated IPC) in Unix. We have seen other ways of performing IPC. For example, processes can communicate through files and through pipes. However, both of those are limited. For example, the only way that two processes can communicate through a pipe is if one process has been forked by the other. Often we would like to have two processes talk with each other when they are otherwise unrelated. For example, they can be created by different shells, different users, or on different machines. Sockets let us do this.

    The Unix socket interface is one of the more arcane things you'll ever run into. It is for that reason that instead of using that directly, you'll be using the following three procedures from sockettome.c:

    (On some machines, you have to use some special linking options -- When you make your final executable, On others, you don't need it -- be prepared to be flexible when you're testing your code on multiple machines.)

    extern int serve_socket(int port);
    		/* This serves a socket on the current machine with the given 
    		   port number.  It returns an fd for that socket.  */
    
    extern int accept_connection(int s);
    		/* This accepts a connection on the given socket (i.e.
                       it should only be called by the server).  It returns
                       the fd for the connection.  This fd is of course r/w */
    
    extern int request_connection(char *hn, int port);
    		/* This is what the client calls to connect to a server's
                       port.  It returns the fd for the connection. */
    
    Sockets work as follows:

    One process ``serves'' a socket. By doing this, it tells the operating system to set up a ``port'' identified by a port number. This is the handle by which other processes may connect with the serving process. There is a nice analogy of sockets to a telephone. A socket is like a telephone, and the port number is like a telephone number. The machine name is like the area code. Thus, in order for someone to "call you up", they need to request a connection to your area code/port number, just like making a phone call.

    When you "serve" a socket, you are setting yourself up with a telephone and a telephone number. The result of serving a socket is a file descriptor. However, it is not a normal file descriptor upon which you may read and write. Instead, the only things you can really do with it are close it (which tells Unix to not let other processes connect to that port), or accept connections on it.

    The server accepts a connection on a socket fd by calling accept_connection(). Accept_connection() only returns when another process has connected to the socket. It returns a new file descriptor which is that connection. This descriptor is r/w. In other words if you write() to it, the bytes will go to the connected process, and if you read() from it, it will read bytes written by the other process.

    "on-serving processes connect with serving ones with request_connection(). This means to connect with the process that has served the socket with the given port number on the given host machine. This is like initiating a phone call. It returns a file descriptor for its end of the connection.

    Once a socket has been served, it may be connected to multiple times. That is, the serving process may call accept_connection() any number of times, as long as there are other processes calling request_connection() to that machine and port number. This is kind of like having multiple phone lines with one telephone number. This will be useful when you write jtalk with more than 2 people.


    Let's look at some example code. Look at serve1.c;

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include "sockettome.h"
    
    main(int argc, char **argv)
    {
      char *un, c, s[1000];
      int port, sock, fd;
    
      if (argc != 2) {
        fprintf(stderr, "usage: serve1 port\n");
        exit(1);
      }
    
      port = atoi(argv[1]);
      if (port < 5000) {
        fprintf(stderr, "usage: serve1 hostname port\n");
        fprintf(stderr, "       port must be > 5000\n");
        exit(1);
      }
      un = getenv("USER");
    
      sock = serve_socket(port);
      fd = accept_connection(sock);
    
      printf("Connection established.  Sending `Server: %s'\n", un);
      sprintf(s, "Server: %s\n", un);
      write(fd, s, strlen(s));
    
      printf("Receiving:\n\n");
      do {
        if (read(fd, &c, 1) != 1) {
          printf("Socket Closed Prematurely\n");
          exit(0);
        } else putchar(c);
      } while (c != '\n');
    
      exit(0);
    }
    

    Serve1.c takes one argument -- the port number of a socket to serve. This port number should be greater than 5000. This is because smaller port numbers may be used for other Unix programs.

    Serve1.c serves a socket and then waits to accept a connection. Once it receives that connection, it sends the string "Server: ", then the username of the owner of the serving process, and a newline to the other end of the connection. Then it reads bytes from the connection and prints them out until it gets a newline. Once done, it exits. You can test serve1 by running a telnet client on another machine. Telnet is a generic client that attaches to a server, then it does two things:

    1. It reads lines from standard input and sends them along the connection.
    2. It reads lines from the socket and prints them to standard output.
    Below, I run the server on hydra3, and the client on my Macintosh. I have padded this output with extra lines so that you can see the timing of things relative to each other.

    On hydra3:
    UNIX> serve1 5555
    
    Connection established.  Sending `Server: plank'
    Receiving:
    
    
    
    The moment I wake up, before I put on my makeup....
    UNIX> 
    
    On my Macintosh:
    
    UNIX> telnet hydra3.eecs.utk.edu 5555
    Trying 160.36.56.85...
    Connected to hydra3.eecs.utk.edu.
    Escape character is '^]'.
    Server: plank
    The moment I wake up, before I put on my makeup....
    Connection closed by foreign host.
    UNIX> 
    

    As you can see, the two processes connect with each other and send the bytes back and forth. Easy enough. Now, a few mundane details. First, suppose I try to run the server again quickly:

    UNIX> serve1 5555
    bind(): Address already in use
    UNIX> 
    
    This happens because the operating system holds the port number typically for two minutes before allowing you to reuse it. Be prepared.

    If you look at that do/while loop above, you'll not that it's really inefficient. It reads one character at a time, and we know from our experience with buffering that this is a bad thing. You may want to try doing read()'s of larger numbers. That's a fine thing, but you need to be aware that with sockets, any number of bytes can come back from a read() call -- it does not necessarily have to match the corresponding write() call, or newlines, or, frankly, anything. That makes it a pain to do things like reading until you receive a newline. You may be tempted to the do/while loop above, but please resist the temptation. There is a better way, illustrated in the next program:

    Serve2.c changes serve1.c in how it writes to the client:

    main(int argc, char **argv)
    {
      char *hn, *un;
      int port, sock, fd;
      int i;
      char s[1000];
      FILE *fsock;
    
      .... Error check, serve the socket, start the connection and send a string.
    
      printf("Receiving:\n\n");
    
      fsock = fdopen(fd, "r");
    
      if (fgets(s, 1000, fsock) == NULL) {
        printf("Error -- socket closed\n");
      } else {
        printf("%s", s);
      }
    }
    

    Fdopen() is just like fopen(), except it takes an open file descriptor as an argument rather than a file. This allows you to use the familiar primitives from the standard I/O library, which have been implemented with sockets in mind. In particular, they do the proper buffering and read retrying to make sure that you read what you want efficiently. In serve2.c, we use fgets() to read the line from the client.


    Adding a client

    Client1.c is very similar to serve2.c. It simply has a client connect using request_connection(). It receives the line of text from the server and sends a line of its own. Here it is working:

    On hydra3:
    UNIX> serve2 5555
    Connection established.  Sending `Server: plank'
    Receiving:
    
    Client: plank
    UNIX> 
    
    On my Macintosh:
    UNIX> client1 hydra3.eecs.utk.edu 5555
    Connection established.  Receiving:
    
    Received: Server: plank
    Sending `Client: plank' to the server
    UNIX> 
    

    Go over both programs until you understand all this. If you run "client1" before "serve2", that's ok. It will wait until the socket is served.

    Client1 can be run from any machine and any user. For example, I got Dr. Vander Zanden to try this from his machine: His output:

    UNIX> client1 hydra3.eecs.utk.edu 5001
    Connection established.  Receiving:
    
    Received: Server: plank
    Sending `Client: bvz' to the server
    UNIX>
    
    and mine:
    UNIX> serve1 5001
    Connection established.  Sending `Server: plank'
    Receiving:
    
    Client: bvz
    UNIX1>
    

    Alternate

    Now, suppose I want to write a limited version of "talk" -- It will let a server/client pair establish a connection, and then alternate sending lines to one another. This is in alternate.c. Each process alternates reading a line from stdin and sending it along the socket, and then reading a line from the socket and sending it along stdout.

    Try running alternate from two windows on the same machine, or from two different machines, or from two different users on different machines. Here is an example of the server on hydra3 and the client on my Macintosh. Pay attention to the part where the client writes before the server is ready (it's the line "While combing my hair now), and what the output is:

    On hydra3:
    UNIX> alternate hydra3.eecs.utk.edu 5555 s
    
    Connection established.  Client should start talking
    
    The moment I wake up
    Before I put on my make up
    
    
    I say a little prayer for you
    
    And wondering what dress to wear now
    While combing my hair now
    UNIX> 
    
    On my Macintosh:
    
    UNIX> alternate hydra3.eecs.utk.edu 5555 c
    Connection established.  Client should start talking
    The moment I wake up
    
    
    Before I put on my make up
    I say a little prayer for you
    
    While combing my hair now
    
    And wondering what dress to wear now
    <CNTL-D>
    UNIX> 
    


    Minitalk

    You'll note that alternate isn't totally useful. What you'd like is for each end of the connection to simultaneously wait for the user to type a line in on stdin and for a line to come in from the socket. You'll be able to do that with select() which we'll go over next lecture. Until then, you can hack up a version of talk() which uses 4 total processes -- two for the client and two for the server. One client-server pair just lets the client type lines in and it prints it out to the server, and the other lets the server type lines in and it prints it out to the client. This is in minitalk.c. Note that minitalk dups the fd onto 0 or 1 depending on whether the process is the parent or child, and then exec's cat in all four processes. This is a real hack, but it works -- you have the server parent reading from its stdin and sending the output along the socket to the client child, and likewise from the client parent to the server child.

    Here's the relevant code:

      .....
    
      printf("Connection made -- input goes to remote site\n");
      printf("                   output comes from remote site\n");
    
      if (fork() == 0) {
        dup2(fd, 0);
      } else {
        dup2(fd, 1);
      }
      close(fd);
      execlp("cat", "cat", 0);
      fprintf(stderr, "Exec failed.  Drag\n");
    }
    

    The only problem with minitalk is that once one of the processes dies, the others don't automatically follow suit. Think about why. Also think about what would happen if you swapped the parent and the child (in other words, if you had the parent perform dup2(fd, 0) and the child perform dup2(fd, 1). Try it to test yourself.

    When you run minitalk, after you finish, type "ps" and kill extraneous "cat" processes.

    On hydra3:
    UNIX> minitalk hydra3.eecs.utk.edu 5555 s
    
    Connection made -- input goes to remote site
                       output comes from remote site
    
    The moment I wake up
    
    Before I put on my make up
    I say a little prayer for you
    
    <CNTL-D>
    UNIX> ps x
      PID TTY      STAT   TIME COMMAND
     3681 ?        S<     0:00 sshd: plank@pts/1
     3682 pts/1    S<s    0:00 -csh
     6098 pts/1    S<     0:00 cat
     6106 pts/1    R<+    0:00 ps x
    UNIX>  While combing my hair now
    And wondering what dress to wear now
    UNIX> kill 6098
    
    On my Macintosh:
    
    UNIX> minitalk hydra3.eecs.utk.edu 5555 c
    Connection made -- input goes to remote site
                       output comes from remote site
    The moment I wake up
    
    Before I put on my make up
    
    
    I say a little prayer for you
    
    
    
    
    
    
    While combing my hair now
    And wondering what dress to wear now
    
    UNIX> 
    

    You can fix the problem by adding another process to the mix. Instead of forking one process, you fork two, and have them both do the execlp of cat. Then you call wait(), and when one of the processes dies, you kill the other one. That cleans up all of the processes -- try it for yourself! The code is in minitalk2.c -- here's the relevant section:

    main(int argc, char **argv)
    {
      char *hn;
      int port, sock, fd;
      int pid1, pid2, p, dummy;
    
    .........
    
      pid1 = fork();
      if (pid1 == 0) {
        dup2(fd, 0);
      } else {
        pid2 = fork();
        if (pid2 == 0) {
          dup2(fd, 1);
        } else {
          close(fd);
          close(0);
          close(1);
          p = wait(&dummy);
          kill((p == pid1) ? pid2 : pid1, SIGKILL);
          exit(0);
        }
      }
      close(fd);
      execlp("cat", "cat", NULL);
      fprintf(stderr, "Exec failed.  Drag\n");
    }
    


    A Final Minitalk - One Connection Per Direction

    Here's a final version of minitalk. It's a good one to go over, as this is the kind of question I like to have you code up in class. What we're going to do is have a very similar structure to the last minitalk, only now we are going to have two connections between the client and the server. The client and server will each fork off two children, and then each set of children will communicate exclusively with its own socket connection.

    The code is in minitalk3.c. I'll break it into a few parts. The first part contains the headers, variable declarations and parses the command line:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <signal.h>
    #include <unistd.h>
    #include "sockettome.h"
    
    main(int argc, char **argv)
    {
      char *hn;
      int port, socket;   /* Port number, and socket fd for server */
      int conn1, conn2;   /* FD's for the two connections */
      int pid1, pid2;     /* Process id's of the two children */
      int pid, dummy;;    /* Used by the parent when it calls wait() */
      int id;             /* ID of process -- Parent/C1/C2: 'P', '1' or '2' */
      int cs;             /* 'c' or 's' - client or server */
    
      if (argc != 4) {
        fprintf(stderr, "usage: minitalk hostname port s|c\n");
        exit(1);
      }
    
      hn = argv[1];
      port = atoi(argv[2]);
      if (port < 5000) {
        fprintf(stderr, "usage: minitalk hostname port s|c\n");
        fprintf(stderr, "       port must be > 5000\n");
        exit(1);
      }
    
    }
    

    The next part has the server serve a socket and accept two connections, while the client requests two connections. When this code is done, the system looks as so:

      /* Set up the connections */
    
      if (strcmp(argv[3], "s") == 0) {
        cs = 's';
        socket = serve_socket(port);
        conn1 = accept_connection(socket);
        conn2 = accept_connection(socket);
        close(socket);
      } else if (strcmp(argv[3], "c") == 0) {
        cs = 'c';
        conn1 = request_connection(hn, port);
        conn2 = request_connection(hn, port);
      } else {
        fprintf(stderr, "usage: minitalk hostname port s|c\n");
        fprintf(stderr, "       last argument must be `s' or `c'\n");
        exit(1);
      }
    
    

    The last part of the code does all of the magic, having the two sets of children talk, each with their own connection. Here's how it looks pictorally:

    And here's the code that makes it happen. As with minitalk2, when one child dies, the parent kills the other:

      /* Set up parent and two children.  In the parent,
         pid1 and pi2 are the process id's of the children */
    
      pid1 = fork();
      if (pid1 == 0) {
        id = '1';
      } else {
        pid2 = fork();
        if (pid2 == 0) {
          id = '2';
        } else {
          id = 'P';
        }
      }
    
      /* Connect C1 of the server to C2 of the client */
    
      if (id == '1' && cs == 's') dup2(conn1, 1);
      if (id == '2' && cs == 'c') dup2(conn1, 0);
    
      /* Connect C1 of the client to C2 of the server */
    
      if (id == '1' && cs == 'c') dup2(conn2, 1);
      if (id == '2' && cs == 's') dup2(conn2, 0);
    
      /* Everyone (parent included) closes the connections */
    
      close(conn1);
      close(conn2);
    
      /* Have the parent call wait, and then kill the other child */
    
      if (id == 'P') {
        pid = wait(&dummy);
        if (pid == pid1) {
          kill(pid2, SIGKILL);
        } else {
          kill(pid1, SIGKILL);
        }
        exit(0);
      }
    
      /* Otherwise, have the children execlp cat */
    
      execlp("cat", "cat", NULL);
      fprintf(stderr, "Exec failed.  Drag\n");
    }