Internet Programming with Java Course

1.4 TCP ñîêåòè – êëèåíòñêè è ñúðâúð ñîêåòè

 

Sockets

 

The socket is the software abstraction used to represent the “terminals” of a connection between two machines. For a given connection, there’s a socket on each machine, and you can imagine a hypothetical “cable” running between the two machines with each end of the “cable” plugged into a socket. Of course, the physical hardware and cabling between machines is completely unknown. The whole point of the abstraction is that we don’t have to know more than is necessary.

In Java, you create a socket to make the connection to the other machine, then you get an InputStream and OutputStream (or, with the appropriate converters, Reader and Writer) from the socket in order to be able to treat the connection as an IO stream object. There are two stream-based socket classes: a ServerSocket that a server uses to “listen” for incoming connections and a Socket that a client uses in order to initiate a connection. Once a client makes a socket connection, the ServerSocket returns (via the accept( ) method) a corresponding server side Socket through which direct communications will take place. From then on, you have a true Socket to Socket connection and you treat both ends the same way because they are the same. At this point, you use the methods getInputStream( ) and getOutputStream( ) to produce the corresponding InputStream and OutputStream objects from each Socket.

The use of the term ServerSocket would seem to be another example of a confusing name scheme in the Java libraries. You might think ServerSocket would be better named “ServerConnector” or something without the word “Socket” in it. You might also think that ServerSocket and Socket should both be inherited from some common base class. Indeed, the two classes do have several methods in common but not enough to give them a common base class. Instead, ServerSocket’s job is to wait until some other machine connects to it, then to return an actual Socket. This is why ServerSocket seems to be a bit misnamed, since its job isn’t really to be a socket but instead to make a Socket object when someone else connects to it.

However, the ServerSocket does create a physical “server” or listening socket on the host machine. This socket listens for incoming connections and then returns an “established” socket (with the local and remote endpoints defined) via the accept( ) method. The confusing part is that both of these sockets (listening and established) are associated with the same server socket. The listening socket can accept only new connection requests and not data packets. So while ServerSocket doesn’t make much sense programmatically, it does “physically.”

When you create a ServerSocket, you give it only a port number. You don’t have to give it an IP address because it’s already on the machine it represents. When you create a Socket, however, you must give both the IP address and the port number where you’re trying to connect. (On the other hand, the Socket that comes back from ServerSocket.accept( ) already contains all this information.)

 

A simple server and client

 

This example makes the simplest use of servers and clients using sockets. All the server does is wait for a connection, then uses the Socket produced by that connection to create an InputStream and OutputStream. After that, everything it reads from the InputStream it echoes to the OutputStream until it receives the line END, at which time it closes the connection.

The client makes the connection to the server, then creates an OutputStream. Lines of text are sent through the OutputStream. The client also creates an InputStream to hear what the server is saying (which, in this case, is just the words echoed back).

Both the server and client use the same port number and the client uses the local loopback address to connect to the server on the same machine so you don’t have to test it over a network. (For some configurations, you might need to be connected to a network for the programs to work, even if you aren’t communicating over that network.)

Here is the server:

 

//: JabberServer.java

// Very simple server that just

// echoes whatever the client sends.

import java.io.*;

import java.net.*;

 

public class JabberServer { 

    // Choose a port outside of the range 1-1024:

    public static final int PORT = 8080;

    public static void main(String[] args)

            throws IOException {

        ServerSocket s = new ServerSocket(PORT);

        System.out.println("Started: " + s);

        try {

            // Blocks until a connection occurs:

            Socket socket = s.accept();

            try {

                System.out.println(

                    "Connection accepted: "+ socket);

                BufferedReader in =

                    new BufferedReader(

                        new InputStreamReader(

                            socket.getInputStream()));

                // Output is automatically flushed

                // by PrintWriter:

                PrintWriter out =

                    new PrintWriter(

                        new BufferedWriter(

                            new OutputStreamWriter(

                                socket.getOutputStream())),true);

                while (true) { 

                    String str = in.readLine();

                    if (str.equals("END")) break;

                    System.out.println("Echoing: " + str);

                    out.println(str);

                }

            // Always close the two sockets...

            } finally {

                System.out.println("closing...");

                socket.close();

            }

        } finally {

            s.close();

        }

    }

}

You can see that the ServerSocket just needs a port number, not an IP address (since it’s running on this machine!). When you call accept( ), the method blocks until some client tries to connect to it. That is, it’s there waiting for a connection but other processes can run (see Chapter 14). When a connection is made, accept( ) returns with a Socket object representing that connection.

