Binary RPC 1 - technology specific

Content:

  1. Introduction
  2. Java RMI
  3. Java EJB 1.x/2.x
  4. Java EJB 3.x
  5. .NET Remoting
  6. .NET WCF
  7. Python pyro4

Introduction:

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:

Java RMI (Remote Method Invocation) has been part of Java since version 1.1 (1997).

Concept:

The concept is very simple:

Deployment diagram:

RMI deployment diagram

Class diagram:

RMI class diagram

Instance and Threading model:

All calls hit a single instance of the server object.

Threading is implementation specific, but typically a thread is started for each request received.

Example:

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();
    }
}

Notes:

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.

Java EJB 1.x/2.x:

EJB's was introduced in J2EE 1.2 (1999).

Concept:

EJB's in version 1.x and 2.x are complex:

Deployment diagram:

EJB 1.x/2.x deployment diagram

Class diagram:

EJB 1.x/2.x 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
a class with business logic
entity bean
a data class for persistence
message driven bean
a class processing messages from a message queue

Session beans are used for RPC calls.

Session beans comes in two flavors:

stateless
calls within a session may hit different objects
stateful
all calls within a session will hit same object

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:

remote interface
defines the business logic methods that can be accesses remotely (outside the application and possible from other system)
local interface
defines the business logic methods that can be accesses locally (inside the application)
remote home interface
defines the methods to create EJB's that can be accesses remotely (outside the application and possible from other system)
local home interface
defines the methods to create EJB's that can be accesses locally (inside the application)

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.

Instance and Threading model:

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.

Example:

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

Notes:

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.

Java EJB 3.x:

EJB 3.x got introduced with Java EE 6 (2009).

Concept:

EJB 3.x is relative simple:

Deployment diagram:

EJB 3.x deployment diagram

Class diagram:

EJB 3.x class diagram

EJB 3.x was a major simplification compared to EJB 1.x/2.x:

Instance and Threading model:

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.

Example:

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);
    }
}

Notes:

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

.NET remoting has been part of .NET since version 1.0 (2002).

It is sort of superseeded by WCF.

Concept:

The concept is very simple:

Deployment diagram:

Remoting deployment diagram

Class diagram:

Remoting 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:

SAO (Server Activated Object)
Remote object life cycle is managed by server
CAO (Client Activated Object)
Remote object life cycle is managed by client

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
Single remote object
SingleCall
Remote object per call

Instance and Threading model:

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).

Example:

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

Notes:

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).

.NET WCF

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.

Concept:

WCF with TCP binding works very similar to Remoting.

So:

Deployment diagram:

WCF deployment diagram

Class diagram:

WCF class diagram

Similar to Remoting SAO Singleton and SingleCall flavors, then WCF has instance context modes Single and PerCall.

Instance and Threading model:

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).

Example:

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

Notes:

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.

Python pyro4

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".

Concept:

Pyro4 works very similar to Java RMI. There are some differences due to the dynamic typing nature of Python.

So:

Instance and Threading model:

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).

Example:

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

Notes:

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.

Article history:

Version Date Description
1.0 October 20th 2018 Initial version
1.1 November 3rd 2018 Add Python pyro4

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj