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:
On hydra1UNIX> nc -l 8888 Hi -- it's me, on hydra2 Are you there? Yes, but I'm leaving. <CNTL-C> UNIX> |
On hydra2UNIX> 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.
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.
#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:
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.
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> |
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> |
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 hydra4UNIX> 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; } |
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); } |