>> Tutorial: How to create a simple client using Grizzly and get a complimentary server!
Contributed and maintained by Erik Svensson
Jan 2008 [Revision number: V1-2]
This publication is applicable to Grizzly 1.6.1 and 1.7.1
This document is meant to introduce the reader to how to write a client using the Grizzly framework. It also shows how to create a simple Echo-server using Grizzly pre-packaged components.
In the unlikely event you don't know what Grizzly is, see the Grizzly site (and why are you reading this?)
Before you proceed, make sure you review the requirements in this section, download the needed components and get a soda. This is meant to be fun!
Programming knowledge in java is mandatory (surprising no-one). Knowledge in and of ant is good. Some knowledge of java NIO makes things easier to understand.
Before you begin, you need to install the following software on your computer:
I try to be as IDE-independant as possible now, since IDE preferences seems to be like religion. So this will be built the old-fashioned way with ant and an editor.
Ok, lets get this show on the road. What we'll do now is creating the directory structure, moving the grizzly jar to the right place and creating the ant build file. It's easier if you follow my name conventions since those names are the ones in the build file I'll shortly give you. We do it in stages:
What we'll create here is really a timing client. It connects to a server and to that server it sends a packet containing a nanosecond timestamp. The server just returns the packet. When the client gets the timestamp back it creates a new timestamp and computes the difference between the two, thereby giving the roundtrip time. For the server we'll implement a simple EchoServer (of course using Grizzly (it's so simple,yay!)).
And here is the code. Don't despair seeing it this way, we'll go through it in detail, the line numbers just makes it easier to navigate the code. And of course, you can always just download it here.
1 package se.phlogiston.grizzly; 2 3 import com.sun.grizzly.CallbackHandler; 4 import com.sun.grizzly.Context; 5 import com.sun.grizzly.Controller; 6 import com.sun.grizzly.ControllerStateListenerAdapter; 7 import com.sun.grizzly.IOEvent; 8 import com.sun.grizzly.TCPConnectorHandler; 9 import com.sun.grizzly.TCPSelectorHandler; 10 import java.net.InetSocketAddress; 11 import java.nio.ByteBuffer; 12 import java.nio.channels.SelectionKey; 13 import java.util.Arrays; 14 import java.util.concurrent.CountDownLatch; 15 16 public class ExampleClient { 17 18 private String host; 19 private int port; 20 21 private TCPConnectorHandler connector_handler; 22 private Controller controller; 23 private TCPSelectorHandler tcp_selector_handler; 24 25 private ByteBuffer buf = ByteBuffer.allocate(100); 26 27 public static void main(String[] args) { 28 ExampleClient c = new ExampleClient(); 29 c.execute(args); 30 } 31 32 public void execute(String[] args) { 33 long elapsed = 0; 34 int repeats = 0; 35 final CountDownLatch started = new CountDownLatch(1); 36 37 for (int i = 0; i < args.length; i++) { 38 if (args[i].equals("-h")) { 39 host = args[++i]; 40 } else if (args[i].equals("-p")) { 41 port = Integer.parseInt(args[++i]); 42 } else if (args[i].equals("-r")) { 43 repeats = Integer.parseInt(args[++i]); 44 } 45 } 46 if (port == 0 ||host == null ) { 47 System.out.println("No port or host set"); 48 System.exit(1); 49 } 50 if (repeats == 0) { 51 System.out.println("No nr of repeats given. Will default to 10"); 52 repeats = 10; 53 } 54 55 controller = new Controller(); 56 tcp_selector_handler = new TCPSelectorHandler(true); 57 controller.addSelectorHandler(tcp_selector_handler); 58 59 controller.addStateListener(new ControllerStateListenerAdapter() { 60 61 public void onException(Throwable e) { 62 System.out.println("Grizzly controller exception:"+e.getMessage()); 63 } 64 65 public void onReady() { 66 System.out.println("Ready!"); 67 started.countDown(); 68 } 69 70 }); 71 72 new Thread(controller).start(); 73 try { 74 started.await(); 75 } catch(Exception e) { 76 System.out.println("Timeout in wait"+e.getMessage()); 77 } 78 79 connector_handler =(TCPConnectorHandler) controller.acquireConnectorHandler(Controller.Protocol.TCP); 80 81 try { 82 byte[] filler = new byte[92]; 83 Arrays.fill(filler,(byte)2); 84 85 connector_handler.connect(new InetSocketAddress(host,port), 86 new CallbackHandler<Context>() { 87 88 public void onConnect(IOEvent<Context> e) { 89 SelectionKey k = e.attachment().getSelectionKey(); 90 System.out.println("Callbackhandler: OnConnect..."); 91 try { 92 connector_handler.finishConnect(k); 93 } catch(Exception ex) { 94 System.out.println("exception in CallbackHandler:"+ex.getMessage()); 95 } 96 e.attachment().getSelectorHandler().register(k,SelectionKey.OP_READ); 97 } 98 99 public void onRead(IOEvent<Context> e){} 100 101 public void onWrite(IOEvent<Context> e){} 102 103 }); 104 105 int ctr = 0; 106 while (ctr < repeats) { 107 buf.putLong(System.nanoTime()); 108 buf.put(filler); 109 buf.flip(); 110 long size = connector_handler.write(buf,true); 111 buf.clear(); 112 connector_handler.read(buf,true); 113 elapsed += System.nanoTime() - buf.getLong(); 114 buf.clear(); 115 ctr++; 116 } 117 System.out.println(""+repeats+" run at a total of "+elapsed/1000+" microseconds. Per packet it comes down to "+(elapsed/repeats)/1000+ 118 " microseconds per roundtrip."); 119 connector_handler.close(); 120 controller.stop(); 121 } catch(Exception e) { 122 System.out.println("Exception in execute..."+e); 123 e.printStackTrace(System.out); 124 } 125 } 126 }
Writing a client in Grizzly is centered round the Controller. To quote the JavaDoc:
A Controller is composed of Handlers, ProtocolChain and Pipeline.
It's the Controller that is our interface to Grizzly.
21-23 We declare our Controller, TCPConnectorHandler and our TCPSelectorHandler.
35 We create a CountDownLatch that we'll use for insuring that we don't use our Controller before it's ready.
55 We create the Controller. As of yet it's not of much use as we need to configure it.
56 Now we create the TCPSeletorHandler. By calling the constructor TCPSelectorHandler(boolean) with 'true' we create a client. Otherwise the TCPSelectorHandler creates a ServerSocketChannel and binds to a port, creating a server.
57 We add our newly created TCPSelectorHandler to the controller. We need to create the TCPSelectorHandler ex plicitly so that we can call the correct constructor. The Controller will create a TCPSelectorHandler if none has been registered with the the Controller when it is started. That one will create a server socket however and that's not what we want.
59 - 70 We create a state listener callback and add it to the controller. The ControllerStateListenerAdapter implements the ControllerStateListener interface and also provides hooks for onRead() and onWrite (see here). We use onReady hook to wait for the controller to be ready to proceess connections.
65 The onReady hook is called and we decrement the CountDownLatch with one. Since we started it with a counter value of '1', this means that the thread waiting at line 68 now is free to continue on.
72 Controller implements Runnable so we give it its own thread and start it.
74 The Controller needs to do some housekeeping, starting some threads and such, and we need to wait for the Controller to be finished. Before that it can't service our request for a connection. So here we await().
79 Now we need a ConnectorHandler to be use for the actual connection. We get the ConnectorHandler by calling Controller.acquireConnectorHandler() with the desired protocol as parameter. As you can see we use Connector.Protocol.TCP. The Connector.Protolcol is an enumeration declared in the Controller. Other protocols availble are UDP, TLS (Transport Layer Security) and Custom.
82 - 83 When I wrote this I was creating a test tool for network and server latency. To get good results I had to pad the outgoing messages to put some strain on the system. So we create a filler of 92 bytes and fill it up with the number 2 (choosen for no particular reason).
85 - 103 Now we connect to the server. We call ConnectorHandler.connect() with the desired host and port and we also provide a callback. In this example we don't really have to write the callback but we do it just to show how it's done. The callback is meant to be used when you want to be notified of a successful connection, a read or a write. A possible reason for writing one might be that you want to be notified of the successful connection (if, for instance, you connect to someone that expects a login message directly on connection) you write a CallbackHandler<Context>. The CallbackHandler is an interface so you have to implement all three methods. I'm only interested in the onConnect() method so the others are empty. What needs to be done is to call finishConnect() with our selectionKey and then associate the selection key with OP_READ and that's it.
You could also replace the entire thing with a
connector_handler.connect(new InetSocketAddress(host,port));As of 1.7 this is done for you by the Controller in the what should be the most common case where you just want to connect and you can do as shown above.
Haven't forgotten that. You can get the source for the server here. Make sure you download it to [PROJECTHOME]. And don't rename it.
We need to build our client and server before we can actually test it. The ant build file I've prepared for you (and I hope you have downloaded) will build both the client and server for you. Just type 'ant' in the [PROJECTHOME] directory and ant will create a jar file with all the needed classes.
In the ant build file I also placed targets to run both the client and the server. Make sure you have two terminal windows, one for the client and one for the server. It should come as no surprise that we need to start the server first.
In the [PROJECTHOME] directory write 'ant run-server'. The server is default set to listen to port 8899 (say one thing for Erik, say he's lacking imagination). If you need/want to change this, open build.xml in your favourite editor and change the '-p' parameter. Now the server is running. Now for the client!
Once again in the [PROJECTHOME] directory, write 'ant run-client'. The client attempts to connect to localhost (127.0.0.1) at port 8899. If you feel the need to change this, edit build.xml and change the arg line element.
The client will by default send 10 messages to the server and calculate the average time. If you want more the 10 repeats add an extra '-r (repeats)' to the line arg element in the client target.
What should pop out is the roundtrip time between the server and the client. I originally wrote this (a bit different I admin but in all specifics the same) to measure the speed of the grizzly server.
We've written a simple echo client using grizzly, going through the various steps needed to create a client. I've also shown an slightly easier way to create the client eliminating the need for at least one callback. I've shown how to create a server using only the classes included in grizzly.
From here you could develop the client to have multiple connections, perhaps using UDP instead of TCP. Apart from that there might not be all that much to do with the client. The server on the other hand... From here on, take a look at the chaining of the ProtocolFilters. There is much interesting goodness there.