This document will show how we develop a simulator for a computer network, from scratch, and step by step. The program we are going to develop is a simple representation of a computer network: it consists of objects that represent different parts of a local network such as packets, nodes, workstations, routers and hubs.
At first, we will just simulate the different steps of packet delivery and have fun with the system. In a second step we will extend the basic functionalities by adding extensions such as a hub and different packet routing strategies. Doing so, we will revisit many object-oriented concepts such as polymorphism, encapsulation, hooks and templates. Finally this system could be refined to become an experiment platform to explore and understand distributed algorithms.
We need to establish the basic model; what does the description above tell us? A network is a number of interconnected nodes, which exchange data packets. We will therefore probably need to model the nodes, the connection links, and the packets:
Let's start exploring by sketching some simple tests; this requires defining a test class:
Since we will create several classes, we used the following notation to refer to the classes in which a method should be defined.
KANetworkEntitiesTest >> testPacketCreation
means that the method testPacketCreation
is defined in the class KANetworkEntitiesTest
.
Packets are the simplest objects in our model: we need to create them, and ask them about the data they contain, but that's about it. Once created, a packet will not change its data, and the packet itself has no knowledge of the network, and no behavior that we can really talk about.
In this unit test, we wrote how we think packets should be created, using a from:to:payload:
constructor message, and how it should be accessed, using three messages sourceAddress
, destinationAddress
, and payload
.
Since we have not yet decided what addresses and payloads should look like, we just pass arbitrary objects as parameters; all that matters is that when we ask the packet, it returns the correct object back.
Of course, if we now compile and run this test method, it will fail, because the class KANetworkPacket
has not been created yet, nor any of the four above messages.
You can either execute and let the system prompt you when needed or we can define the class:
The class-side constructor method creates an instance then sends it an initialization message:
The initialization method is only supposed to be called when creating packets.
Once a packet is created, all we need to do with it is to obtain its payload, or the addresses of its source or destination nodes. We thus define an accessor method for each instance variable.
Now our test should be running and passing. That's enough for our admittedly simplistic model of packets; we completely ignore the layers of the OSI model, but it could be an interesting exercise to model that more precisely.
The first obvious thing we can say about a network node is that if we want to be able to send packets to it, then it should have an address; let's translate that into a test:
Like before, before running this test, we have to define the KANetworkNode
class:
Then a class-side constructor method taking the address of the new node as parameter:
The constructor relies on an instance-side initialization method:
And we can ask a node for its address:
Again our simplistic tests should now pass.
After nodes and packets, we should look at links. In the real world, a network cable is usually bidirectional, but here we're going to keep it simple and define links as simple one-way connections. To make a two-way connection, we will just make two links, one in each direction.
Therefore, a link has a source and a destination node; additionally, to be able to send packets, nodes need to know about their outgoing links.
This test creates two nodes and a link; after telling the link to attach itself, we check that it did so: the source node should confirm that it has an outgoing link to the destination node.
Note that the constructor could have registered the link with node1
, but we opted for a separate message attach
instead, because it's bad form to have a constructor change other objets; this way we can build links between arbitrary nodes and still have control of when the connection really becomes part of the network model.
Again, we need to define class of links:
A constructor that passes the two required parameters to an instance-side initialization message:
The initialization method itself:
Accessors:
The attach
method of a link delegates to the source node (the link knows which node has to do something, and the node knows what to do precisely):
If each node knows about all its outgoing links, it means it has a collection of those; we therefore need to extend KANetworkNode
, first with an additional instance variable outgoingLinks
:
This variable needs to be initialized properly:
We can then implement the attach:
method:
And finally the testing method on instances of KANetworkNode
:
Again, all the tests should now pass.
The next big feature is that nodes should be able to send and receive packets, and links to transmit them.
We create and setup two nodes, a link between them, and a packet. Now, to control which packets get delivered in which order, we specify that it happens in separate, controlled steps. This will allow us to model packet delivery precisely, to simulate latency, out-of-order reception, etc.:
send:via:
. At that point the packet should be passed to the link for transmission, but not completely delivered yet.transmit:
, and thus the packet should be received by the destination node.To send a packet, the node emits it on the link:
Since the packet will not be delivered right away, emitting a packet really just stores it in the link, until the user elects this packet to proceed using the transmit:
message.
Storing packets requires adding an instance variable to KANetworkLink
, as well as specifying how this instance variable should be initialized.
We also add a testing method to check whether a given packet is currently being transmitted by a link:
Transmitting a packet means passing it to the destination node, which will receive it. A link can not transmit packets that have not been sent via it, and once transmitted, the packet leaves the link:
Nodes only consume packets addressed to them; this is what will happen in our test, so we can worry about the alternative case later (notYetImplemented
is a special message that we can use in place of code that we will have to write eventually, but prefer to ignore for the time being).
Consuming a packet represents what the node will do with it at the application level; for the moment, let's just define an empty consume:
method, as a template for later:
Additionally to consuming the packet, we remember it did arrive; for now this is mostly for our tests and for debug, but we could see that becoming useful in the future, if we want to simulate packet losses and re-emissions.
We thus add the arrivedPackets
instance variable, with proper initialization and accessing:
At that point all our tests should pass.
Note that the message notYetImplemented
is not called, since our tests do not require routing (yet).
If a node wants to send a packet to itself, it does not need any connection to do so. In real-world networking stacks, loopback routing shortcuts the lower networking layers; however, this is finer detail than we are modeling here. Still, we want to model the fact that the loopback link is a little special, so each node will store its own loopback link, separately from the outgoing links.
The loopback link is implicitely created as part of the node itself.
We also introduce a new send:
message, which takes the responsibility of selecting the link to emit the packet.
For triggering packet transmission, we have to use a specific accessor to find the loopback link of the node.
First, we have to add yet another instance variable in nodes:
As with all instance variables, we have to remember to make sure it is correctly initialized; we thus modify initialize
:
The accessor has nothing special:
And finally we can focus on the send:
method and automatic link selection.
This method has to rely on some routing algorithm to identify which links will transmit the packet closer to its destination.
Since some routing algorithms select more than one link, we will implement routing as an iteration method, which evaluates the given block for each selected link.
One of the simplest routing algorithm is flooding: just send the packet via every link; this is obviously a waste of bandwidth, but it works. We can however make a special case for loopback, when the destination address is the one of the current node. When the address of a packet is the address
Now we have the basic model working, and we can try more realistic examples.
More realistic tests will require non-trivial networks.
We thus need an object that represents the network as a whole, to avoid keeping many nodes and links in individual variables.
We will introduce a new class KANetwork
, whose responsibility is to help us build, assemble then find the nodes and links involved in a network.
Let's start by creating another test class, to keep things in order:
Since every test needs to rebuild the whole network from scratch, we specify so in the setUp
method:
Before anything else, let's write a tests that will pass once we've made progress; we want to access network nodes given only their addresses. Here we check that we get a hub node based on its address:
We will have to implement this nodeAt:ifNone:
on our KANetwork
class; but first we need to decide how its instances are built.
Let's build network net
, with the main part connected in a star shape around a hub
node; a pair of nodes ping
and pong
are part of the network but not connected to hub
, and the alone
node is just by itself, not even added to the network:
This method builds nodes as before, but sends connect:to:
to the net
object instead of creating links explicitly; this is how we tell the network which nodes and links it should remember.
Let's not forget about proper initialization:
Connecting nodes means creating the links in both directions, then adding both nodes and both links in their corresponding collections:
Note that the attach
method we defined previously effectively returns the link.
Now we can implement node lookup:
We can also make a convenience nodeAt:
method for node lookup, that will raise an exception if it does not find the node.
Let's first write a test which validates this behavior:
Then we can simply express the nodeAt:
using the predefined Pharo exception NotFound
:
And finally, we want to be able to lookup links between two nodes. Again we define a new test:
And we define the method linkFrom:to:
identifying a link between source and destination nodes with matching addresses:
As a final check, let's reproduce some of our previous tests in the network we just built.
You can see that we used new testing methods isAddressedTo:
and isOriginatingFrom:
; these are just to avoid explicitly comparing addresses, for convenience:
The second test is transmitting a packet between directly connected nodes:
Both those tests should pass with no additional work, since they just reproduce what we already tested in KANetworkEntitiesTest
.
Until now, we only tested packet delivery between directly connected nodes; let's try sending a node so that the packet has to be forwarded through the hub.
If you run this test, you will see that it fails because of the notYetImplemented
message we left earlier; it's time to fix that!
When a node receives a packet but is not the recipient, it should forward the packet:
Now we need to implement packet forwarding, but there is a trap.
An easy solution would be to simply send:
the packet again: the hub would send the packet to all its connected nodes, one of which happens to be pc1
, the recipient, so all is good!
Wrong.
The packet would be sent back to other nodes than the recipient; what would those nodes do when they receive a packet not addressed to them? Forward it. Where? To all their neighbours, which would forward it again... so when would the forwarding stop?
To fix this, we need hubs to behave differently from nodes. When receiving a packet addressed to another node, a hub should forward it, but a normal node should just ignore it.
Let's first define an empty forward:from:
method for nodes:
Now we can add a new class for hubs, which will have an actual implementation of forwarding:
A hub does not have routing information, so all we can do is forward the packet on all outbound links, unless the link goes back where the packet arrived from:
Now we can use a proper hub in our test, replacing the relevant line in KANetworkTest >> buildNetwork:
, and check that the testSendViaHub
unit test passes.
this is out of place, it's not about packet delivery…
When a workstation consumes a packet, it simply increments a packet counter.
Let's start by subclassing KANetworkNode
:
We need to initialize the receivedCount
instance variable.
Properly redefining initialize:
is enough, because the address is initialized separately in the constructor method KANetworkNode >> withAddress:
; however, it's really important not to forget the super initialize
message, because that method does
Now we can redefine consume:
accordingly:
Define accessors and the printOn:
method, for debugging. Test the behavior of workstation nodes.
When a printer consumes a packet, it prints it; we can model the output tray as a list where packet payloads get queued, and the supply tray as the number of blank sheets it contains.
The implementation is very similar; we subclass KANetworkNode
to redefine the consume:
method:
Initialization is a bit different, though; since the standard initialize
method has no argument, the only sensible initial value for the supply
instance variable is zero:
We therefore need a way to pass the initial supply of paper available to a fresh instance:
For convenience, we can provide an extended constructor to create printers with a non-empty supply in one message:
Define accessors and the printOn:
method, for debugging. Test the behavior of printer nodes.
When a server node consumes a packet, it converts the payload to uppercase, then sends that back to the sender of the request.
This is yet another subclass which redefines the consume:
method, but this time the node is stateless, so we have no initialization or accessor methods to write:
Define accessors and the printOn:
method, for debugging. Test the behavior of server nodes.
Let's now model a more realistic network with a cycle between three central nodes:
Since we want to keep the previous tests unchanged, we define a new test class with a different buildNetwork
method:
automatic routing?