Cromis IPC – Fast inter process communication (Named Pipes)
About half a year ago I had a mission to make a server that was able to handle multiple ISAPI modules and DLL modules over HTTP. I did not want to use Apache or IIS, for two reasons
- I must support clean, easy installations of the software, even on feeble laptops with easy maintenance over time.
- I like to have complete control over workings of such server and I want it to be as fast as possible.
Because these two conditions ruled out any available web server, I decided to make my own. The server only serves ISAPI and DLL modules, not HTML or other web stuff. So this was a viable solution (maybe I will write more about it in the future). One of the main design principles for this server was stability. The decision was that the main process will only receive requests and pass them on to appropriate module handling processes. This is a sandbox approach where each module runs in its own process. If that module does something stupid only the hosting process is affected and not the whole server (lets say it is a Google Chrome analogy). And the server can easily run another process and repeats the action, so the user doesn’t even know something was wrong. Now one of the main decisions I had to make was how the main process and worker processes will communicate with each other. We have a lot of inter process communication technologies:
- Messages
- Sockets (TCP /IP, UDP)
- Mail Slots
- Shared Memory
- Named Pipes
- COM, COM+
- …many more
I wanted a very fast and scalable solution. After debating it with my colleagues and reading about other solutions, I decided for Named Pipes. Why? Because they are fast, very fast and they have client / server paradigm build right into them. Shared Memory is probably a little faster, but then you have to roll your own synchronization and manage the memory locks etc… And Named Pipes can work over computers in LAN if needs arises. Nice bonus, if you ask me. After reading about them on MSDN and looking at some implementations I wrote my own implementation. It is a typical client server implementation, but as spartan as it can be. No bloat and no overhead is in it.
Maybe the most noticeable change from other implementations is, that mine is focused on messaging and packets of data. I have seen implementations that are basically wrappers around Named Pipes API and the user is left to make the communication protocol. That is ok, but if you are building IPC, you already know you need to communicate and you only need a flexible carrier for your data. So that is exactly what I have done. I love simplicity in code usage. Using my IPC code comes down to something like this:
Client side:
procedure TForm1.btnSendClick(Sender: TObject); var Result: IIPCData; Request: IIPCData; IPCClient: TIPCClient; TimeStamp: TDateTime; begin IPCClient := TIPCClient.Create; try IPCClient.ServerName := eServerName.Text; Request := AcquireIPCData; Request.Data.WriteUTF8String('Command', 'GetTime'); Result := IPCClient.ExecuteRequest(Request); if IPCClient.AnswerValid then begin TimeStamp := Result.Data.ReadDateTime('TDateTime'); ListBox1.Items.Add(Format('Response: TDateTime [%s]', [DateTimeToStr(TimeStamp)])); ListBox1.Items.Add(Format('Response: Integer [%d]', [Result.Data.ReadInteger('Integer')])); ListBox1.Items.Add(Format('Response: Real [%f]', [Result.Data.ReadReal('Real')])); ListBox1.Items.Add(Format('Response: String [%s]', [Result.Data.ReadUTF8String('String')])); ListBox1.Items.Add('-----------------------------------------------------------'); end else ListBox1.Items.Add(Format('Error: Code %d', [IPCClient.LastError])); finally IPCClient.Free; end; end;
Server Side:
procedure TForm1.OnExecuteRequest(const Request, Response: IIPCData); begin ListBox1.Items.Add('Request Recieved'); Response.Data.WriteDateTime('TDateTime', Now); Response.Data.WriteInteger('Integer', 5); Response.Data.WriteReal('Real', 5.33); Response.Data.WriteUTF8String('String', This is a test string'); Caption := Format('%d requests processed', [ListBox1.Count]); end;
Can it be any simpler? As you can see there is support for basic data types. You can also send streams over etc… This is what I meant by flexible data carrier. The “AcquireIPCData” returns “IIPCData” interface which holds inside my “TStreamStorage” implementation. So data carrier is based on TMemoryStream as that is the fastest way to transport data to the Pipe and back. TStreamStorage is for now like this (name / value pairs implementation), but will probably expand in the future:
TStreamStorage = class private FStorage: TMemoryStream; procedure WriteHeaders(const Name: ustring; const DataLength: Int64); function FindNamedPosition(const Name: ustring; var ValueSize: Int64): Boolean; public constructor Create; destructor Destroy; override; // stream writing procedures (name and value pairs) procedure WriteUnicodeString(const Name: ustring; const Value: ustring); procedure WriteUTF8String(const Name: ustring; const Value: astring); procedure WriteDateTime(const Name: ustring; const Value: TDateTime); procedure WriteInteger(const Name: ustring; const Value: Integer); procedure WriteBoolean(const Name: ustring; const Value: Boolean); procedure WriteStream(const Name: ustring; const Value: TStream); procedure WriteReal(const Name: ustring; const Value: Real); // stream reading functions (name and value pairs) function ReadUnicodeString(const Name: ustring): ustring; function ReadUTF8String(const Name: ustring): astring; function ReadDateTime(const Name: ustring): TDateTime; function ReadInteger(const Name: ustring): Integer; function ReadBoolean(const Name: ustring): Boolean; function ReadStream(const Name: ustring): TStream; function ReadReal(const Name: ustring): Real; // misc procedures and properties property Storage: TMemoryStream read FStorage; procedure Clear; end;
All you have to do, to pass data to the Pipe is:
WriteFile(fHandle, Request.Data.Storage.Memory^, Request.Data.Storage.Size, ABytes, nil);
I don’t believe this can be done significantly faster. If you need a fast IPC you can download the “Cromis IPC” from my download section. If you have questions or problems using the code drop a comment or contact me directly.
Gdhami wrote,
Thanks for sharing the code
One question: is there a way to allow two-way communication?
I mean, AFAICS, you implemented the [ Client => Server ] part, but not the other way around [ Server => Client ].
I realize this is probably not your original plan, but it would be *very* handy to extend it to support bi-way communication.
Link | January 14th, 2012 at 4:07 am
Iztok Kacin wrote,
No for now there is no such way, nor is it planned. Two way communication would mean a permanent connection between the client and server and pipes do not work that way. I also don’t see the benefits big enough to weight out the complications in design.
You can achieve this if you have server and client at both sided and you simulate the two way communication. I know not build in but still doable.
Link | January 16th, 2012 at 11:41 pm
MaartenW wrote,
I do like the setup of Cromis IPC.
Is it possible to set up a subscription, so the Client asks for regularly published data once and the Server sends them unsollicited on a regular basis until it gets a signal to stop? How do I setup the Client to accept these unsollicited data?
Link | February 28th, 2012 at 5:26 pm
Iztok Kacin wrote,
@MaartenW
For now there is no way to notify the client from the server that new data is available. However I do plan to implement such notification system. It will probably only work on a single machine and not over LAN. I have to ponder yet the best way to achieve this.
Link | March 17th, 2012 at 9:28 am