CS360 Lecture notes -- Sockets


Introduction, and playing with sockets using nc.

Sockets are the most general constructs for 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 (or both of them have a common ancestor). 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.

A socket is a construct maintained and controlled by the operating system. You can do three basic things with a socket:

  1. Serve the socket: To do this, you must ask the operating system to allow you to serve a socket with a given port number. If the operating system responds affirmatively, then you can:

  2. Listen for and accept client connections: Here you tell the operating system that you want to wait for clients to connect to the socket. When they do, the operating system will give you a handle for the connection. This handle will be a file descriptor.

  3. Request a connection as a client: Here you tell your operating system that you want to request a connection to a specific port number on (potentially) a different machine. That machine is called the host. If a process on that host has served that socket and is accepting connections, then you will also receive a file descriptor for the connection. You and the server may now talk to each other through your file descriptors, much like you have processes talk with each other over pipes.
The Unix program nc gives you a very simple mechanism for playing with sockets. If you call it with the "-l" option, then it will serve a socket on that port and accept a connection. You can call nc with a host and port number, and it will request a connection. When the connection is made, the two nc processes may communicate with each other through standard input. Below, I show a communication with a server on hydra1 and a client on hydra2:

On hydra1
UNIX> nc -l 8888


Hi -- it's me, on hydra2

Are you there?
Yes, but I'm leaving.

<CNTL-C>
UNIX>
On hydra2

UNIX> nc hydra1 8888
Hi -- it's me, on hydra2

Are you there?


Yes, but I'm leaving.

What?
Ncat: Broken pipe.
UNIX> 

There are a lot of options to nc, so you should play with it for a bit.


Socket Rules At The University of Tennessee

Unfortunately, sockets have historically been exploited for security violations, so our department is relatively strict about them. You should be aware of the following:
This lecture focuses on programming with sockets, as implemented by the program src/sockettome.c (name provided by Harrison "Jay" Cross, a student in my CS360 class in 2011).

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

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

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

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 file descriptor by calling accept_connection(). Accept_connection() only returns when another process has connected to the socket. It returns a new file descriptor which is the handle to the connection. This descriptor is r/w. 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 connected process.

Clients connect with servers by calling request_connection(). They must specify the host and the port of the server, This is like initiating a phone call. If the host/port combination is being server, and if the server is calling accept_connection(), then request_connection() returns a file descriptor for its end of the connection. The client uses it like the server uses the file descriptor from accept_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 two people.


Let's look at some example code. Start with src/serve1.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "sockettome.h"

/* This program serves a socket on the given port, and
   accepts a connection.  It sends the username of whoever is running the 
   program along the connection, and then it reads a line of text, one
   character at a time.  It prints the line and exits.  You can connect
   to this program with nc. */

int main(int argc, char **argv)
{
  char *un, c, s[1000];
  int port, sock, fd;

  /* Parse the command line and error check. */

  if (argc != 2) {
    fprintf(stderr, "usage: serve1 port\n");
    exit(1);
  }

  port = atoi(argv[1]);
  if (port < 8000) {
    fprintf(stderr, "usage: serve1 port\n");
    fprintf(stderr, "       port must be > 8000\n");
    exit(1);
  }

  /* Serve the socket and accept the connection. */

  sock = serve_socket(port);
  fd = accept_connection(sock);

  /* Send the username along the socket. */

  un = getenv("USER");
  printf("Connection established.  Sending `Server: %s'\n", un);
  sprintf(s, "Server: %s\n", un);
  write(fd, s, strlen(s));

  /* Read a line of text, one character at a time. */
  /* This is bad code, because you shouldn't call read(.., 1,...).  */
  /* You'll see a better way to do this soon. */

  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 8000. 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 nc client on another machine. As mentioned above, you can use nc to be 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 hydra4. I have padded this output with extra lines so that you can see the timing of things relative to each other.

On hydra3:
UNIX> bin/serve1 8888

Connection established.  Sending `Server: plank'
Receiving:



The moment I wake up, before I put on my makeup....
UNIX> 
On hydra4:

UNIX> nc hydra3 8888
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> bin/serve1 8888
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 note 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:

src/serve2.c changes serve1.c in how it writes to the client:

  /* Here is a new variable declaration */
  FILE *fsock;

   .................

  /* Here's the new code -- see how you can use fgets now, instead of read(). */

  fsock = fdopen(fd, "r");

  if (fgets(s, 1000, fsock) == NULL) {
    printf("Error -- socket closed\n");
  } else {
    printf("%s", s);
  }
  return 0;
}

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

src/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> bin/serve2 8888
Connection established.  Sending `Server: plank'
Receiving:

Client: plank
UNIX> 
On hydra4:
UNIX> bin/client1 hydra3 8888
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:

My output on hydra3:
UNIX> bin/serve1 8001

Connection established.  Sending `Server: plank'
Receiving:

Client: bvz
UNIX>
His output on his machine:
UNIX> bin/client1 hydra3.eecs.utk.edu 8001
Connection established.  Receiving:

Received: Server: plank
Sending `Client: bvz' to the server
UNIX>


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 src/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 bin/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 hydra4. 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> bin/alternate hydra3.eecs.utk.edu 8888 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 hydra4:

UNIX> bin/alternate hydra3.eecs.utk.edu 8888 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 threads when we learn about them. 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 src/minitalk1.c. Note that minitalk1 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");
  exit(1);
}

The only problem with minitalk1 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 minitalk1, after you finish, type "ps" and kill extraneous "cat" processes.

On hydra3:
UNIX> bin/minitalk1 hydra3.eecs.utk.edu 8888 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 hydra4

UNIX> bin/minitalk1 hydra3.eecs.utk.edu 8888 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 src/minitalk2.c -- here's the relevant section:

  pid1 = fork();

  /* The first child will have standard input come from the socket. */

  if (pid1 == 0) {
    dup2(fd, 0);

  /* Otherwise, fork again: */

  } else {
    pid2 = fork();

    /* The second child will have standard output go to the socket. */

    if (pid2 == 0) {
      dup2(fd, 1);

    /* The parent closes all extraneous file descriptors, and waits for
       one child to die.  When that happens, the parent kills the other child. */

    } else {
      close(fd);
      close(0);
      close(1);
      p = wait(&dummy);
      kill((p == pid1) ? pid2 : pid1, SIGKILL);
      exit(0);
    }
  }

  /* The children both close the socket fd (remember, they have each dup'd
     this to either zero or one, and then exec cat. */

  close(fd);
  execlp("cat", "cat", NULL);
  fprintf(stderr, "Exec failed.  Drag\n");
  return 0;
}


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 during an exam. 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 src/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"

int 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 < 8000) {
    fprintf(stderr, "usage: minitalk hostname port s|c\n");
    fprintf(stderr, "       port must be > 8000\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 child:

  /* Set up parent and two children.  In the parent,
     pid1 and pid2 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");
  exit(1);
}