I was going through some of the older parts of my darcs repository the other day when I stumbled upon a pretty neat and powerful little remoting library that I wrote back in 2001. It is based on dynamic proxies and plain sockets, almost legacy programming techniques nowadays in a world of BCI (bytecode instrumentation), AOP, EJBs, Terracotta etc.
Anyway, I thought that the implementation is fairly interesting (and useful) and would make good blog post.
It basically implements a remote proxy, that can either be instantiated by the client or instantiated by the server and sent to the client. In either case, when methods are invoked upon the proxy then they are executed on the server. The communication is socket-based and the server is holding a resizable thread pool that can grow and shrink based on usage. The beauty of using dynamic proxies is that once the proxy has been instantiated, the RPC is transparent, e.g. pretty much the same as with RMI but without all the stub and skeleton mess. Is a very simple library, not meant as a full RMI replacement, but does its job pretty well. I have used it among other things for:
So without further ado, let's dive into some code. First, and most importantly, would be to take a look at how the remote proxy can be used from a client's perspective.
This simple unit test is testing (and highlights) two basic features.
Create and use a client side remote proxy (that should create and use a matching instance on the server).
Let the server create a proxy and send it to the client which uses it.
publicvoidtestSimpleProxy(){// 1)
// creates a new remote proxy for the POJOImpl1 class
// which maps to an instance of this class on the server
RemoteProxyproxy1=RemoteProxy.createClientProxy(newString[]{"test.POJO1"},// interface(s)
"test.POJOImpl1",// implementation
"localhost",// server IP or hostname
6663// server port
);// retrieves the POJOImpl1 instance
POJO1pojo1=(POJO1)proxy1.getInstance();// 2)
// invoke a method on the proxy (executed on the server)
assertEquals("POJO1 here",pojo1.test());// 3)
// retrieve the proxy that is created on the server
RemoteProxyproxy2=pojo1.getPOJO2();// retrieves the POJOImpl2 instance
POJO2pojo2=(POJO2)proxy2.getInstance();// 4)
// invoke a method on the proxy (executed on the server)
assertEquals("POJO2 here",pojo2.test());// 5)
// close the proxies (close() must always be called)
proxy1.close();proxy2.close();}
That was easy. So much for the client side.
How can we now create the server that serves these two proxies? The only thing we have to do is to create a RemoteProxyServer by passing in the class loader that we want to use to instantiate our proxied objects as well as an implementation of the Invoker interface (which has one single method called invoke), an interface that gives you the possibility to invoke methods on your proxied objects any way you want. This example simply shows the most basic way of doing it:
// create a remote proxy server with a simple Invoker impl
RemoteProxyServerremoteProxyServer=newRemoteProxyServer(classLoader,returnnewInvoker(){publicObjectinvoke(Stringhandle,StringmethodName,Class[]paramTypes,Object[]args,Objectcontext){Objectresult;try{Objectinstance=RemoteProxy.getWrappedInstance(handle);Methodmethod=instance.getClass().getMethod(methodName,paramTypes);result=method.invoke(instance,args);}catch(Exceptione){thrownewWrappedRuntimeException(e);}returnresult;}};);// start all server threads
remoteProxyServer.start();
Let's now dive into the implementation of the server a little bit. When we invoke remoteProxyServer.start() then the server starts up X worker threads (managed by a thread pool). The work done by of one of these thread is roughly - in pseudo code:
Get the object output and input streams
Loop:: read from input stream
if command == CREATE: create an instance on the server and send a handle to the output stream
else if command == INVOKE: grab the parameters, invoke the method and send the result to the output stream
else if command == CLOSE: exit the thread
Here are some code excerpts, highlighting the algorithm:
publicvoidrun(){try{m_socket.setTcpNoDelay(true);m_socket.setSoTimeout(m_timeout);m_in=newObjectInputStream(m_socket.getInputStream());m_out=newObjectOutputStream(m_socket.getOutputStream());}catch(IOExceptione){thrownewWrappedRuntimeException(e);}while(m_running){try{switch(m_in.read()){caseCommand.CREATE:handleCreateCommand();break;caseCommand.INVOKE:handleInvocationCommand();break;caseCommand.CLOSE:m_running=false;break;default:break;}}catch(Exceptione){close();thrownewWrappedRuntimeException(e);}}close();}privatevoidhandleCreateCommand()throwsIOException,ClassNotFoundException,InstantiationException,IllegalAccessException{StringclassName=(String)m_in.readObject();Classklass=Class.forName(className,false,m_loader);Objectinstance=klass.newInstance();// get a handle to the proxied instance
Stringhandle=RemoteProxy.wrapInstance(instance);m_out.writeObject(handle);m_out.flush();}privatevoidhandleInvocationCommand()throwsIOException,ClassNotFoundException{Objectcontext=m_in.readObject();Stringhandle=(String)m_in.readObject();StringmethodName=(String)m_in.readObject();Class[]paramTypes=(Class[])m_in.readObject();Object[]args=(Object[])m_in.readObject();Objectresult=null;try{result=m_invoker.invoke(handle,methodName,paramTypes,args,context);}catch(Exceptione){result=e;// pass the exception to the client
}m_out.writeObject(result);m_out.flush();}
If the client asks for a server created proxy, then the server would create it like this: RemoteProxy proxy = RemoteProxy.createServerProxy(myInstance, "localhost", 6663);, and write it to the object output stream - or even more simple have one of the already proxied objects create it (on the server) and return it (to the client).
This is pretty much the whole server. But if you're still with me, I'm sure you're eager to know what the RemoteProxy looks like. Well, here are some of the more interesting parts of its internals:
publicclassRemoteProxyimplementsInvocationHandler,Serializable{...// constructors and field declarations are omitted
/**
* Creates a new proxy to a class. To be used on the client side to
* create a new proxy to an object.
*
* @param interfaces the name of the interfaces for the object to create the proxy for
* @param impl the name of the the impl class to create the proxy for
* @param address the address to connect to
* @param port the port to connect to
* @param ctx the context carrying the users principals and credentials
* @param loader the class loader to use for instantiating the proxy
* @return the new remote proxy instance
*/publicstaticRemoteProxycreateClientProxy(String[]interfaces,Stringimpl,Stringaddress,intport,Objectcontext,ClassLoaderloader){returnnewRemoteProxy(interfaces,impl,address,port,context,loader);}...// some factory methods are omitted
/**
* Look up and retrives a proxy to an object from the server.
*
* @return the proxy instance
*/publicObjectgetInstance(){if(m_proxy!=null){returnm_proxy;}if(m_loader==null){m_loader=Thread.currentThread().getContextClassLoader();}try{m_socket=newSocket(InetAddress.getByName(m_address),m_port);m_socket.setTcpNoDelay(true);m_out=newObjectOutputStream(m_socket.getOutputStream());m_in=newObjectInputStream(m_socket.getInputStream());}catch(Exceptione){thrownewWrappedRuntimeException(e);}if(m_handle==null){// is a client side proxy
if(m_targetInterfaceNames==null){thrownewIllegalStateException("interface class name can not be null");}if(m_targetImplName==null){thrownewIllegalStateException("implementation class name can not be null");}try{// create a new instance on the server and get the handle to it in return
m_out.write(Command.CREATE);m_out.writeObject(m_targetImplName);m_out.flush();m_handle=(String)m_in.readObject();m_targetInterfaces=newClass[m_targetInterfaceNames.length];for(inti=0;i<m_targetInterfaceNames.length;i++){try{m_targetInterfaces[i]=Class.forName(m_targetInterfaceNames[i],false,m_loader);}catch(ClassNotFoundExceptione){thrownewWrappedRuntimeException(e);}}}catch(Exceptione){thrownewWrappedRuntimeException(e);}}// create and return a regular Java Dynamic Proxy
m_proxy=Proxy.newProxyInstance(m_loader,m_targetInterfaces,this);returnm_proxy;}/**
* This method is invoked automatically by the proxy. Should not be called directly.
*
* @param proxy the proxy instance that the method was invoked on
* @param method the Method instance corresponding to the interface method invoked
* on the proxy instance.
* @param args an array of objects containing the values of the arguments passed
* in the method invocation on the proxy instance.
* @return the value to return from the method invocation on the proxy instance.
*/publicObjectinvoke(Objectproxy,Methodmethod,Object[]args){try{m_out.write(Command.INVOKE);m_out.writeObject(m_context);m_out.writeObject(m_handle);m_out.writeObject(method.getName());m_out.writeObject(method.getParameterTypes());m_out.writeObject(args);m_out.flush();finalObjectresponse=m_in.readObject();if(responseinstanceofException){throw(Exception)response;}returnresponse;}catch(Exceptione){thrownewWrappedRuntimeException(e);}}/**
* Closes the proxy and the connection to the server.
*/publicvoidclose(){try{m_out.write(Command.CLOSE);m_out.flush();m_out.close();m_in.close();m_socket.close();}catch(IOExceptione){thrownewWrappedRuntimeException(e);}}}
That's all there is to it.
Ok, perhaps not as powerful and flexible as Spring Remoting, EJBs, RMI or Terracotta's Network-Attached Memory.
But plain sockets and DPs are not so bad either. Or is it just me that's being a bit sentimental? At least it was pretty cool stuff back in 2001 :-)
If you want to take a look at the code or are thinking of using it then you can check it out using this command (if you don't have darcs installed you can get it here):
darcs get http://jonasboner.com/darcs/remoteproxy
Happy hacking.