Java package java.net
From EGEE-see WIki
Contents |
Basics
This is the basic Java blocking IO networking package – there’s nothing more low-level in Java. It is part of Java since the first version (JDK 1.0). Since version 1.4 non-blocking IO through buffers and channels is also integral part of standard Java distribution (but we are not going to go through non-blocking IO in this tutorial).
Package java.net can be divided into two parts: application layer and transport layer classes. TCP and UDP protocols (on top of IP) are fully supported so a complete freedom while writing network applications is guaranteed.
Application layer classes
This part includes higher level classes which enables working with URI and URL addresses and connections created using those addresses. The most important classes are URI, URL, URLConnection, and HttpURLConnection. These classes can be useful if for acquiring connections with a computer on Internet, if you want to parse HTML pages, create HTTP-based connections etc.
This part of the java.net package is all that is needed for a basic support for application layer in TCP/IP protocol – there is a huge number of third party classes on Internet which are not integral part of the standard Java distribution – some of them will help you send an email, some other will help you to contact some Web service etc.
Transport layer classes
This part of the java.net package is what distributed network programming is all about. This is the lowest level that Java allows somebody to work on while doing network programming (TCP & UDP) and what all other high level network and distributed programming Java toolkits are based on.
Basic three elements in this „low level“ part of java.net are:
- address
- socket
- interface
Address (IPv4, Ipv6) is being used as a identifier for network adapter in a network. Although two different classes exist in this package for IPv4 and IPv6 (INet4Address and INet6Address), a suggestion is to use base class InetAddress because Java will automatically adopt and use the right class so a programmer doesn’t have to worry about protocol version.
Interfaces are being used for querying network adapter and hardware for properties (through Java interface NetworkInterface). Some of things that can be found out are which is hardware (MAC) address of the network adapter, what is device’s MTU, which addresses are mapped on a network device etc.
Socket is, basically, standardized bidirectional communication mechanism. Socket class for TCP is Socket and socket class for UDP protocol called DatagramSocket. Both sides in UDP communication are with same rights (client/server paradigm is not completely meaningful in this case). UDP protocol is much simpler and usually being used for streaming protocols (audio & video network transmissions) because it is unsafe transportation mechanism. Being unsafe means that it is not a good choice in network programming so usually it's best to choose TCP as a network transportation protocol.
TCP server in Java – startup and shutdown
Basic classes around which TCP communication in java.net is based on are ServerSocket on server side and Socket on client side (although Socket appears also on the server side when connection is established with the client but we will return to this later).
So if communication in the application is based upon sockets, two sides in conversation are present: client and server. Server must „open“ or „occupy“ a port and if it succeeds (if there is no firewall which may forbid port opening and if there is no program that already uses that port) any client application (on a computer that has a network route to server) can connect to the server and initiate connection – keep in mind that this doesn't mean that every communication will be successful for the client that initiates the connection, it means only that client can connect to the server (for example, telnet program is one basic TCP program – it can connect with any TCP server, but only correctly written server programs can refuse connection immediately or at least after some time).
Openning a port
Which port to occupy? This is a completely reasonable question. If a computer where a program is being run is outside of controlled network (for example, home computer) this should not be a problem – Microsoft Windows XP with Service Pack 2, for example, will ask if program should be allowed to use a port at the very moment program tries to open that port – and that's all. If some other software firewall is being used the same kind of question will be asked.
Problems, though, can appear if a computer that should run the network program is in a controlled environment (university or company network) because there is always some hardware firewall put between that computer and the exterior network – you will need network administrator's permission to do something like that. There is a number of programs that can help you see all occupied ports on local machine; a simple, popular and freeware solution is Sysinternals TcpView.
When opening a port, a convention is not to use a port with numeric value below 1024 (maximum is 65535) because lower values are used by numerous standard protocols and sub-protocols. Server programs opens a port by constructing ServerSocket with integer argument or using a constructor without arguments and afterwards using bind method. The latter is a good thing to remember in situations when some TCP parameters should be set (SO_TIMEOUT for example), because those parameters cannot be set after port opening.
Simple example of port opening and shutdown is:
try {
ServerSocket listener = new ServerSocket(programPort);
// ...
}
catch (Exception ex) {
ex.printStackTrace();
}
finally {
try {
if (listener != null)
listener.close();
}
catch (Exception e) { }
}
This code shows not only how to open a port, but also a correct way to shutdown the connection – in the finally block through close() method call (and doing this on both sides of connection – client and server). Also, Socket allows flexible shutdown of one of two streams you can grab from a connection (for example a program only sends data to server, input stream can be can shut down through method call Socket.closeInputStream()).
Accepting client connections
As it is already said, from the moment a port is opened client can contact it by combining IP address and port to create a complete information needed for establishing a correct connection. Client can create Socket object which will be client’s end of the communication channel towards the server. Client can use that object to send data using output stream that it can get through getOutputStream() method call and also to receive data from the server by using input stream which can be obtained using getInputStream() method call (of course, both of these methods are elements of Socket).
Server on the other hand awaits inbound connections using accept()blocking method call of the server object – ServerSocket. This method is labeled as blocking because it blocks server’s active thread until some client tries to connect to that server. The result value of the accept() method call is a Socket which presents server’s side of the newly established connection. Server can now communicate with same data streams client can use (that’s because Socket is a bidirectional communication mechanism). It is a completely logical way of communication: what is written into server’s output stream client reads in its input stream and vice-versa. The moment something is written into an output stream on one side, other side immediately reads from its input stream (before that thread that reads data from input stream is in the blocked state).
What follows is a simple example of how to accept a connection and delegate (this means – give some object complete ownership of another object) Socket object to the class specifically created for that very usage.
package chat;
import java.net.*;
public class Server {
public static void main(String[] args) {
try {
// first we have to open a port
int port = Integer.parseInt(args[0]);
ServerSocket listener = new ServerSocket(port);
// now we are entering blocking state
// and wait for new connections
Socket client = listener.accept();
// a client initiated a connection – we are creating Chat object
// and delegate connection control to that object
Chat c = new Chat(client);
c.communicate();
// the moment communiqué is finished we close the connection
listener.close();
} catch (Exception ex) {
System.out.println(ex);
ex.printStackTrace();
}
}
}
The previous code presents following, very simple, protocol: server awaits connection initiation from a remote client and the moment connection has been established, an object that holds the implementation of the communication protocol inside it is created, or easier to say: send data to client and receive data back from it.
The interesting thing about Java network programming is that the code needed for opening a port (and effectively establishing a server) is very simple, which is a proof that basically Java network programming is built on simple concepts.
TCP client – contacting server and starting a conversation
When we program a client application we have to know server IP address and port server opened on that address (the number used when creating ServerSocket object). So, to create the client Socket object one needs to pass precisely these two things to the constructor – IP address and target socket.
If a server exists with those two parameters connection will be established or exception will be thrown because of the timeout or because there is no route defined towards the server. If a successful connection is established, client's Socket object will represent client's side of the connection (server's side is created as a result of the accept() method call of the ServerSocket object).
As it is already said, sockets are bidirectional communication mechanisms – server has one Socket object and client has another – and they talk to each other using data streams that can be acquired directly from the Socket objects.
package chat;
import java.net.*;
public class Client {
public static void main(String[] args) {
try {
// we are collection remote computer's information
int port = Integer.parseInt(args[1]);
String host = args[0];
// creating connection to the remote server having IP address
// „host“ and port number put in „port“
Socket server = new Socket(host, port);
// we created a connection! Now we will delegate connection
// to another object - Chat
Chat s = new Chat(server);
s.communicate();
// when we come to this point communication is finished
server.close();
} catch (Exception ex) {
System.out.println(ex);
ex.printStackTrace();
}
}
}
The symmetry between client and server is obvious which is a good things because logic used to write a server is similar to the logic needed for the client (of course, only in this example, servers are very often much more complex than clients). All we need to know when writing this low-level connection initiating code is: it connection is created, we will have usable Socket object – if something irregular happens, we will step into catch() code segment because of exception.
Simple client-server protocol
Protocol that will explained shortly is a simple small text messages transportation protocol, which simulate simple chat program. Object which hold complete protocol logic is Chat and it looks like this:
package chat;
import java.net.*;
public class Chat {
Socket client;
public Chat(Socket client) {
this.client = client;
}
public void communicate() {
try {
ReadThread read = new ReadThread(client);
WriteThread write = new WriteThread(client);
read.start();
write.start();
read.join();
// write.interrupt();
write.join();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
So, after Chat object accepts connection Socket, we call communicate() which delegates control further, but now to specialized threads – one will be used for sending data and another for accepting data from the remote computer. Chat awaits completion of the both threads before communicate() method is finished (which is, logically, a sign that communication is finished).
Input/output control
Both „chatters“ will use the command prompt for typing text messages (and for viewing them also). How are we going to enable transportation typed characters? We need input and output control which is integrated inside Java using System console streams System.in and System.out.
But, we first have to see how can we get Socket streams – those are the ones that allow data transportation through the network.
Input stream represents inbound data, data that we read which is sent from remote computer:
InputStream input = socket.getInputStream();
Output stream (we write in this stream what we expect remote computer will receive):
OutputStream output = socket.getOutputStream();
It is obvious that, by writing data into output stream, practically, data is being sent to the remote computer, and when data is being read from the input stream, practically, data is being sent from another computer. In case no data was sent by remote computer thread holding the statement which reads from Socket's input stream will be blocked (again, this is why this kind of IO is called blocked). This is the reason why it's really the best to take the reading from the main program thread and isolate inside another thread, so any kind of problems could be also isolated in that class, but also to enable functioning of the main thread (and thus not degrade GUI functionality or maybe another interaction paradigm being used to communicate with the user of the program). We are going to use this approach in this sample program – we will have independent thread to do our reading of the data (and also for writing just to be completely coherent).
Following code shows how to efficiently use input and output streams, how to control output to console after receiving and reading from the console before sending.
Let's see first the thread for reading data. After instantiation of the class and starting up of the Java thread which wraps our object we enter the main loop of our program – whenever some text is sent from the remote computer we immediately route it to the system console so the user of the program can immediately see text someone else has sent.
We will use buffer stream classes because we don't want to worry about flushing the buffer, we would prefer that Java takes care of that process. At this point we have to say that official Java documentation has much more information concerning streams and it would be a very good idea to read as much as possible, because knowing when is a good time to use a specific class is an extremely useful thing in network programming.
package chat;
import java.io.*;
import java.net.*;
public class ReadThread extends Thread {
Socket client;
InputStream in;
BufferedReader pin;
public ReadThread(Socket client) {
try {
this.client = client;
// acquiring the input stream
this.in = client.getInputStream();
// we will introduce here stream object which is very
// useful (BufferedReader) because it will help us read entire lines
pin = new BufferedReader(new InputStreamReader(in));
} catch (IOException ex) {
ex.printStackTrace();
}
}
public void run() {
try {
String s;
// everything we receive we will route to the
// system console immediately
while ((s = pin.readLine()) != null) {
System.out.println("> " + s);
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
// proper complete connection shutdown
try {
pin.close();
in.close();
client.close();
} catch (IOException ex1) {
ex1.printStackTrace();
}
}
}
}
Thread that takes care of writing data into output stream (or, easier to say, sending data) is written in such a way that it sends data immediately after user pressed ENTER button in the keyboard.
Sending is committed by writing typed character data into the output stream – other computer immediately “senses” that data is ready for receiving and it reads data from its input stream and outputs it to the console – and that is the entire communication process.
package chat;
import java.io.*;
import java.net.*;
public class WriteThread extends Thread {
Socket client;
OutputStream out;
PrintWriter pout;
public WriteThread(Socket client) {
try {
this.client = client;
this.out = client.getOutputStream();
pout = new PrintWriter(out, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
public void run() {
try {
// we are acquiring system console input stream
// as a source of data which will be sent
InputStream is = System.in;
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String s;
// we are reading one line at the time until everything is read
// or until some error occurs
while (!pout.checkError() && ((s = br.readLine()) != null)) {
pout.println(s);
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
pout.close();
out.close();
client.close();
} catch (IOException ex1) {
ex1.printStackTrace();
}
}
}
}
Unexpected situations
The thing that makes distributed application creation harder is the probability that something can go wrong – this probability is much higher in distributed applications that it is in normal applications because programs are loosely coupled; some problems which are often a LAN cable can be damaged or there can be a power failure on some network adapter which is only a mediator between two endpoints (router, switch, ISP server etc.), sometimes because of throughput problems we may encounter problems with late responses from the server (in that case program can receive data that was expected much earlier so it can make problems) and these are just more often problems, you may (and probably will) encounter something not mentioned here.
The good thing in Java is that it contains a complete exception hierarchy which wraps all possible error one may encounter – programmer only needs to know how to catch thrown exception and (which is much more important) not to allow your program to become unstable because of that exception, but to allow reconnection like nothing happened, or at least to inform the user what happened so user can make actions to solve the problem. As you may see, correct exception management is crucial in network programming. Java documentation (which completely corresponds with the JDK version you have) is a good start when one would like to make a strategy for exception-handling.
Every exception has its point of throwing (point of creation) and sometimes a large amount of time is needed to find it – that’s why a bottom-up approach of programming where work starts with the level closest to the socket management and then continues building up through the business logic of the application (which is the logical core of the application) towards the presentation layer (which may be either some kind of web framework like Struts, or classic GUI like Swing or maybe SWT) and handling layer–specific exceptions as soon as possible (and only informing upper levels that something irregular just happened). If layers are being combined ad-hoc a lot of problems will appear.
Remember: architectural problems may not be as easy to solve as simple problems, so you should spend more time planning architecture and dividing big use cases into smaller ones – that’s the way to keep implementation problems inside the layer that keeps them (and thus make development time more efficient).
Object serialization
One more thing deeply integrated into Java is serialization. Every object which implements interface java.io.Serializable is said to be exactly that – serializable. It means that that object can be represented like a stream of bytes which can completely describe it. This interface serves just as a sign to the Java compiler – it doesn’t have any methods in it.
Why is the serialization a good thing? Because it allows complete object representation (not only description, but also values of data inside the object) as a unique series of bytes and it allows, at the same time, to reinterpret original object using that serialized byte data.
Basically, because a specific kind of streams can be used to hold serialized objects’ data, you may save a serialized object into a file, and also (which is much more interesting for us, network programmers) send that stream of bytes to another computer, using TCP for example, so a program that accepts data can make a reverse action on data stream and create an object which will afterwards be in the same state as it was on the source side of the connection. But, keep in mind that the other side (acceptor’s side) must have a definition of the object so it can have information how to reinterpret the object byte stream (basically, if you wish to send instantiated MyClass object you need the same MyObject.class file on both side of the connection). One more thing which that should be kept in mind all the time is that serialization is a Java specific thing (non-portable) which is very dependable on JDK version (the same Java object may have completely different object stream signature in JDK 1.4 and JDK 6.0).
Object is serialized from memory into a byte stream using ObjectOutputStream class, and object of type ObjectInputStream is used to reinterpret object from a byte data stream. When these object streams are lined to the socket’s underlying data streams you will be able to “send objects” through the network – which is, you must agree, a very powerful programming technique (RMI uses serialization as a transport mechanism).
Simple example of how to send an object to a server:
MyObject msgObject = new MyObject (...);
try {
socket = new Socket(url, port);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(msgObject);
oos.flush();
}
catch(IOException e) {
...
}
finally {
...
}
And a simple on-thread server which can receive and reinterpret sent object is:
ServerSocket serverSocket = new ServerSocket(port);
Socket socket = null;
MyObject myObject = null;
try {
socket = serverSocket.accept();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Object obj = reader.readObject();
if (!(obj instanceof MyObject))
throw new RuntimeException("Received object is not supported");
else
myObject = (MyObject) obj;
doSomethingWithTheObject(myObject);
}
catch(Exception e) {
...
}
finally {
...
}
Distributed programming in Java: conventions
Good idea when constructing a server application in a classic client/server architecture (when multiple client connections towards a single server are needed) is to implement one-thread-per-connection technique, where every inbound connection is being delegated to a thread that will take care of every aspect of that connection independently from all other connections. Not only that entire processing is being encapsulated inside that thread, but also normal processing of the main server’s thread is being enabled because it can (after you delegate the connection to a independent thread) continue waiting for the next inbound connection, and thus not lose any time.
Programmer should think, though, about an efficient number of active thread at one time – if the server encounters heavy traffic it should stop enabling connections (maybe it’s better to deny a service than to give a bad service which may fail at any time).
One more thing programmer should be aware of is the logging process. It is almost impossible to imagine any kind of serious distributed programming without using some logger. Numeroud loggers are available in the market, in Java a wide freeware selection is present (Avalon, JDK>=1.4 has integrated logging), but a definite industry standard for years already is the Apache log4j logger because of its ease of use and wide specter of implemented abilities. Loggers are used primarily because debugging doesn’t help when dealing with distributed applications and also because it allows software specialists to see in details how application worked for example a day or two before when some error occurred. Often program use logging even after development – when they are put on production servers.
Finally, we should also say a couple of words concerning thread pools. Thread pools are extremely helpful because they help you to work with a number of threads without any risk of having a “ghost thread” (it’s a thread that is out of control), and also they help you to control all threads active in the program at every moment – new threads can be added to the pool and removed when they are not useful any more, schedule tasks can be added also (Java TimerTask is not a good solution) and a lot of other things. Since Java 5.0 a very powerful mechanism called Executor framework is integral part of standard Java – it is a very flexible collection of classes and powerful enough for most of real-life situations.
