-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlan-simulator.pillar
909 lines (672 loc) · 31.3 KB
/
lan-simulator.pillar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
{ "title": "Designing a simple network simulator" }
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.
!! Basic definitions and a starting point
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:
- Nodes have addresses, can send and receive packets;
- Links connect two nodes together, and transmit packets between them;
- Packets transport a payload and have the address of the node to which it should be delivered; if we want nodes to be able to answer, packets should also have the address of the node which originally sent it.
Let's start exploring by sketching some simple tests; this requires defining a test class:
[[[language=smalltalk
TestCase subclass: #KANetworkEntitiesTest
instanceVariableNames: ''
classVariableNames: ''
category: 'NetworkSimulator-Tests'
]]]
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 simple value objects
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.
[[[language=smalltalk
KANetworkEntitiesTest >> testPacketCreation
| src dest payload packet |
src := Object new.
dest := Object new.
payload := Object new.
packet := KANetworkPacket from: src to: dest payload: payload.
self assert: packet sourceAddress equals: src.
self assert: packet destinationAddress equals: dest.
self assert: packet payload equals: payload
]]]
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:
[[[language=smalltalk
Object subclass: #KANetworkPacket
instanceVariableNames: 'sourceAddress destinationAddress payload'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
The class-side constructor method creates an instance then sends it an initialization message:
[[[language=smalltalk
KANetworkPacket class >> from: sourceAddress to: destinationAddress payload: anObject
^ self new
initializeSource: sourceAddress
destination: destinationAddress
payload: anObject
]]]
The initialization method is only supposed to be called when creating packets.
[[[language=smalltalk
KANetworkPacket >> initializeSource: source destination: destination payload: anObject
sourceAddress := source.
destinationAddress := destination.
payload := anObject
]]]
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.
[[[language=smalltalk
KANetworkPacket >> sourceAddress
^ sourceAddress
]]]
[[[language=smalltalk
KANetworkPacket >> destinationAddress
^ destinationAddress
]]]
[[[language=smalltalk
KANetworkPacket >> payload
^ payload
]]]
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.
!!! Nodes
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:
[[[language=smalltalk
KANetworkEntitiesTest >> testNodeCreation
| address node |
address := Object new.
node := KANetworkNode withAddress: address.
self assert: node address equals: address
]]]
Like before, before running this test, we have to define the ==KANetworkNode== class:
[[[language=smalltalk
Object subclass: #KANetworkNode
instanceVariableNames: 'address'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
Then a class-side constructor method taking the address of the new node as parameter:
[[[language=smalltalk
KANetworkNode class >> withAddress: aNetworkAddress
^ self new
initializeAddress: aNetworkAddress;
yourself
]]]
The constructor relies on an instance-side initialization method:
[[[language=smalltalk
KANetworkNode >> initializeAddress: aNetworkAddress
address := aNetworkAddress
]]]
And we can ask a node for its address:
[[[language=smalltalk
KANetworkNode >> address
^ address
]]]
Again our simplistic tests should now pass.
!!! Links are one-way connections between nodes
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.
[[[language=smalltalk
KANetworkEntitiesTest >> testNodeLinking
| node1 node2 link |
node1 := KANetworkNode withAddress: #address1.
node2 := KANetworkNode withAddress: #address2.
link := KANetworkLink from: node1 to: node2.
link attach.
self assert: (node1 hasLinkTo: node2)
]]]
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:
[[[language=smalltalk
Object subclass: #KANetworkLink
instanceVariableNames: 'source destination'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
A constructor that passes the two required parameters to an instance-side initialization message:
[[[language=smalltalk
KANetworkLink class >> from: sourceNode to: destinationNode
^ self new
initializeFrom: sourceNode to: destinationNode
]]]
The initialization method itself:
[[[language=smalltalk
KANetworkLink >> initializeFrom: sourceNode to: destinationNode
source := sourceNode.
destination := destinationNode.
]]]
Accessors:
[[[language=smalltalk
KANetworkLink >> source
^ source
]]]
[[[language=smalltalk
KANetworkLink >> destination
^ destination
]]]
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):
[[[language=smalltalk
KANetworkLink >> attach
source attach: self
]]]
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==:
[[[language=smalltalk
Object subclass: #KANetworkNode
instanceVariableNames: 'address outgoingLinks'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
This variable needs to be initialized properly:
[[[language=smalltalk
KANetworkNode >> initialize
outgoingLinks := Set new.
]]]
We can then implement the ==attach:== method:
[[[language=smalltalk
KANetworkNode >> attach: anOutgoingLink
outgoingLinks add: anOutgoingLink
]]]
And finally the testing method on instances of ==KANetworkNode==:
[[[language=smalltalk
KANetworkNode >> hasLinkTo: aNetworkNode
^ outgoingLinks
anySatisfy: [ :any | any destination == aNetworkNode ]
]]]
Again, all the tests should now pass.
!!! Nodes can emit packets
The next big feature is that nodes should be able to send and receive packets, and links to transmit them.
[[[language=smalltalk
KANetworkEntitiesTest >> testSendAndTransmit
| srcNode destNode link packet |
srcNode := KANetworkNode withAddress: #src.
destNode := KANetworkNode withAddress: #dest.
link := (KANetworkLink from: srcNode to: destNode) attach; yourself.
packet := KANetworkPacket from: #address to: #dest payload: #payload.
srcNode send: packet via: link.
self assert: (link isTransmitting: packet).
self deny: (destNode hasReceived: packet).
link transmit: packet.
self deny: (link isTransmitting: packet).
self assert: (destNode hasReceived: packet)
]]]
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.:
- First, we tell the node to send the packet using ==send:via:==. At that point the packet should be passed to the link for transmission, but not completely delivered yet.
- Then, we tell the link to actually pass the packet along using ==transmit:==, and thus the packet should be received by the destination node.
!!!! Sending
To send a packet, the node emits it on the link:
[[[language=smalltalk
KANetworkNode >> send: aPacket via: aLink
aLink emit: aPacket
]]]
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.
[[[language=smalltalk
Object subclass: #KANetworkLink
instanceVariableNames: 'source destination packetsToTransmit'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
[[[language=smalltalk
KANetworkLink >> initialize
packetsToTransmit := OrderedCollection new
]]]
[[[language=smalltalk
KANetworkLink >> emit: aPacket
"Packets are not transmitted right away, but stored.
Transmission is explicitly triggered later, by sending #transmit:."
packetsToTransmit add: aPacket
]]]
We also add a testing method to check whether a given packet is currently being transmitted by a link:
[[[language=smalltalk
KANetworkLink >> isTransmitting: aPacket
^ packetsToTransmit includes: aPacket
]]]
!!!! Transmitting
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:
[[[language=smalltalk
KANetworkLink >> transmit: aPacket
"Transmit aPacket to the destination node of the receiver link."
(self isTransmitting: aPacket)
ifTrue: [
packetsToTransmit remove: aPacket.
destination receive: aPacket from: self ]
]]]
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).
[[[language=smalltalk
KANetworkNode >> receive: aPacket from: aLink
aPacket destinationAddress = address
ifTrue: [
self consume: aPacket.
arrivedPackets add: aPacket ]
ifFalse: [ self notYetImplemented ]
]]]
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:
[[[language=smalltalk
KANetworkNode >> consume: aPacket
"Default handling is to do nothing."
]]]
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:
[[[language=smalltalk
Object subclass: #KANetworkNode
instanceVariableNames: 'address outgoingLinks arrivedPackets'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
[[[language=smalltalk
KANetworkNode >> initialize
outgoingLinks := Set new.
arrivedPackets := OrderedCollection new
]]]
[[[language=smalltalk
KANetworkNode >> hasReceived: aPacket
^ arrivedPackets includes: aPacket
]]]
At that point all our tests should pass.
Note that the message ==notYetImplemented== is not called, since our tests do not require routing (yet).
!!! A standalone node can transmit a packet to itself
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.
[[[language=smalltalk
KANetworkEntitiesTest >> testLoopback
| node packet |
node := KANetworkNode withAddress: #address.
packet := KANetworkPacket from: #address to: #address payload: #payload.
node send: packet.
node loopback transmit: packet.
self assert: (node hasReceived: packet).
self deny: (node loopback isTransmitting: packet)
]]]
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:
[[[language=smalltalk
Object subclass: #KANetworkNode
instanceVariableNames: 'address outgoingLinks loopback arrivedPackets'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
As with all instance variables, we have to remember to make sure it is correctly initialized; we thus modify ==initialize==:
[[[language=smalltalk
KANetworkNode >> initialize
loopback := KANetworkLink from: self to: self.
outgoingLinks := Set new.
arrivedPackets := OrderedCollection new
]]]
The accessor has nothing special:
[[[language=smalltalk
KANetworkNode >> loopback
^ loopback
]]]
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.
[[[language=smalltalk
KANetworkNode >> send: aPacket
"Send aPacket, leaving the responsibility of routing to the node."
self
linksTowards: aPacket destinationAddress
do: [ :link | self send: aPacket via: 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
[[[language=smalltalk
KANetworkNode >> linksTowards: anAddress do: aBlock
"Simple flood algorithm: route via all outgoing links.
However, just loopback if the receiver node is the routing destination."
anAddress = address
ifTrue: [ aBlock value: self loopback ]
ifFalse: [ outgoingLinks do: aBlock ]
]]]
Now we have the basic model working, and we can try more realistic examples.
!! Modeling the network itself
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:
[[[language=smalltalk
TestCase subclass: #KANetworkTest
instanceVariableNames: 'net hub alone'
classVariableNames: ''
category: 'NetworkSimulator-Tests'
]]]
Since every test needs to rebuild the whole network from scratch, we specify so in the ==setUp== method:
[[[language=smalltalk
KANetworkTest >> setUp
self buildNetwork
]]]
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:
[[[language=smalltalk
KANetworkTest >> testNetworkFindsNodesByAddress
self
assert: (net nodeAt: hub address ifNone: [ self fail ])
equals: hub
]]]
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:
+file://images/lan-star.svg|width=50+
[[[language=smalltalk
KANetworkTest >> buildNetwork
alone := KANetworkNode withAddress: #alone.
net := KANetwork new.
hub := KANetworkNode withAddress: #hub.
#(mac pc1 pc2 prn) do: [ :addr |
| node |
node := KANetworkNode withAddress: addr.
net connect: node to: hub ].
net
connect: (KANetworkNode withAddress: #ping)
to: (KANetworkNode withAddress: #pong)
]]]
!!!! The network class
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.
[[[language=smalltalk
Object subclass: #KANetwork
instanceVariableNames: 'nodes links'
classVariableNames: ''
category: 'NetworkSimulator-Core'
]]]
Let's not forget about proper initialization:
[[[language=smalltalk
KANetwork >> initialize
nodes := Set new.
links := Set new
]]]
Connecting nodes means creating the links in both directions, then adding both nodes and both links in their corresponding collections:
[[[language=smalltalk
KANetwork >> connect: aNode to: anotherNode
self add: aNode.
self add: anotherNode.
links add: (self makeLinkFrom: aNode to: anotherNode) attach.
links add: (self makeLinkFrom: anotherNode to: aNode) attach
]]]
Note that the ==attach== method we defined previously effectively returns the link.
[[[language=smalltalk
KANetwork >> add: aNode
nodes add: aNode
]]]
[[[language=smalltalk
KANetwork >> makeLinkFrom: aNode to: anotherNode
^ KANetworkLink from: aNode to: anotherNode
]]]
!!!! Looking up nodes
Now we can implement node lookup:
[[[language=smalltalk
KANetwork >> nodeAt: anAddress ifNone: noneBlock
^ nodes
detect: [ :any | any address = anAddress ]
ifNone: noneBlock
]]]
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:
[[[language=smalltalk
KANetworkTest >> testNetworkOnlyFindsAddedNodes
self
should: [ net nodeAt: alone address ]
raise: NotFound
]]]
Then we can simply express the ==nodeAt:== using the predefined Pharo exception ==NotFound==:
[[[language=smalltalk
KANetwork >> nodeAt: anAddress
^ self
nodeAt: anAddress
ifNone: [ NotFound signalFor: anAddress in: self ]
]]]
!!!! Looking up links
And finally, we want to be able to lookup links between two nodes. Again we define a new test:
[[[language=smalltalk
KANetworkTest >> testNetworkFindsLinks
| link |
self
shouldnt: [ link := net linkFrom: #pong to: #ping ]
raise: NotFound.
self
assert: link source
equals: (net nodeAt: #pong).
self
assert: link destination
equals: (net nodeAt: #ping)
]]]
And we define the method ==linkFrom:to:== identifying a link between source and destination nodes with matching addresses:
[[[language=smalltalk
KANetwork >> linkFrom: sourceAddress to: destinationAddress
^ links
detect: [ :anyLink |
anyLink source address = sourceAddress
and: [ anyLink destination address = destinationAddress ] ]
ifNone: [
NotFound
signalFor: sourceAddress -> destinationAddress
in: self ]
]]]
As a final check, let's reproduce some of our previous tests in the network we just built.
[[[language=smalltalk
KANetworkTest >> testSelfSend
| packet |
packet := KANetworkPacket
from: alone address
to: alone address
payload: #something.
self assert: (packet isAddressedTo: alone).
self assert: (packet isOriginatingFrom: alone).
alone send: packet.
self deny: (alone hasReceived: packet).
self assert: (alone loopback isTransmitting: packet).
alone loopback transmit: packet.
self deny: (alone loopback isTransmitting: packet).
self assert: (alone hasReceived: packet)
]]]
You can see that we used new testing methods ==isAddressedTo:== and ==isOriginatingFrom:==; these are just to avoid explicitly comparing addresses, for convenience:
[[[language=smalltalk
KANetworkPacket >> isAddressedTo: aNode
^ destinationAddress = aNode address
]]]
[[[language=smalltalk
KANetworkPacket >> isOriginatingFrom: aNode
^ sourceAddress = aNode address
]]]
The second test is transmitting a packet between directly connected nodes:
[[[language=smalltalk
KANetworkTest >> testDirectSend
| packet ping pong link |
packet := KANetworkPacket from: #ping to: #pong payload: #ball.
ping := net nodeAt: #ping.
pong := net nodeAt: #pong.
link := net linkFrom: #ping to: #pong.
ping send: packet.
self assert: (link isTransmitting: packet).
self deny: (pong hasReceived: packet).
link transmit: packet.
self deny: (link isTransmitting: packet).
self assert: (pong hasReceived: packet)
]]]
Both those tests should pass with no additional work, since they just reproduce what we already tested in ==KANetworkEntitiesTest==.
!! Packet delivery in a more realistic network
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.
[[[language=smalltalk
KANetworkTest >> testSendViaHub
| hello mac pc1 firstLink secondLink |
hello := KANetworkPacket from: #mac to: #pc1 payload: 'Hello!'.
mac := net nodeAt: #mac.
pc1 := net nodeAt: #pc1.
firstLink := net linkFrom: #mac to: #hub.
secondLink := net linkFrom: #hub to: #pc1.
self assert: (hello isAddressedTo: pc1).
self assert: (hello isOriginatingFrom: mac).
mac send: hello.
self deny: (pc1 hasReceived: hello).
self assert: (firstLink isTransmitting: hello).
firstLink transmit: hello.
self deny: (pc1 hasReceived: hello).
self assert: (secondLink isTransmitting: hello).
secondLink transmit: hello.
self assert: (pc1 hasReceived: hello).
]]]
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:
[[[language=smalltalk
KANetworkNode >> receive: aPacket from: aLink
aPacket destinationAddress = address
ifTrue: [
self consume: aPacket.
arrivedPackets add: aPacket ]
ifFalse: [ self forward: aPacket from: aLink ]
]]]
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?
!!! Differenciating hubs from normal nodes
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:
[[[language=smalltalk
KANetworkNode >> forward: aPacket from: arrivalLink
"Do nothing. Normal nodes do not route packets."
]]]
Now we can add a new class for hubs, which will have an actual implementation of forwarding:
[[[language=smalltalk
KANetworkNode subclass: #KANetworkHub
instanceVariableNames: ''
classVariableNames: ''
category: 'NetworkSimulator-Nodes'
]]]
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:
[[[language=smalltalk
KANetworkHub >> forward: aPacket from: arrivalLink
self
linksTowards: aPacket destinationAddress
do: [ :link |
link destination == arrivalLink source
ifFalse: [ self send: aPacket via: link ] ]
]]]
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.
[[[language=smalltalk
hub := KANetworkHub withAddress: #hub.
]]]
!!! Other examples of specialized nodes
@@todo this is out of place, it's not about packet delivery…
!!!! Workstations count received packets
When a workstation consumes a packet, it simply increments a packet counter.
Let's start by subclassing ==KANetworkNode==:
[[[language=smalltalk
KANetworkNode subclass: #KANetworkWorkstation
instanceVariableNames: 'receivedCount'
classVariableNames: ''
category: 'NetworkSimulator-Nodes'
]]]
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
[[[language=smalltalk
KANetworkWorkstation >> initialize
super initialize.
receivedCount := 0
]]]
Now we can redefine ==consume:== accordingly:
[[[language=smalltalk
KANetworkWorkstation >> consume: aPacket
receivedCount := receivedCount + 1
]]]
@@todo Define accessors and the ==printOn:== method, for debugging. Test the behavior of workstation nodes.
!!!! Printers accumulate printouts
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:
[[[language=smalltalk
KANetworkNode subclass: #KANetworkPrinter
instanceVariableNames: 'supply tray'
classVariableNames: ''
category: 'NetworkSimulator-Nodes'
]]]
[[[language=smalltalk
KANetworkPrinter >> consume: aPacket
supply > 0 ifTrue: [ ^ self "no paper, do nothing" ].
supply := supply - 1.
tray add: aPacket payload
]]]
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:
[[[language=smalltalk
KANetworkPrinter >> initialize
super initialize.
supply := 0.
tray := OrderedCollection new
]]]
We therefore need a way to pass the initial supply of paper available to a fresh instance:
[[[language=smalltalk
KANetworkPrinter >> resupply: paperSheets
supply := supply + paperSheets
]]]
For convenience, we can provide an extended constructor to create printers with a non-empty supply in one message:
[[[language=smalltalk
KANetworkPrinter class >> withAddress: anAddress initialSupply: paperSheets
^ (self withAddress: anAddress)
resupply: paperSheets;
yourself
]]]
@@todo Define accessors and the ==printOn:== method, for debugging. Test the behavior of printer nodes.
!!!! Servers answer requests
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:
[[[language=smalltalk
KANetworkNode subclass: #KANetworkServer
instanceVariableNames: ''
classVariableNames: ''
category: 'NetworkSimulator-Nodes'
]]]
[[[language=smalltalk
KANetworkServer >> consume: aPacket
| response |
response := aPacket payload asUppercase.
self send: (KANetworkPacket
from: self address
to: aPacket sourceAddress
payload: response)
]]]
@@todo Define accessors and the ==printOn:== method, for debugging. Test the behavior of server nodes.
!! Cycles and routing tables
Let's now model a more realistic network with a cycle between three central nodes:
+file://images/lan-routes.svg|width=50+
Since we want to keep the previous tests unchanged, we define a new test class with a different ==buildNetwork== method:
[[[language=smalltalk
TestCase subclass: #KARoutingNetworkTest
instanceVariableNames: 'net'
classVariableNames: ''
category: 'Kata-NetworkSimulator-Tests'
]]]
[[[language=smalltalk
KARoutingNetworkTest >> setUp
self buildNetwork
]]]
[[[language=smalltalk
KARoutingNetworkTest >> buildNetwork
| routers |
net := KANetwork new.
routers := #(A B C) collect: [ :each | KANetworkHub withAddress: each ].
net connect: routers first to: routers second.
net connect: routers second to: routers third.
net connect: routers third to: routers first.
#(a1 a2) do: [ :addr |
net connect: routers first to: (KANetworkNode withAddress: addr) ].
#(b1 b2 b3) do: [ :addr |
net connect: routers second to: (KANetworkNode withAddress: addr) ].
net connect: routers third to: (KANetworkNode withAddress: #c1)
]]]
@@todo automatic routing?
% Local Variables:
% tab-width: 4
% indent-tabs-mode: nil
% End: