Writing distributed applications, especially deployed across a network, tends to be a challenge. This is not only due to the trickiness of the network programming but more so because of your code and business logic getting messed up with communication details. That makes it probably not flexible but hard to reuse.
Meanwhile, most programmers already know how to make their code flexible, reusable, and testable. Yes, reducing code coupling, often achieved by introducing an additional level of indirection, is the definite way to go. Then why don't we apply the same technique to overall application architecture? Simply, decoupling communication details from application logic will help us to build a flexibly distributable, fully testable application consisting of reusable modules.
In this article, we will get through a few simple examples of x2net application and see how distribution works in an x2 way.
Background
x2 is a set of concepts and specifications that facilitates the development of highly flexible cross-platform, cross-language distributed systems. Before further going on, it is recommended to look at its
README.md and
concepts.md.
x2net is the reference port of x2 written in C# targeting universal .NET environments.
In order to focus on the structural aspect, we begin with an extremely simple application:
C#
- public class Hello
- {
- public static void Main()
- {
- while (true)
- {
- var input = Console.ReadLine();
- if (input == "bye")
- {
- break;
- }
- var greeting = String.Format("Hello, {0}!", input);
- Console.WriteLine(greeting);
- }
- }
- }
Defining Events
An x2 application is composed of logic cases (or flows) that communicate only with one another. So defining a shared event hierarchy is the key activity at design time. In this simple example, we can grab the key feature that makes up a greeting sentence out of the name input. We define a request/response event pair for this feature as follows,
XML
- <?xml version="1.0" encoding="utf-8"?>
- <x2 namespace="hello">
- <definitions>
- <!-- Hello request event. -->
- <event name="HelloReq" id="1">
- <!-- Input name. -->
- <property name="Name" type="string"/>
- </event>
- <!-- Hello response event. -->
- <event name="HelloResp" id="2">
- <!-- Resultant greeting sentence. -->
- <property name="Greeting" type="string"/>
- </event>
- </definitions>
- </x2>
Running x2net.xpiler on this XML definition file will yield a corresponding C# source file which we can include into our project.
Once we define events, we can write the application logic cases to handle those events. Here, we write a simple case which creates the greeting sentence.
C#
- using x2net;
-
- public class HelloCase : Case
- {
- protected override void Setup()
- {
-
- Bind(new HelloReq(), OnHelloReq);
- }
-
- void OnHelloReq(HelloReq req)
- {
-
- new HelloResp {
-
- Greeting = String.Format("Hello, {0}!", req.Name)
- }
- .InResponseOf(req)
- .Post();
- }
- }
Please note that logic cases react to their interesting events by posting another event in return. They know nothing about the communication details: where request events come from or where response events are headed. Consequently, these logic cases may be freely located, without any change, at any point of the entire distributed application. And they can also be easily tested in isolation.
Having relevant events and cases, now we are ready to set up our first x2net application with these constructs.
C#
- using x2net;
-
- public class HelloStandalone
- {
- class LocalCase : Case
- {
- protected override void Setup()
- {
-
- Bind(new HelloResp(), (e) => {
- Console.WriteLine(e.Greeting);
- });
- }
- }
-
- public static void Main()
- {
- Hub.Instance
- .Attach(new SingleThreadFlow()
- .Add(new HelloCase())
- .Add(new LocalCase()));
-
- using (new Hub.Flows().Startup())
- {
- while (true)
- {
- var input = Console.ReadLine();
- if (input == "bye")
- {
- break;
- }
- new HelloReq { Name = input }.Post();
- }
- }
- }
- }
This works exactly the same as our original console application, but in an x2 way:
- A console input generates a HelloReq event.
- HelloCase takes the HelloReq event and posts a HelloResp event in return, with the generated greeting sentence.
- LocalCase takes the HelloResp event and prints its content to console output.
Now that we have an x2 application, we can easily change the threading model or distribution topology of our application. For example, applying the following change will let our every case run in a separate thread:
C#
- public static void Main()
- {
- Hub.Instance
- .Attach(new SingleThreadFlow()
- .Add(new HelloCase()))
- .Attach(new SingleThreadFlow()
- .Add(new LocalCase()));
Changing its threading model may not be so interesting. But how about making it a client/server application in minutes?
Client/Server Distribution
First, we prepare a server that runs the HelloCase as its main logic case:
C#
- using x2net;
-
- public class HelloTcpServer : AsyncTcpServer
- {
- public HelloTcpServer() : base("HelloServer")
- {
- }
-
- protected override void Setup()
- {
-
- EventFactory.Register<HelloReq>();
-
-
- Bind(new HelloResp(), Send);
-
- Listen(6789);
- }
-
- public static void Main()
- {
- Hub.Instance
- .Attach(new SingleThreadFlow()
- .Add(new HelloCase())
- .Add(new HelloTcpServer()));
-
- using (new Hub.Flows().Startup())
- {
- while (true)
- {
- var input = Console.ReadLine();
- if (input == "bye")
- {
- break;
- }
- }
- }
- }
- }
Then, we can write a simple client to connect to the server to get things done:
C#
- using x2net;
-
- public class HelloTcpClient : TcpClient
- {
- class LocalCase : Case
- {
- protected override void Setup()
- {
-
- Bind(new HelloResp(), (e) => {
- Console.WriteLine(e.Greeting);
- });
- }
- }
-
- public HelloTcpClient() : base("HelloClient")
- {
- }
-
- protected override void Setup()
- {
-
- EventFactory.Register<HelloResp>();
-
- Bind(new HelloReq(), Send);
-
- Connect("127.0.0.1", 6789);
- }
-
- public static void Main()
- {
- Hub.Instance
- .Attach(new SingleThreadFlow()
- .Add(new LocalCase())
- .Add(new HelloTcpClient()));
-
- using (new Hub.Flows().Startup())
- {
- while (true)
- {
- var input = Console.ReadLine();
- if (input == "bye")
- {
- break;
- }
- new HelloReq { Name = input }.Post();
- }
- }
- }
- }
Please note that the HelloCase does not change whether it is run in a standalone application or in a server.
In the above server link, you might wonder how we send a response event to the very client who issued the original request. The built-in event property _Handle does the trick. When an x2net link receives an event from a network, its _Handle property is set as the unique link session handle. If the _Handle property of the response event is the same as the original request, which is done by the InResponseOf extension method call, the server can locate the target link session with the _Handle property in this simple example.
Wrapping Up
The logic-communication decoupling itself is neither a new nor a popular concept. If you are accustomed to SendPacket-like communication, it may take some time until you feel comfortable with the x2-style distribution. This shift is somewhat like moving from message passing to generative communication, and it is surely worth a try.