The responsibility for cleaning up the sockets is crafted carefully here. If the ServerSocket constructor fails, the program just quits (notice we must assume that the constructor for ServerSocket doesn’t leave any open network sockets lying around if it fails). For this case, main( ) throws IOException so a try block is not necessary. If the ServerSocket constructor is successful then all other method calls must be guarded in a try-finally block to ensure that, no matter how the block is left, the ServerSocket is properly closed.

The same logic is used for the Socket returned by accept( ). If accept( ) fails, then we must assume that the Socket doesn’t exist or hold any resources, so it doesn’t need to be cleaned up. If it’s successful, however, the following statements must be in a try-finally block so that if they fail the Socket will still be cleaned up. Care is required here because sockets use important non-memory resources, so you must be diligent in order to clean them up (since there is no destructor in Java to do it for you).

Both the ServerSocket and the Socket produced by accept( ) are printed to System.out. This means that their toString( ) methods are automatically called. These produce:

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]

Socket[addr=127.0.0.1,PORT=1077,localport=8080]

 

Shortly, you’ll see how these fit together with what the client is doing.

The next part of the program looks just like opening files for reading and writing except that the InputStream and OutputStream are created from the Socket object. Both the InputStream and OutputStream objects are converted to Java 1.1 Reader and Writer objects using the “converter” classes InputStreamReader and OutputStreamWriter, respectively. You could also have used the Java 1.0 InputStream and OutputStream classes directly, but with output there’s a distinct advantage to using the Writer approach. This appears with PrintWriter, which has an overloaded constructor that takes a second argument, a boolean flag that indicates whether to automatically flush the output at the end of each println( ) (but not print( )) statement. Every time you write to out, its buffer must be flushed so the information goes out over the network. Flushing is important for this particular example because the client and server each wait for a line from the other party before proceeding. If flushing doesn’t occur, the information will not be put onto the network until the buffer is full, which causes lots of problems in this example.

When writing network programs you need to be careful about using automatic flushing. Every time you flush the buffer a packet must be created and sent. In this case, that’s exactly what we want, since if the packet containing the line isn’t sent then the handshaking back and forth between server and client will stop. Put another way, the end of a line is the end of a message. But in many cases messages aren’t delimited by lines so it’s much more efficient to not use auto flushing and instead let the built-in buffering decide when to build and send a packet. This way, larger packets can be sent and the process will be faster.

Note that, like virtually all streams you open, these are buffered. There’s an exercise at the end of the chapter to show you what happens if you don’t buffer the streams (things get slow).

The infinite while loop reads lines from the BufferedReader in and writes information to System.out and to the PrintWriter out. Note that these could be any streams, they just happen to be connected to the network.

When the client sends the line consisting of “END” the program breaks out of the loop and closes the Socket.

Here’s the client:

//: JabberClient.java

// Very simple client that just sends

// lines to the server and reads lines

// that the server sends.

import java.net.*;

import java.io.*;

 

public class JabberClient {

    public static void main(String[] args)

            throws IOException {

        // Passing null to getByName() produces the

        // special "Local Loopback" IP address, for

        // testing on one machine w/o a network:

        InetAddress addr =

            InetAddress.getByName(null);

        // Alternatively, you can use

        // the address or name:

        // InetAddress addr = InetAddress.getByName("127.0.0.1");

        // InetAddress addr = InetAddress.getByName("localhost");

        System.out.println("addr = " + addr);

        Socket socket =

            new Socket(addr, JabberServer.PORT);

        // Guard everything in a try-finally to make

        // sure that the socket is closed:

        try {

            System.out.println("socket = " + socket);

            BufferedReader in =

                new BufferedReader(

                    new InputStreamReader(

                        socket.getInputStream()));

            // Output is automatically flushed

            // by PrintWriter:

            PrintWriter out =

                new PrintWriter(

                    new BufferedWriter(

                        new OutputStreamWriter(

                            socket.getOutputStream())),true);

            for(int i = 0; i < 10; i ++) {

                out.println("howdy " + i);

                String str = in.readLine();

                System.out.println(str);

            }

            out.println("END");

        } finally {

            System.out.println("closing...");

            socket.close();

        }

    }

}

In main( ) you can see all three ways to produce the InetAddress of the local loopback IP address: using null, localhost, or the explicit reserved address 127.0.0.1. Of course, if you want to connect to a machine across a network you substitute that machine’s IP address. When the InetAddress addr is printed (via the automatic call to its toString( ) method) the result is:

localhost/127.0.0.1

By handing getByName( ) a null, it defaulted to finding the localhost, and that produced the special address 127.0.0.1.

Note that the Socket called socket is created with both the InetAddress and the port number. To understand what it means when you print out one of these Socket objects, remember that an Internet connection is determined uniquely by these four pieces of data: clientHost, clientPortNumber, serverHost, and serverPortNumber. When the server comes up, it takes up its assigned port (8080) on the localhost (127.0.0.1). When the client comes up, it is allocated to the next available port on its machine, 1077 in this case, which also happens to be on the same machine (127.0.0.1) as the server. Now, in order for data to move between the client and server, each side has to know where to send it. Therefore, during the process of connecting to the “known” server, the client sends a “return address” so the server knows where to send its data. This is what you see in the example output for the server side:

Socket[addr=127.0.0.1,port=1077,localport=8080]

This means that the server just accepted a connection from 127.0.0.1 on port 1077 while listening on its local port (8080). On the client side:

Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

which means that the client made a connection to 127.0.0.1 on port 8080 using the local port 1077.

You’ll notice that every time you start up the client anew, the local port number is incremented. It starts at 1025 (one past the reserved block of ports) and keeps going up until you reboot the machine, at which point it starts at 1025 again. (On UNIX machines, once the upper limit of the socket range is reached, the numbers will wrap around to the lowest available number again.)

Once the Socket object has been created, the process of turning it into a BufferedReader and PrintWriter is the same as in the server (again, in both cases you start with a Socket). Here, the client initiates the conversation by sending the string “howdy” followed by a number. Note that the buffer must again be flushed (which happens automatically via the second argument to the PrintWriter constructor). If the buffer isn’t flushed, the whole conversation will hang because the initial “howdy” will never get sent (the buffer isn’t full enough to cause the send to happen automatically). Each line that is sent back from the server is written to System.out to verify that everything is working correctly. To terminate the conversation, the agreed-upon “END” is sent. If the client simply hangs up, then the server throws an exception.

You can see that the same care is taken here to ensure that the network resources represented by the Socket are properly cleaned up, using a try-finally block.

Sockets produce a “dedicated” connection that persists until it is explicitly disconnected. (The dedicated connection can still be disconnected un-explicitly if one side, or an intermediary link, of the connection crashes.) This means the two parties are locked in communication and the connection is constantly open. This seems like a logical approach to networking, but it puts an extra load on the network. Later in the chapter you’ll see a different approach to networking, in which the connections are only temporary.

 

Serving multiple clients

The JabberServer works, but it can handle only one client at a time. In a typical server, you’ll want to be able to deal with many clients at once. The answer is multithreading, and in languages that don’t directly support multithreading this means all sorts of complications. In Chapter 14 you saw that multithreading in Java is about as simple as possible, considering that multithreading is a rather complex topic. Because threading in Java is reasonably straightforward, making a server that handles multiple clients is relatively easy.

The basic scheme is to make a single ServerSocket in the server and call accept( ) to wait for a new connection. When accept( ) returns, you take the resulting Socket and use it to create a new thread whose job is to serve that particular client. Then you call accept( ) again to wait for a new client.

In the following server code, you can see that it looks similar to the JabberServer.java example except that all of the operations to serve a particular client have been moved inside a separate thread class:

//: MultiJabberServer.java

// A server that uses multithreading to handle

// any number of clients.

import java.io.*;

import java.net.*;

 

class ServeOneJabber extends Thread {

    private Socket socket;

    private BufferedReader in;

    private PrintWriter out;

    public ServeOneJabber(Socket s)

            throws IOException {

        socket = s;

        in =

            new BufferedReader(

                new InputStreamReader(

                    socket.getInputStream()));

        // Enable auto-flush:

        out =

            new PrintWriter(

                new BufferedWriter(

                    new OutputStreamWriter(

                        socket.getOutputStream())), true);

        // If any of the above calls throw an

        // exception, the caller is responsible for

        // closing the socket. Otherwise the thread

        // will close it.

        start(); // Calls run()

    }

    public void run() {

        try {

            while (true) { 

                String str = in.readLine();

                if (str.equals("END")) break;

                System.out.println("Echoing: " + str);

                out.println(str);

            }

            System.out.println("closing...");

        } catch (IOException e) {

        } finally {

            try {

                socket.close();

            } catch(IOException e) {}

        }

    }

}

 

public class MultiJabberServer { 

    static final int PORT = 8080;

    public static void main(String[] args)

            throws IOException {

        ServerSocket s = new ServerSocket(PORT);

        System.out.println("Server Started");

        try {

            while(true) {

                // Blocks until a connection occurs:

                Socket socket = s.accept();

                try {

                    new ServeOneJabber(socket);

                } catch(IOException e) {

                    // If it fails, close the socket,

                    // otherwise the thread will close it:

                    socket.close();

                }

            }

        } finally {

            s.close();

        }

    }

}

