Over the last 10-15 years web services aka text (XML/JSON) over HTTP/HTTPS has become the standard for client server communication.
But before that binary RPC (Remote Procedure Call) protocols were widely used.
And when very high performance is needed then binary RPC protocols can still be relevant. Web services will typical max out around 50000-100000 requests per minute per node while binary RPC protocols often can do 1-2 million requests per minute per node.
Binary RPC protocols has several disadvantages though:
Java RMI (Remote Method Invocation) has been part of Java since version 1.1 (1997).
The concept is very simple:
Deployment diagram:
Class diagram:
All calls hit a single instance of the server object.
Threading is implementation specific, but typically a thread is started for each request received.
Data class:
package rmi.common;
import java.io.Serializable;
public class Data implements Serializable {
private static final long serialVersionUID = 1L;
private int iv;
private String sv;
public Data() {
this(0, "");
}
public Data(int iv, String sv) {
this.iv = iv;
this.sv = sv;
}
public int getIv() {
return iv;
}
public void setIv(int iv) {
this.iv = iv;
}
public String getSv() {
return sv;
}
public void setSv(String sv) {
this.sv = sv;
}
}
Note that the class is Serializable.
Interface shared by client stub and server implementation:
package rmi.common;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Test extends Remote {
public int add(int a, int b) throws RemoteException;
public String dup(String s) throws RemoteException;
public Data process(Data d) throws RemoteException;
public int getCounter() throws RemoteException;
public void noop() throws RemoteException;
}
Note that the interface extends Remote and all methods throw RemoteException.
Server implementation:
package rmi.server;
import rmi.common.Data;
import rmi.common.Test;
public class TestImpl implements Test {
private int counter = 0;
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public String dup(String s) {
return s + s;
}
public Data process(Data d) {
return new Data(d.getIv() + 1, d.getSv() + "X");
}
@Override
public int getCounter() {
counter++;
return counter;
}
@Override
public void noop() {
// nothing
}
}
Server main:
package rmi.server;
import java.rmi.AccessException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class Server {
private static final int REGISTRY_PORT = 1099;
private static final String NAME = "Test";
private static final int SERVER_PORT = 12345;
public static void main(String[] args) throws AccessException, RemoteException {
LocateRegistry.createRegistry(REGISTRY_PORT).rebind(NAME, UnicastRemoteObject.exportObject(new TestImpl(), SERVER_PORT));
}
}
Test client:
package rmi.client;
import java.rmi.AccessException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.stream.IntStream;
import rmi.common.Data;
import rmi.common.Test;
public class Client {
private static final String REGISTRY = "localhost";
private static final int REGISTRY_PORT = 1099;
private static final String NAME = "Test";
private static void testFunctional() throws AccessException, RemoteException, NotBoundException {
Test tst = (Test)LocateRegistry.getRegistry(REGISTRY, REGISTRY_PORT).lookup(NAME);
int a = 123;
int b = 456;
int c = tst.add(a, b);
System.out.println(c);;
String s = "ABC";
String s2 = tst.dup(s);
System.out.println(s2);
Data d = new Data(123, "ABC");
Data d2 = tst.process(d);
System.out.printf("%d %s\n", d2.getIv(), d2.getSv());
}
private static void testInstantiation() throws AccessException, RemoteException, NotBoundException {
Test tst1 = (Test)LocateRegistry.getRegistry(REGISTRY, REGISTRY_PORT).lookup(NAME);
for(int i = 0; i < 2; i++) {
int n = tst1.getCounter();
System.out.println(n);
}
Test tst2 = (Test)LocateRegistry.getRegistry(REGISTRY, REGISTRY_PORT).lookup(NAME);
for(int i = 0; i < 2; i++) {
int n = tst2.getCounter();
System.out.println(n);
}
}
private static final int REP = 100000;
private static void testPerformance() throws AccessException, RemoteException, NotBoundException {
Test tst = (Test)LocateRegistry.getRegistry(REGISTRY, REGISTRY_PORT).lookup(NAME);
long t1 = System.currentTimeMillis();
IntStream.range(0, REP).parallel().forEach(i -> { try { tst.noop(); } catch(RemoteException re) { } });
long t2 = System.currentTimeMillis();
System.out.printf("%d requests per second\n", REP * 1000 / (t2 - t1));
}
public static void main(String[] args) throws AccessException, RemoteException, NotBoundException {
testFunctional();
testInstantiation();
testPerformance();
}
}
The default network protocol used is JRMP (Java Remote Method Protocol). But it is possible to use IIOP (Internet InterORB Protocol).
RMIRegistry can be run as a standalone utility (Java comes with such), but in the example the server creates the RMIRegistry itself - that is easier in my opinion.
In old Java versions it was necessary to generate both stub and skeleton upfront with the rmic tool. That is no longer necessary.
Many older RMI examples use dynamic download and load of code and therefore required the use of a security manager. That feature is not relevant today in my opinion.
EJB's was introduced in J2EE 1.2 (1999).
EJB's in version 1.x and 2.x are complex:
Deployment diagram:
Class diagram:
I think the class diagram nicely illustrate the complexity.
EJB's in version 1.x and 2.x comes in 3 flavors:
Session beans are used for RPC calls.
Session beans comes in two flavors:
A stateless session bean can actually keep state. But the state is difficult to use in any way, because calls within a session may hit different objects.
EJB's in version 1.x and 2.x have up to 4 interfaces:
Home interfaces for session beans are just a single create method with no parameters. But home interface for entity beans can be complex.
The local stuff is out of scope for this artcile.
To further muddy the waters then the actual bean class has to implement all methods from remote and local interface, but are prohibited from actually declaring implementing the interfaces.
The EJB container in the application server has a bean object pool to handle all requests. Beans are reused but each bean instance only serve one request at a time.
Stateless session bean => All calls hit a random instance of the bean.
Stateful session bean => at session start the client get assigned a random instance of the bean and all calls within session hit that instance.
The EJB container in the application server has a thread pool to handle remote EJB requests. The size of the thread pool is defined in the application server configuration.
Data class:
package ejb2.common;
import java.io.Serializable;
public class Data implements Serializable {
private static final long serialVersionUID = 1L;
private int iv;
private String sv;
public Data() {
this(0, "");
}
public Data(int iv, String sv) {
this.iv = iv;
this.sv = sv;
}
public int getIv() {
return iv;
}
public void setIv(int iv) {
this.iv = iv;
}
public String getSv() {
return sv;
}
public void setSv(String sv) {
this.sv = sv;
}
}
Note that it is serializable.
Remote interface:
package ejb2.common;
import java.rmi.RemoteException;
import javax.ejb.EJBObject;
public interface Test extends EJBObject {
public int add(int a, int b) throws RemoteException;
public String dup(String s) throws RemoteException;
public Data process(Data d) throws RemoteException;
public int getCounter() throws RemoteException;
public void noop() throws RemoteException;
}
Note that it extends EJBObject and all methods throw RemoteException.
Remote home interface:
package ejb2.common;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
public interface TestHome extends EJBHome {
public Test create() throws CreateException, RemoteException;
}
Note that it extends EJBHome.
EJB bean implementation class:
package ejb2.server;
import javax.ejb.CreateException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import ejb2.common.Data;
public class TestBean implements SessionBean {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unused")
private SessionContext sessionContext;
private int counter = 0;
public int add(int a, int b) {
return a + b;
}
public String dup(String s) {
return s + s;
}
public Data process(Data d) {
return new Data(d.getIv() + 1, d.getSv() + "X");
}
public int getCounter() {
counter++;
return counter;
}
public void noop() {
// nothing
}
// standard methods:
public void ejbCreate() throws CreateException {
}
public void ejbRemove() {
}
public void ejbActivate() {
}
public void ejbPassivate() {
}
public void setSessionContext(SessionContext sessionContext) {
this.sessionContext = sessionContext;
}
}
Note that it extends SessionBean.
ejb-jar.xml (EJB deployment descriptor) defining the two EJB's:
<ejb-jar version="3.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
<enterprise-beans>
<session>
<ejb-name>TestStateless2</ejb-name>
<home>ejb2.common.TestHome</home>
<remote>ejb2.common.Test</remote>
<ejb-class>ejb2.server.TestBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
<session>
<ejb-name>TestStateful2</ejb-name>
<home>ejb2.common.TestHome</home>
<remote>ejb2.common.Test</remote>
<ejb-class>ejb2.server.TestBean</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>TestStateless2</ejb-name>
<method-name>*</method-name>
</method>
<method>
<ejb-name>TestStateful2</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>NotSupported</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
This define names, stateless/stateful, interfaces, bean class, transactions etc..
Client:
package ejb2.client;
import java.rmi.RemoteException;
import java.util.Hashtable;
import java.util.stream.IntStream;
import javax.ejb.CreateException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
import ejb2.common.Data;
import ejb2.common.Test;
import ejb2.common.TestHome;
public class Client {
// WildFly/JBoss specific:
private static final String PREFIX = "org.jboss.ejb.client.naming";
private static final String CTXFAC = "org.jboss.naming.remote.client.InitialContextFactory";
private static final String URL = "http-remoting://localhost:8080";
private static final String NAME1 = "ejb/test-ejb2/TestStateless2!ejb2.common.TestHome";
private static final String NAME2 = "ejb/test-ejb2/TestStateful2!ejb2.common.TestHome";
static {
Hashtable<String,Object> props = new Hashtable<>();
props.put(Context.URL_PKG_PREFIXES, PREFIX);
props.put(Context.INITIAL_CONTEXT_FACTORY, CTXFAC);
props.put(Context.PROVIDER_URL, URL);
props.put("jboss.naming.client.ejb.context", true);
props.put("org.jboss.ejb.client.scoped.context", true);
try {
ctx = new InitialContext(props);
} catch (NamingException e) {
e.printStackTrace();
}
}
//
private static Context ctx;
private static void testFunctional(String name) throws NamingException, RemoteException, CreateException {
TestHome home = (TestHome)PortableRemoteObject.narrow(ctx.lookup(name), TestHome.class);
Test tst = home.create();
int a = 123;
int b = 456;
int c = tst.add(a, b);
System.out.println(c);;
String s = "ABC";
String s2 = tst.dup(s);
System.out.println(s2);
Data d = new Data(123, "ABC");
Data d2 = tst.process(d);
System.out.printf("%d %s\n", d2.getIv(), d2.getSv());
}
private static void testInstantiation(String name) throws NamingException, RemoteException, CreateException {
TestHome home = (TestHome)PortableRemoteObject.narrow(ctx.lookup(name), TestHome.class);
Test tst1 = home.create();
for(int i = 0; i < 2; i++) {
int n = tst1.getCounter();
System.out.println(n);
}
Test tst2 = home.create();
for(int i = 0; i < 2; i++) {
int n = tst2.getCounter();
System.out.println(n);
}
}
private static final int REP = 100000;
private static void testPerformance(String name) throws NamingException, RemoteException, CreateException {
TestHome home = (TestHome)PortableRemoteObject.narrow(ctx.lookup(name), TestHome.class);
Test tst = home.create();
long t1 = System.currentTimeMillis();
IntStream.range(0, REP).parallel().forEach(i -> { try { tst.noop(); } catch(RemoteException ex) { } });
long t2 = System.currentTimeMillis();
System.out.printf("%d requests per second\n", REP * 1000 / (t2 - t1));
}
public static void test(String label, String name) throws NamingException, RemoteException, CreateException {
System.out.println(label + ":");
testFunctional(name);
testInstantiation(name);
testPerformance(name);
}
public static void main(String[] args) throws NamingException, RemoteException, CreateException {
test("Stateless", NAME1);
test("Stateful", NAME2);
}
}
To make the home object create method work for WildFly that is intended for EJB 3.x I had to add a jboss-ejb-client.properties file with:
endpoint.name = client-endpoint remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED = false remote.connections = default remote.connection.default.host = localhost remote.connection.default.port = 8080
EJB's have advanced transaction capabilities. The example shown have disabled transaction support for the methods. But it is common to define transactions for all methods. Furthermore session beans can do both CMT (Container Managed Transactions) and BMT (Bean Managed Transactions).
The network protocol for remote EJB calls is RMI over IIOP.
EJB 3.x got introduced with Java EE 6 (2009).
EJB 3.x is relative simple:
Deployment diagram:
Class diagram:
EJB 3.x was a major simplification compared to EJB 1.x/2.x:
The EJB container in the application server has a bean object pool to handle all requests. Beans are reused but each bean instance only serve one request at a time.
Stateless session bean => All calls hit a random instance of the bean.
Stateful session bean => at session start the client get assigned a random instance of the bean and all calls within session hit that instance.
The EJB container in the application server has a thread pool to handle remote EJB requests. The size of the thread pool is defined in the application server configuration.
Data class:
package ejb3.common;
import java.io.Serializable;
public class Data implements Serializable {
private static final long serialVersionUID = 1L;
private int iv;
private String sv;
public Data() {
this(0, "");
}
public Data(int iv, String sv) {
this.iv = iv;
this.sv = sv;
}
public int getIv() {
return iv;
}
public void setIv(int iv) {
this.iv = iv;
}
public String getSv() {
return sv;
}
public void setSv(String sv) {
this.sv = sv;
}
}
Note that it is serializable.
Remote interface:
package ejb3.common;
public interface Test {
public int add(int a, int b);
public String dup(String s);
public Data process(Data d);
public int getCounter();
public void noop();
}
Note no annotations.
Stateless EJB bean:
package ejb3.server;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import ejb3.common.Data;
import ejb3.common.Test;
@Remote(Test.class)
@Stateless
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class TestStateless implements Test {
private int counter = 0;
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public String dup(String s) {
return s + s;
}
public Data process(Data d) {
return new Data(d.getIv() + 1, d.getSv() + "X");
}
@Override
public int getCounter() {
counter++;
return counter;
}
@Override
public void noop() {
// nothing
}
}
@Remote define the remote interface. @Stateless declare it as a stateless EJB (pleae see definition in previous section).
Stateful EJB bean:
package ejb3.server;
import javax.ejb.Remote;
import javax.ejb.Stateful;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import ejb3.common.Data;
import ejb3.common.Test;
@Remote(Test.class)
@Stateful
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class TestStateful implements Test {
private int counter = 0;
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public String dup(String s) {
return s + s;
}
public Data process(Data d) {
return new Data(d.getIv() + 1, d.getSv() + "X");
}
@Override
public int getCounter() {
counter++;
return counter;
}
@Override
public void noop() {
// nothing
}
}
@Remote define the remote interface. @Stateful declare it as a stateful EJB (pleae see definition in previous section).
Client:
package ejb3.client;
import java.util.Hashtable;
import java.util.stream.IntStream;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import ejb3.common.Data;
import ejb3.common.Test;
public class Client {
// WildFly/JBoss specific:
private static final String PREFIX = "org.jboss.ejb.client.naming";
private static final String CTXFAC = "org.jboss.naming.remote.client.InitialContextFactory";
private static final String URL = "http-remoting://localhost:8080";
private static final String NAME1 = "ejb/test-ejb3/TestStateless!ejb3.common.Test";
private static final String NAME2 = "ejb/test-ejb3/TestStateful!ejb3.common.Test";
static {
Hashtable<String,Object> props = new Hashtable<>();
props.put(Context.URL_PKG_PREFIXES, PREFIX);
props.put(Context.INITIAL_CONTEXT_FACTORY, CTXFAC);
props.put(Context.PROVIDER_URL, URL);
props.put("jboss.naming.client.ejb.context", true);
try {
ctx = new InitialContext(props);
} catch (NamingException e) {
e.printStackTrace();
}
}
//
private static Context ctx;
private static void testFunctional(String name) throws NamingException {
Test tst = (Test)ctx.lookup(name);
int a = 123;
int b = 456;
int c = tst.add(a, b);
System.out.println(c);;
String s = "ABC";
String s2 = tst.dup(s);
System.out.println(s2);
Data d = new Data(123, "ABC");
Data d2 = tst.process(d);
System.out.printf("%d %s\n", d2.getIv(), d2.getSv());
}
private static void testInstantiation(String name) throws NamingException {
Test tst1 = (Test)ctx.lookup(name);
for(int i = 0; i < 2; i++) {
int n = tst1.getCounter();
System.out.println(n);
}
Test tst2 = (Test)ctx.lookup(name);
for(int i = 0; i < 2; i++) {
int n = tst2.getCounter();
System.out.println(n);
}
}
private static final int REP = 100000;
private static void testPerformance(String name) throws NamingException {
Test tst = (Test)ctx.lookup(name);
long t1 = System.currentTimeMillis();
IntStream.range(0, REP).parallel().forEach(i -> { tst.noop(); });
long t2 = System.currentTimeMillis();
System.out.printf("%d requests per second\n", REP * 1000 / (t2 - t1));
}
public static void test(String label, String name) throws NamingException {
System.out.println(label + ":");
testFunctional(name);
testInstantiation(name);
testPerformance(name);
}
public static void main(String[] args) throws NamingException {
test("Stateless", NAME1);
test("Stateful", NAME2);
}
}
EJB's have advanced transaction capabilities. The example shown have disabled transaction support for the methods. But it is common to define transactions for all methods. Furthermore session beans can do both CMT (Container Managed Transactions) and BMT (Bean Managed Transactions).
There are more about EJB's and transactions here.
The network protocol for remote EJB calls is RMI over IIOP.
.NET remoting has been part of .NET since version 1.0 (2002).
It is sort of superseeded by WCF.
The concept is very simple:
Deployment diagram:
Class diagram:
.NET remoting supports two channels (transports):
The examples show TCP channel as that is the topic for this article.
.NET Remoting supports two remote object activation models:
Both the previous diagrams and the following examples show SAO. The CAO model does not fit the same common interface and server only implementation model.
SAO exist in two flavors:
Singleton flavor => All calls hit a single instance of the remote object.
SingleCall => Each call result in a new instance of the remote object being created.
.NET remoting use the standard .NET ThreadPool (where SetMinThreads and SetMaxThreads methods control the size).
Common:
using System;
namespace Remoting.Common
{
[Serializable]
public class Data
{
public int Iv { get; set; }
public string Sv { get; set; }
public Data() : this(0, "")
{
}
public Data(int iv, string sv)
{
this.Iv = iv;
this.Sv = sv;
}
}
public interface ITest
{
int Add(int a, int b);
String Dup(String s);
Data Process(Data d);
int GetCounter();
void Noop();
}
}
Note data class serialzable but nothing on interface.
Server:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using Remoting.Common;
namespace Remoting.Server
{
public class Test : MarshalByRefObject, ITest
{
private int counter;
public int Add(int a, int b)
{
return a + b;
}
public String Dup(String s)
{
return s + s;
}
public Data Process(Data d)
{
return new Data(d.Iv + 1, d.Sv + "X");
}
public int GetCounter()
{
counter++;
return counter;
}
public void Noop()
{
// nothing
}
}
public class Program
{
private const int SERVER_PORT = 12345;
private const string NAME1 = "Test1";
private const string NAME2 = "Test2";
public static void Main(string[] args)
{
ChannelServices.RegisterChannel(new TcpServerChannel(SERVER_PORT), false);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(Test), NAME1, WellKnownObjectMode.Singleton);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(Test), NAME2, WellKnownObjectMode.SingleCall);
Console.ReadKey();
}
}
}
Client C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using Remoting.Common;
namespace Remoting.Client
{
public class Program
{
private const string SERVER = "localhost";
private const int SERVER_PORT = 12345;
private const string NAME1 = "Test1";
private const string NAME2 = "Test2";
private static void TestFunctional(string name)
{
ITest tst = (ITest)Activator.GetObject(typeof(ITest), string.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name));
int a = 123;
int b = 456;
int c = tst.Add(a, b);
Console.WriteLine(c);;
String s = "ABC";
String s2 = tst.Dup(s);
Console.WriteLine(s2);
Data d = new Data(123, "ABC");
Data d2 = tst.Process(d);
Console.WriteLine("{0} {1}", d2.Iv, d2.Sv);
}
private static void TestInstantiation(string name)
{
ITest tst1 = (ITest)Activator.GetObject(typeof(ITest), string.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name));
for(int i = 0; i < 2; i++)
{
int n = tst1.GetCounter();
Console.WriteLine(n);
}
ITest tst2 = (ITest)Activator.GetObject(typeof(ITest), string.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name));
for(int i = 0; i < 2; i++)
{
int n = tst2.GetCounter();
Console.WriteLine(n);
}
}
private const int REP = 100000;
private static void TestPerformance(string name)
{
ITest tst = (ITest)Activator.GetObject(typeof(ITest), string.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name));
DateTime dt1 = DateTime.Now;
Enumerable.Range(0, REP).AsParallel().ForAll(i => { tst.Noop(); });
DateTime dt2 = DateTime.Now;
Console.WriteLine("{0} requests per second", (int)(REP / (dt2 - dt1).TotalSeconds));
}
private static void Test(string label, string name)
{
Console.WriteLine(label + ":");
TestFunctional(name);
TestInstantiation(name);
TestPerformance(name);
}
public static void Main(string[] args)
{
ChannelServices.RegisterChannel(new TcpClientChannel(), false);
Test("Singleton", NAME1);
Test("SingleCall", NAME2);
}
}
}
Client VB.NET:
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports Remoting.Common
Namespace Remoting.Client
Public Class Program
Private Const SERVER As String = "localhost"
Private Const SERVER_PORT As Integer = 12345
Private Const NAME1 As String = "Test1"
Private Const NAME2 As String = "Test2"
Private Shared Sub TestFunctional(name As String)
Dim tst As ITest = DirectCast(Activator.GetObject(GetType(ITest), String.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)), ITest)
Dim a As Integer = 123
Dim b As Integer = 456
Dim c As Integer = tst.Add(a, b)
Console.WriteLine(c)
Dim s As String = "ABC"
Dim s2 As String = tst.Dup(s)
Console.WriteLine(s2)
Dim d As New Data(123, "ABC")
Dim d2 As Data = tst.Process(d)
Console.WriteLine("{0} {1}", d2.Iv, d2.Sv)
End Sub
Private Shared Sub TestInstantiation(name As String)
Dim tst1 As ITest = DirectCast(Activator.GetObject(GetType(ITest), String.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)), ITest)
For i As Integer = 0 To 1
Dim n As Integer = tst1.GetCounter()
Console.WriteLine(n)
Next
Dim tst2 As ITest = DirectCast(Activator.GetObject(GetType(ITest), String.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)), ITest)
For i As Integer = 0 To 1
Dim n As Integer = tst2.GetCounter()
Console.WriteLine(n)
Next
End Sub
Private Const REP As Integer = 100000
Private Shared Sub TestPerformance(name As String)
Dim tst As ITest = DirectCast(Activator.GetObject(GetType(ITest), String.Format("tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)), ITest)
Dim dt1 As DateTime = DateTime.Now
Enumerable.Range(0, REP).AsParallel().ForAll(Sub(i)
tst.Noop()
End Sub)
Dim dt2 As DateTime = DateTime.Now
Console.WriteLine("{0} requests per second", CInt(REP / (dt2 - dt1).TotalSeconds))
End Sub
Private Shared Sub Test(label As String, name As String)
Console.WriteLine(label & ":")
TestFunctional(name)
TestInstantiation(name)
TestPerformance(name)
End Sub
Public Shared Sub Main(args As String())
ChannelServices.RegisterChannel(New TcpClientChannel(), False)
Test("Singleton", NAME1)
Test("SingleCall", NAME2)
End Sub
End Class
End Namespace
It is possible to manage the client remote object relationship in more detail, but that is beyond the scope of this text.
.NET remoting server objects can be hosted both in a standalone program as in the shown examples and in ASP.NET, but standalone program seems to be the standard.
.NET remoting can use both binary serialization (default for TCP channel) as in the shown examples and SOAP serialization (default for HTTP channel).
WCF has been part of .NET since version 3.0 (2006).
WCF is most known for its support for SOAP web services and RESTful web services, but is also support binary RPC calls over TCP socket.
WCF with TCP binding works very similar to Remoting.
So:
Deployment diagram:
Class diagram:
Similar to Remoting SAO Singleton and SingleCall flavors, then WCF has instance context modes Single and PerCall.
Single mode => All calls hit a single instance of the remote object.
PerCall mode => Each call result in a new instance of the remote object being created.
.NET WCF ServiceHost use the standard .NET ThreadPool (where SetMinThreads and SetMaxThreads methods control the size).
Common:
using System;
using System.Runtime.Serialization;
using System.ServiceModel;
namespace WCF.Common
{
[DataContract]
public class Data
{
[DataMember]
public int Iv { get; set; }
[DataMember]
public string Sv { get; set; }
public Data() : this(0, "")
{
}
public Data(int iv, string sv)
{
this.Iv = iv;
this.Sv = sv;
}
}
[ServiceContract]
public interface ITest
{
[OperationContract]
int Add(int a, int b);
[OperationContract]
String Dup(String s);
[OperationContract]
Data Process(Data d);
[OperationContract]
int GetCounter();
[OperationContract]
void Noop();
}
}
Note the standard WCF [DataContract], [DataMember], [ServiceContract] and [OperationContract] attributes.
Server:
using System;
using System.ServiceModel;
using WCF.Common;
namespace WCF.Server
{
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single, ConcurrencyMode=ConcurrencyMode.Multiple, UseSynchronizationContext=false)]
public class Test1 : ITest
{
private int counter;
public int Add(int a, int b)
{
return a + b;
}
public String Dup(String s)
{
return s + s;
}
public Data Process(Data d)
{
return new Data(d.Iv + 1, d.Sv + "X");
}
public int GetCounter()
{
counter++;
return counter;
}
public void Noop()
{
// nothing
}
}
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall, ConcurrencyMode=ConcurrencyMode.Single)]
public class Test2 : ITest
{
private int counter;
public int Add(int a, int b)
{
return a + b;
}
public String Dup(String s)
{
return s + s;
}
public Data Process(Data d)
{
return new Data(d.Iv + 1, d.Sv + "X");
}
public int GetCounter()
{
counter++;
return counter;
}
public void Noop()
{
// nothing
}
}
public class Program
{
private const int SERVER_PORT = 12345;
private const string NAME1 = "Test1";
private const string NAME2 = "Test2";
public static void Main(string[] args)
{
ServiceHost host1 = new ServiceHost(typeof(Test1));
host1.AddServiceEndpoint(typeof(ITest), new NetTcpBinding(), string.Format("net.tcp://localhost:{0}/{1}", SERVER_PORT, NAME1));
host1.Open();
ServiceHost host2 = new ServiceHost(typeof(Test2));
host2.AddServiceEndpoint(typeof(ITest), new NetTcpBinding(), string.Format("net.tcp://localhost:{0}/{1}", SERVER_PORT, NAME2));
host2.Open();
Console.ReadKey();
}
}
}
Note the standard WCF [ServiceBehavior] attribute.
Client C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using WCF.Common;
namespace WCF.Client
{
public class Program
{
private const string SERVER = "localhost";
private const int SERVER_PORT = 12345;
private const string NAME1 = "Test1";
private const string NAME2 = "Test2";
private static void TestFunctional(string name)
{
ITest tst = ChannelFactory<ITest>.CreateChannel(new NetTcpBinding(), new EndpointAddress(string.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)));
int a = 123;
int b = 456;
int c = tst.Add(a, b);
Console.WriteLine(c);;
String s = "ABC";
String s2 = tst.Dup(s);
Console.WriteLine(s2);
Data d = new Data(123, "ABC");
Data d2 = tst.Process(d);
Console.WriteLine("{0} {1}", d2.Iv, d2.Sv);
}
private static void TestInstantiation(string name)
{
ITest tst1 = ChannelFactory<ITest>.CreateChannel(new NetTcpBinding(), new EndpointAddress(string.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)));
for(int i = 0; i < 2; i++)
{
int n = tst1.GetCounter();
Console.WriteLine(n);
}
ITest tst2 = ChannelFactory<ITest>.CreateChannel(new NetTcpBinding(), new EndpointAddress(string.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)));
for(int i = 0; i < 2; i++)
{
int n = tst2.GetCounter();
Console.WriteLine(n);
}
}
private const int REP = 10000;
private static void TestPerformance(string name)
{
ITest tst = ChannelFactory<ITest>.CreateChannel(new NetTcpBinding(), new EndpointAddress(string.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)));
DateTime dt1 = DateTime.Now;
Enumerable.Range(0, REP).AsParallel().ForAll(i => { tst.Noop(); });
DateTime dt2 = DateTime.Now;
Console.WriteLine("{0} requests per second", (int)(REP / (dt2 - dt1).TotalSeconds));
}
public static void Test(string label, string name)
{
Console.WriteLine(label + ":");
TestFunctional(name);
TestInstantiation(name);
TestPerformance(name);
}
public static void Main(string[] args)
{
Test("Single", NAME1);
Test("PerCall", NAME2);
}
}
}
Client VB.NET:
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.ServiceModel
Imports WCF.Common
Namespace WCF.Client
Public Class Program
Private Const SERVER As String = "localhost"
Private Const SERVER_PORT As Integer = 12345
Private Const NAME1 As String = "Test1"
Private Const NAME2 As String = "Test2"
Private Shared Sub TestFunctional(name As String)
Dim tst As ITest = ChannelFactory(Of ITest).CreateChannel(New NetTcpBinding(), New EndpointAddress(String.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)))
Dim a As Integer = 123
Dim b As Integer = 456
Dim c As Integer = tst.Add(a, b)
Console.WriteLine(c)
Dim s As String = "ABC"
Dim s2 As String = tst.Dup(s)
Console.WriteLine(s2)
Dim d As New Data(123, "ABC")
Dim d2 As Data = tst.Process(d)
Console.WriteLine("{0} {1}", d2.Iv, d2.Sv)
End Sub
Private Shared Sub TestInstantiation(name As String)
Dim tst1 As ITest = ChannelFactory(Of ITest).CreateChannel(New NetTcpBinding(), New EndpointAddress(String.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)))
For i As Integer = 0 To 1
Dim n As Integer = tst1.GetCounter()
Console.WriteLine(n)
Next
Dim tst2 As ITest = ChannelFactory(Of ITest).CreateChannel(New NetTcpBinding(), New EndpointAddress(String.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)))
For i As Integer = 0 To 1
Dim n As Integer = tst2.GetCounter()
Console.WriteLine(n)
Next
End Sub
Private Const REP As Integer = 10000
Private Shared Sub TestPerformance(name As String)
Dim tst As ITest = ChannelFactory(Of ITest).CreateChannel(New NetTcpBinding(), New EndpointAddress(String.Format("net.tcp://{0}:{1}/{2}", SERVER, SERVER_PORT, name)))
Dim dt1 As DateTime = DateTime.Now
Enumerable.Range(0, REP).AsParallel().ForAll(Sub(i)
tst.Noop()
End Sub)
Dim dt2 As DateTime = DateTime.Now
Console.WriteLine("{0} requests per second", CInt(REP / (dt2 - dt1).TotalSeconds))
End Sub
Public Shared Sub Test(label As String, name As String)
Console.WriteLine(label & ":")
TestFunctional(name)
TestInstantiation(name)
TestPerformance(name)
End Sub
Public Shared Sub Main(args As String())
Test("Single", NAME1)
Test("PerCall", NAME2)
End Sub
End Class
End Namespace
WCF supports bunch of other stuff that is not described here: hosting in ASP.NET, SOAP web services, RESTful web services etc.. It will be too much even to just try and list all the possibilities.
WCF is a very complex stack and has a reputation for being slow. And my experience is also that WCF with binary serialization and TCP binding is not faster than WCF for web services. And then there is not really any point in using binary instead of web services.
Pyro4 is a remoting framework for Python by Irmen de Jong. It can be installed with "pip install pyro4".
As serializer framework I use dill. It can be installed with "pip install dill".
Pyro4 works very similar to Java RMI. There are some differences due to the dynamic typing nature of Python.
So:
Each proxy object get its own instance of the remote object and all calls via that proxy hit that instance.
Pyro4 uses a fixed size thread pool. Default pool size is 40. Default can be changed by setting the config variable THREADPOOL_SIZE (and THREADPOOL_SIZE_MIN).
server.py:
import Pyro4
class Data(object):
def __init__(self, _iv = 0, _sv = ''):
self.iv = _iv
self.sv = _sv
@Pyro4.expose
class Test(object):
def __init__(self):
self.counter = 0
def add(self,a,b):
return a + b
def dup(self,s):
return s + s
def process(self,d):
return Data(d.iv + 1, d.sv + 'X')
def getCounter(self):
self.counter = self.counter + 1
return self.counter
def noop(self):
return
Pyro4.config.SERIALIZER = 'dill'
srv = Pyro4.Daemon()
uri = srv.register(Test)
ns = Pyro4.locateNS('localhost', 9090)
ns.register('Test', uri)
srv.requestLoop()
client.py:
import Pyro4
import time
class Data(object):
def __init__(self, _iv = 0, _sv = ''):
self.iv = _iv
self.sv = _sv
def testFunctional(name):
tst = Pyro4.Proxy(name)
print(tst.add(123, 456))
print(tst.dup('ABC'))
d2 = tst.process(Data(123,'ABC'))
print('%d %s' % (d2.iv,d2.sv))
def testInstantiation(name):
tst = Pyro4.Proxy(name)
for i in range(0, 2):
print(tst.getCounter())
tst = Pyro4.Proxy(name)
for i in range(0, 2):
print(tst.getCounter())
def testPerformance(name):
tst = Pyro4.Proxy(name)
REP = 10000
t1 = time.time()
for i in range(0, REP):
tst.noop()
t2 = time.time()
print('%d requests per second' % (REP / (t2 - t1)))
Pyro4.config.SERIALIZER = 'dill'
Pyro4.config.NS_HOST = 'localhost'
Pyro4.config.NS_PORT = 9090
NAME = 'PYRONAME:Test'
testFunctional(NAME)
testInstantiation(NAME)
testPerformance(NAME)
Running name server on Windows:
set PYRO_SERIALIZERS_ACCEPTED=serpent,json,dill
pyro4-ns -n localhost -p 9090
Running server on Windows:
set PYRO_SERIALIZERS_ACCEPTED=serpent,json,dill
python server.py
Running client on Windows:
set PYRO_SERIALIZERS_ACCEPTED=serpent,json,dill
python client.py
It is supposedly possible to run the name server in the same process as the remote server, but it is not easy and beyond my Python skills.
Performance of pyro4 with dill is actually pretty good.
Version | Date | Description |
---|---|---|
1.0 | October 20th 2018 | Initial version |
1.1 | November 3rd 2018 | Add Python pyro4 |
See list of all articles here
Please send comments to Arne Vajhøj