Ma client server
Last updated at 4:55 am UTC on 27 August 2020
Applications have the need to be accessible across networks of computers. This is accomplished by an implementation of data-exchange between computing processes on the network. In the client-server model, a client sends a request to a server, the server processes the request and sends back a response.
Ma client server makes it easy to develop client-server applications. Here is an example server program which exports the local Compiler to clients on the network:
using: [ :requestString | (Compiler evaluate: requestString) asString ]
The framework provides two layers of interoperation. The above example uses the lower layer, referred to as the "String layer", because you send a String (or ByteArray) request and receive a String (or ByteArray) response. In the upper layer, the "object request layer" you send whole objects as requests and get whole objects as responses. The upper layer utilizes the Ma object serialization framework to convert the objects to ByteArrays, as well as the lower layer to send those ByteArrays automatically.
Using the String Layer
Setting up the server
If you just need to send and receive ByteArrays, you write your server program any way you want. Just keep in mind that, somewhere, you're going to want to take-in a ByteArray as input and you must answer a ByteArray as the response.
To start the server:
myServerSocket := MaServerSocket new
[ :requestByteArray |
"Use requestByteArray to calculate a response..." ]
MaServerSocket utilizes SharedQueue's and background Processes to ensure that request processing is not held up by sending or receiving the ByteArrays over the network, which occurs entirely in the background. The answer block is only ever evaluated for one client request at a time.
When you're done, be sure to shutdown the socket so resources are properly deallocated:
Getting answers in the client
Your client program creates a MaClientSocket by specifying the ip address as numbers in a ByteArray:
mySocket := MaClientSocket location:
No connections are made just from creating the socket. That happens when you actually send the request:
responseByteArray := mySocket sendData: myRequestByteArray
There's also a sendData:startingAt:count:waitForReplyIn: method for maximum efficiency. If the reply ByteArray you supply wasn't large enough, you'll get a bigger one.
Optional Socket Events
The MaServerSocket knows about a "server", which defaults to a MaServer that does nothing. But if you subclass MaServer and override some of its methods they'll be called when a socket is queued, a ByteArray request has been fully delivered, or sending a response, etc. See MaServer for more information.
Using the Object Request Layer
If you choose to use the object request layer, you send "request" objects and get back whole response objects. Client and server programs do not need to be concerned with parsing ByteArrays.
Setting up the server
When you are ready to start your server program, instead of creating a MaServerSocket, you create a MaNetworkRequestServer:
myServer := MaNetworkRequestServer protocol:
Dictionary. "etc." }.
What's this? Yes, you have to tell it all the classes that you'll be sending and receiving, accounting for the full object-graph of your requests. For example, if you have SubmitArticleRequest that has an Article object, you must include not only SubmitArticleRequest, Article and all the other kinds of objects that Article references, and what they reference, and so on. You don't however, need to ever specify String, ByteArray, Set, Symbol, Array, SmallInteger, Float, True, False, or UndefinedObject. Those are automatically part of every protocol.
You may also optionally specify a MaServerConsole to receive events. Subclass MaServerConsole and override one of its notification methods and it'll be called if you:
myServer console: myConsole
When you're ready to start the server, you use processOn:do:
[ :requestObject |
"Use requestObject to calculate a response..." ]
NOTE:The process block must not include a return carat (^). It is not necessary. The last expression in the block will be the value of the server response. Any return carat will cause clients to never get a response.
When you're done, the server should be shutdown to release operating system resources:
Setting up the client
Your client program creates a MaNetworkServerLink pointing to the server hostAddress and port is listening:
myLinkToServer := MaNetworkServerLink
Time that the client should wait for a response can be adjusted:
myLinkToServer timeoutSeconds: 30
Getting answers in the client
Now your client program can get answers from the server by:
myLinkToServer submit: someRequestObject
someRequestObject must inherit from MaClientServerRequest. Remember, someRequestObject must also be a member of the protocol, as will the response you get back from calling submit:. Adding a subclass to the protocol automatically includes all of its superclasses.
That's it! When finished accessing the server, don't forget to disconnect to release network resources back to the operating system.
Ma client server can be installed from the SqueakMap Package catalog. There are versions for particular releases of Squeak, and a (head) version representing the latest version of all packages intended for development the trunk image.
The head version can be loaded via the following script:
Installer new merge: #maInstaller.
(Smalltalk classNamed: #MaInstaller) new merge: #clientServerTester.
The test-suite for Ma client server is different than most test suites in that individual tests cannot be run from the TestRunner browser. Instead, the entire test suite is run by first saving the image, then evaluating:
MaClientServerTestCase suite debug
This expression causes the image to be saved under a temporary name, _macsTest-conductor. This image will spawn three copies of itself, one for the server, one to represent "client1" and one more to represent "client2".
The conductor then actually uses the Ma client server framework to instruct client1 and client2 to hit the server image with various kinds of requests depending on the test. I did this because I wanted a test that truly exercises between different images instead of only in memory.