Contents:
Sockets
URL Objects
The java.net package provides two basic mechanisms for accessing data and other resources over a network. The fundamental mechanism is called a socket. A socket allows programs to exchange groups of bytes called packets. There are a number of classes in java.net that support sockets, including Socket, ServerSocket, DatagramSocket, DatagramPacket, and MulticastSocket. The java.net package also includes a URL class that provides a higher-level mechanism for accessing and processing data over a network.
A socket is a mechanism that allows programs to send packets of bytes to each other. The programs do not need to be running on the same machine, but if they are running on different machines, they do need to be connected to a network that allows the machines to exchange data. Java's socket implementation is based on the socket library that was originally part of BSD UNIX. Programmers who are familiar with UNIX sockets or the Microsoft WinSock library should be able to see the similarities in the Java implementation.
When a program creates a socket, an identifying number called a port number is associated with the socket. Depending on how the socket is used, the port number is either specified by the program or assigned by the operating system. When a socket sends a packet, the packet is accompanied by two pieces of information that specify the destination of the packet:
Sockets typically work in pairs, where one socket acts as a client and the other functions as a server. A server socket specifies the port number for the network communication and then listens for data that is sent to it by client sockets. The port numbers for server sockets are well-known numbers that are known to client programs. For example, an FTP server uses a socket that listens at port 21. If a client program wants to communicate with an FTP server, it knows to contact a socket that listens at port 21.
The operating system normally specifies port numbers for client sockets because the choice of a port number is not usually important. When a client socket sends a packet to a server socket, the packet is accompanied by the port number of the client socket and the client's network address. The server is then able to use that information to respond to the client.
When using sockets, you have to decide which type of protocol that you want it to use to transport packets over the network: a connection-oriented protocol or a connectionless protocol. With a connection-oriented protocol, a client socket establishes a connection to a server socket when it is created. Once the connection has been established, a connection-oriented protocol ensures that data is delivered reliably, which means:
A connectionless protocol allows a best-effort delivery of packets. It does not guarantee that packets are delivered or that packets are read by the receiving program in the same order they were sent. A connectionless protocol trades these deficiencies for performance advantages over connection-oriented protocols. Here are two types of situations in which connectionless protocols are frequently preferred over connection-oriented protocols:
Table 8.1 shows the roles of the various socket classes in the java.net package.
Client |
Server |
|
---|---|---|
Connection-oriented Protocol |
Socket |
ServerSocket |
Connectionless Protocol |
DatagramSocket |
DatagramSocket |
As of Java 1.1, the java.net package also contains a MulticastSocket class that supports connectionless, multicast data communication.
When you are writing code that implements the server side of a connection-oriented protocol, your code typically follows this pattern:
The code that implements the client side of a connection-oriented protocol is quite simple. It creates a Socket object that opens a connection with a server, and then it uses that Socket object to communicate with the server.
Now let's look at an example. The example consists of a pair of programs that allows a client to get the contents of a file from a server. The client requests the contents of a file by opening a connection to the server and sending it the name of a file followed by a newline character. If the server is able to read the named file, it responds by sending the string "Good:\n" followed by the contents of the file. If the server is not able to read the named file, it responds by sending the string "Bad:" followed by the name of the file and a newline character. After the server has sent its response, it closes the connection.
Here's the program that implements the server side of this file transfer:
public class FileServer extends Thread { public static void main(String[] argv) { ServerSocket s; try { s = new ServerSocket(1234, 10); }catch (IOException e) { System.err.println("Unable to create socket"); e.printStackTrace(); return; } try { while (true) { new FileServer(s.accept()); } }catch (IOException e) { } } private Socket socket; FileServer(Socket s) { socket = s; start(); } public void run() { InputStream in; String fileName = ""; PrintStream out = null; FileInputStream f; try { in = socket.getInputStream(); out = new PrintStream(socket.getOutputStream()); fileName = new DataInputStream(in).readLine(); f = new FileInputStream(fileName); }catch (IOException e) { if (out != null) out.print("Bad:"+fileName+"\n"); out.close(); try { socket.close(); }catch (IOException ie) { } return; } out.print("Good:\n"); // send contents of file to client. byte[] buffer = new byte[4096]; try { int len; while ((len = f.read(buffer)) > 0) { out.write(buffer, 0, len); }// while }catch (IOException e) { }finally { try { in.close(); out.close(); socket.close(); }catch (IOException e) { } } } }
The FileServer class implements the server side of the file transfer; it is a subclass of Thread to make it easier to write code that can handle multiple connections at the same time. The main() method provides the top-level logic for the program. The first thing that main() does is to create a ServerSocket object to listen for connections. The constructor for ServerSocket takes two parameters: the port number for the socket and a value that specifies the maximum length of the pending connections queue. The operating system can accept connections on behalf of the socket when the server program is busy doing something other than accepting connections. If the second parameter is greater than zero, the operating system can accept up to that many connections on behalf of the socket and store them in a queue. If the second parameter is zero, however, the operating system does not accept any connections on behalf of the server program. The remainder of the main() method accepts a connection, creates a new instance of the FileServer class to process the connection, and then waits for the next connection.
Each FileServer object is responsible for handling a connection accepted by its main() method. A FileServer object uses a private variable, socket, to refer to the Socket object that allows it to communicate with the client program on the other end of the connection. The constructor for FileServer sets its socket variable to refer to the Socket object that is passed to it by the main() method and then calls its start() method. The FileServer class inherits the start() method from the Thread class; the start() method starts a new thread that calls the run() method. Because the rest of the connection processing is done asynchronously in a separate thread, the constructor can return immediately. This allows the main() method to accept another connection right away, instead of having to wait for this connection to be fully processed before accepting another.
The run() method uses the in and out variables to refer to InputStream and PrintStream objects that read from and write to the connection associated with the Socket object, respectively. These streams are created by calling the getInputStream() and getOutputStream() methods of the Socket object. The run() method then reads the name of the file that the client program wants to receive and creates a FileInputStream to read that file. If any of the methods called up to this point have detected a problem, they throw some kind of IOException. In this case, the server sends a response to the client that consists of the string "Bad:" followed by the filename and then closes the socket and returns, which kills the thread.
If everything up to this point has been fine, the server sends the string "Good:" and then copies the contents of the file to the socket. The copying is done by repeatedly filling a buffer with bytes from the file and writing the buffer to the socket. When the contents of the file are exhausted, the streams and the socket are closed, the run() method returns, and the thread dies.
Now let's take a look at the client part of this program:
public class FileClient { private static boolean usageOk(String[] argv) { if (argv.length != 2) { String msg = "usage is: " + "FileClient server-name file-name"; System.out.println(msg); return false; } return true; } public static void main(String[] argv) { int exitCode = 0; if (!usageOk(argv)) return; Socket s = null; try { s = new Socket(argv[0], 1234); }catch (IOException e) { String msg = "Unable to connect to server"; System.err.println(msg); e.printStackTrace(); System.exit(1); } InputStream in = null; try { OutputStream out = s.getOutputStream(); new PrintStream(out).print(argv[1]+"\n"); in = s.getInputStream(); DataInputStream din = new DataInputStream(in); String serverStatus = din.readLine(); if (serverStatus.startsWith("Bad")) { exitCode = 1; int ch; while((ch = in.read()) >= 0) { System.out.write((char)ch); }// while }catch (IOException e) { }finally { try { s.close(); }catch (IOException e) { } } } }
The usageOk() method is simply a utility method that verifies that the correct number of arguments have been passed to the client application. It outputs a help message if the number of arguments is not what is expected. It is generally a good idea to include a method like this in a Java application that uses command-line parameters.
The main() method does the real work of FileClient. After it verifies that it has the correct number of parameters, it attempts to create a socket connected to the server program running on the specified host and listening for connections on port number 1234. The socket that it creates is encapsulated by a Socket object. The constructor for the Socket object takes two arguments: the name of the machine the server program is running on and the port number. After the socket is successfully opened, the client sends the specified filename, followed by a new line character, to the server. The client then gets an InputStream from the socket to read what the server is sending and reads the success/failure code that the server sends back. If the request is a success, the client reads the contents of the requested file.
Note that the finally clause at the end closes the socket. If the program did not explicitly close the socket, it would be closed automatically when the program terminates. However, it is a good programming practice to explicitly close a socket when you are done with it.
Communicating with a connectionless protocol is simpler than using a connection-oriented protocol, as both the client and the server use DatagramSocket objects. The code for the server-side program has the following pattern:
On the client-side, the order is simply reversed:
Let's look at an example that shows how this pattern can be coded into a server that provides the current time and a client that requests the current time. Here's the code for the server class:
public class TimeServer { static DatagramSocket socket; public static void main(String[] argv) { try { socket = new DatagramSocket(7654); }catch (SocketException e) { System.err.println("Unable to create socket"); e.printStackTrace(); System.exit(1); } DatagramPacket datagram; datagram = new DatagramPacket(new byte[1], 1); while (true) { try { socket.receive(datagram); respond(datagram); }catch (IOException e) { e.printStackTrace(); } } } static void respond(DatagramPacket request) { ByteArrayOutputStream bs; bs = new ByteArrayOutputStream(); DataOutputStream ds = new DataOutputStream(bs); try { ds.writeLong(System.currentTimeMillis()); }catch (IOException e) { } DatagramPacket response; byte[] data = bs.toByteArray(); response = new DatagramPacket(data, data.length, request.getAddress(), request.getPort()); try { socket.send(response); }catch (IOException e) { // Give up, we've done our best. } } }
The main() method of the TimeServer class begins by creating a DatagramSocket object that uses port number 7654. The socket variable refers to this DatagramSocket, which is used to communicate with clients. Then the main() method creates a DatagramPacket object to contain data received by the DatagramSocket. The two-argument constructor for DatagramPacket creates objects that receive data. The first argument is an array of bytes to contain the data, while the second argument specifies the number of bytes to read. When a DatagramSocket is asked to receive a packet into a DatagramPacket, only the specified number of bytes are read. Even though the client is not really sending any information to the server, we still create a DatagramPacket with a 1-byte buffer. In theory, all that the server needs is an empty packet that specifies the client's network address and port number, but attempting to receive a zero-byte packet does not work. When the receive() method of a DatagramSocket is called to receive a zero-byte packet, it returns immediately, rather than waiting for a packet to arrive. Finally, the server enters an infinite loop that receives requests from clients using the receive() method of the DatagramSocket, and sends responses.
The respond() method handles sending responses. It starts by writing the current time as a long value to an array of bytes. Next, the respond() method prepares to send the array of bytes by creating a DatagramPacket object that encapsulates the array and the address and port number of the client that requested the time. Notice that the constructor used to create a DatagramPacket object for sending a packet takes four arguments: an array of bytes, the number of bytes to send, the client's network address, and the client's port number. The address and port are retrieved from the request DatagramPacket with the getAddress() and getPort() methods. The respond() method finishes its work by actually sending the DatagramPacket using the send() method of the DatagramSocket.
Now here's the code for the corresponding client program:
public class TimeClient { private static boolean usageOk(String[] argv) { if (argv.length != 1) { String msg = "usage is: " + "TimeClient server-name"; System.out.println(msg); return false; } return true; } public static void main(String[] argv) { if (!usageOk(argv)) System.exit(1); DatagramSocket socket; try { socket = new DatagramSocket(); }catch (SocketException e) { System.err.println("Unable to create socket"); e.printStackTrace(); System.exit(1); return; } long time; try { byte[] buf = new byte[1]; socket.send(new DatagramPacket(buf, 1, InetAddress.getByName(argv[0]), 7654)); DatagramPacket response = new DatagramPacket(new byte[8],8); socket.receive(response); ByteArrayInputStream bs; bs = new ByteArrayInputStream(response.getData()); DataInputStream ds = new DataInputStream(bs); time = ds.readLong(); }catch (IOException e) { e.printStackTrace(); System.exit(1); return; } System.out.println(new Date(time)); socket.close(); } }
The main() method does the real work of TimeClient. After it verifies that it has the correct number of parameters with usageOk(), it creates a DatagramSocket object for communicating with the server. Note that the constructor for this DatagramSocket does not specify any parameters; a client DatagramSocket is not explicitly connected to a specific port. Then the main() method creates a DatagramPacket object to contain the request to be sent to the server. Since this DatagramPacket is being used to send a packet, the code uses the four-argument constructor that specifies an array of bytes, the number of bytes to send, the specified network address for a time server, and the server's port number. The DatagramPacket is then sent to the server with the send() method of the DatagramSocket.
Now the main() method creates another DatagramPacket to receive the response from the server. The two-argument constructor is used this time because the object is being created to receive data. After calling the receive() method of the DatagramSocket to get the response from the server, the main() method gets the data from the response DatagramPacket by calling getData(). The data is wrapped in a DataInputStream so that the data can be read as a long value. If everything has gone smoothly, the client finishes by printing the current time and closing the socket.