CS360 Lecture notes -- Sockets

  • Jim Plank
  • Directory: /blugreen/homes/plank/cs360/notes/Sockets
  • 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 socketfun.c.

    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 socketfun.c:

    (On solaris, you have to use some special linking options if you use gcc and socketfun.c. When you make your final executable, specify ``-lsocket -lnsl''. This is done in the makefile for this lecture. When you are using cc, or an operating system like SunOS, you don't need to specify these.)

    extern int serve_socket(char *hn, int port);
    		/* This serves a socket at the given host name and port
    		   number.  It returns an fd for that socket.  Hn should
                       be the name of the server's machine. */
    
    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.

    "Non-serving processes connect with serving ones with request_connection(). It says 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 and client1.c.

    Serve1.c takes two arguments -- the host name and port number of a socket to serve. The host name should be the name of the machine running serve1 (for example, if you are on cetus1c, then you can call serve1 with a first argument of "cetus1c" or "cetus1c.cs.utk.edu" -- you need to give enough to specify the machine). The 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.

    Client1.c is similar. It takes a host name and a port number as arguments, but instead of serving a socket, it tries to connect to a socket at the host and port number. Once connected, it reads bytes from the connection and prints them until it receives a newline. Then it sends the string "Client: ", then the username of the owner process, and a newline to the serving process.

    So, lets try them: Get two xterms on your screen and cd to the right directory. Then on one of them, type: (I'm assuming you are on the machine cetus1c")

    UNIX1> serve1 cetus1c 5001
    
    It serves the socket, and is now waiting to accept a connection. Run client1 on the other xterm:
    UNIX2> client1 cetus1c 5001
    
    When I do this, I get the following. On the first xterm, I get:
    UNIX1> serve1 cetus1c 5001
    Connection established.  Sending `Server: plank'
    Receiving:
    Client: plank
    UNIX1>
    
    on the other, I get:
    UNIX2> client1 cetus1c 5001
    Connection established.  Receiving:
    Server: plank
    Sending `Client: plank'
    UNIX2>
    
    Go over both programs until you understand all this. If you run "client1" before "serve1", that's ok. It will wait until the socket is served.

    Now, suppose you try:

    UNIX1> serve1 cetus2f 5001
    bind(): Can't assign requested address
    UNIX1>
    
    That's serve_socket()'s way of saying that you can't serve a socket on cetus2f when you are currently running on cetus1c.

    Client1 can be run from any machine and any user. First try running it from a different machine. For example, start up serve1 as before on one xterm:

    UNIX1> serve1 cetus1c 5001
    
    and log into another machine to run client1:
    UNIX2> rlogin cetus2e
    ....
    UNIX2> client1 cetus1c 5001
    Connection established.  Receiving:
    Server: plank
    Sending `Client: plank'
    UNIX2>
    
    It should work fine. Now, try doing this with another user. For example, run serve1 as before:
    UNIX1> serve1 cetus1c 5001
    
    Then, get a friend to run client1. For example, I got Dr. Booth to try this from her machine:
    UNIX> client1 cetus1c 5001
    
    She received:
    UNIX> client1 cetus1c 5001
    Connection established.  Receiving:
    Server: plank
    Sending `Client: booth'
    UNIX>
    
    and I received:
    UNIX1> serve1 cetus1c 5001
    Connection established.  Sending `Server: plank'
    Receiving:
    Client: booth
    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. Make sure you understand what the procedure inout() does. 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.


    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.

    Try it out.

    The only problem with minitalk is that once one of the processes dies, the others don't automatically follow suit. Think about why. When you run minitalk, after you finish, type "ps" and kill extraneous "cat" processes.