The ServeOneJabber thread takes the Socket object that’s produced by accept( ) in main( ) every time a new client makes a connection. Then, as before, it creates a BufferedReader and auto-flushed PrintWriter object using the Socket. Finally, it calls the special Thread method start( ), which performs thread initialization and then calls run( ). This performs the same kind of action as in the previous example: reading something from the socket and then echoing it back until it reads the special “END” signal.

The responsibility for cleaning up the socket must again be carefully designed. In this case, the socket is created outside of the ServeOneJabber so the responsibility can be shared. If the ServeOneJabber constructor fails, it will just throw the exception to the caller, who will then clean up the thread. But if the constructor succeeds, then the ServeOneJabber object takes over responsibility for cleaning up the thread, in its run( ).

Notice the simplicity of the MultiJabberServer. As before, a ServerSocket is created and accept( ) is called to allow a new connection. But this time, the return value of accept( ) (a Socket) is passed to the constructor for ServeOneJabber, which creates a new thread to handle that connection. When the connection is terminated, the thread simply goes away.

If the creation of the ServerSocket fails, the exception is again thrown through main( ). But if it succeeds, the outer try-finally guarantees its cleanup. The inner try-catch guards only against the failure of the ServeOneJabber constructor; if the constructor succeeds, then the ServeOneJabber thread will close the associated socket.

To test that the server really does handle multiple clients, the following program creates many clients (using threads) that connect to the same server. Each thread has a limited lifetime, and when it goes away, that leaves space for the creation of a new thread. The maximum number of threads allowed is determined by the final int maxthreads. You’ll notice that this value is rather critical, since if you make it too high the threads seem to run out of resources and the program mysteriously fails.

//: MultiJabberClient.java

// Client that tests the MultiJabberServer

// by starting up multiple clients.

import java.net.*;

import java.io.*;

 

class JabberClientThread extends Thread {

    private Socket socket;

    private BufferedReader in;

    private PrintWriter out;

    private static int counter = 0;

    private int id = counter++;

    private static int threadcount = 0;

    public static int threadCount() {

        return threadcount;

    }

    public JabberClientThread(InetAddress addr) {

        System.out.println("Making client " + id);

        threadcount++;

        try {

            socket = new Socket(addr, MultiJabberServer.PORT);

        } catch(IOException e) {

            // If the creation of the socket fails,

            // nothing needs to be cleaned up.

        }

        try {   

            in =

                new BufferedReader(

                    new InputStreamReader(

                        socket.getInputStream()));

            // Enable auto-flush:

            out =

                new PrintWriter(

                    new BufferedWriter(

                        new OutputStreamWriter(

                            socket.getOutputStream())), true);

            start();

        } catch(IOException e) {

            // The socket should be closed on any

            // failures other than the socket

            // constructor:

            try {

                socket.close();

            } catch(IOException e2) {}

        }

        // Otherwise the socket will be closed by

        // the run() method of the thread.

    }

    public void run() {

        try {

            for(int i = 0; i < 25; i++) {

                out.println("Client " + id + ": " + i);

                String str = in.readLine();

                System.out.println(str);

            }

            out.println("END");

        } catch(IOException e) {

        } finally {

            // Always close it:

            try {

                socket.close();

            } catch(IOException e) {}

            threadcount--; // Ending this thread

        }

    }

}

 

public class MultiJabberClient {

    static final int MAX_THREADS = 40;

    public static void main(String[] args)

            throws IOException, InterruptedException {

        InetAddress addr =

            InetAddress.getByName(null);

        while(true) {

            if(JabberClientThread.threadCount()

                 < MAX_THREADS)

                new JabberClientThread(addr);

            Thread.currentThread().sleep(100);

        }

    }

}

The JabberClientThread constructor takes an InetAddress and uses it to open a Socket. You’re probably starting to see the pattern: the Socket is always used to create some kind of Reader and/or Writer (or InputStream and/or OutputStream) object, which is the only way that the Socket can be used. (You can, of course, write a class or two to automate this process instead of doing all the typing if it becomes painful.) Again, start( ) performs thread initialization and calls run( ). Here, messages are sent to the server and information from the server is echoed to the screen. However, the thread has a limited lifetime and eventually completes. Note that the socket is cleaned up if the constructor fails after the socket is created but before the constructor completes. Otherwise the responsibility for calling close( ) for the socket is relegated to the run( ) method.

The threadcount keeps track of how many JabberClientThread objects currently exist. It is incremented as part of the constructor and decremented as run( ) exits (which means the thread is terminating). In MultiJabberClient.main( ), you can see that the number of threads is tested, and if there are too many, no more are created. Then the method sleeps. This way, some threads will eventually terminate and more can be created. You can experiment with MAX_THREADS to see where your particular system begins to have trouble with too many connections.