16 April 2012

Peer to Peer networking in Pharo Smalltalk

Ok, now sometimes you want to make something with a network. And when you do, you're out of luck. All you'll find on official Pharo - the collaborative book about Sockets is:
| server client |
server := Socket newTCP.
server listenOn: 12345 backlogSize: 4.
server waitForConnectionFor: 600.
client := server accept.
client receiveData
now how are you supposed to do anything with that???

This post will probably clarify how the Sockets work in Pharo Smalltalk, and will also share the solution for P2P connectivity that's just awesome for some simple multiplayer gaming.
First of all: one tool that can help you during a development is a netcat. It's usually pre-installed on the Linux and Unix systems, and here is how you use it:

For Server:
echo "Smalltalk" | nc -lk 12345
This will send a "Smalltalk" string to anyone that connects to 12345 port of your computer.
-l stands for listener e.i. acts like a server listening for requests.
-k forces netcat to stay listening for another connection after its current connection is completed.

For Client:
echo "Smalltalk" | nc localhost 12345
This will connect to the 12345 port of the localhost (or any other provided host name or IP) and send it a "Smalltalk" string.


Sockets in Pharo:
First of all you need to allocate and initialize Socket. In Pharo Sockets support both TCP and UDP protocols but we'll forces on TCP for now.
socket := Socket newTCP.
socket is just a variable (instance variable) name. We'll use it later.

Now we can use this Socket to listen for a request or to sent the request itself.

Connecting:
socket connectToHostNamed: host port: port.
Here a session will try to connect to specified host and port.

Listening:
socket listenOn: port backlogSize: 50 interface: ip.
session := socket waitForAcceptFor: 60.
listenOn tells the Socket that it should listen the specified port and interface is determined by your's IP (ip is passed like: #[192 168 1 5]) that you should input manually. But it won't work unless you'll send waitForAcceptFor: to your socket specifying how long it should wait for incoming connection. The returned result if the connection have been established is also a socket that you can use to communicate with your client. If the timeout has been reached the return value will be nil.
Some time I want it to wait for ever until someone connects. This can be accomplished the next way:
[session isNil] whileTrue:
[
socket listenOn: port backlogSize: 50 interface: ip.
session := socket waitForAcceptFor: 60.
]
Remember to put all this into a separate thread, or it'll freeze all your VM while waiting for connection.


Talking:
Smalltalk is all about talking ;) So as you have you'r Sockets hooked up, you can exchange messages between them.
First of all to check if you have some unread stuff in you'r Socket, the next statement will return true:
socket dataAvailable.

Also you can use a numerous methods for reading and writing anything across this connection. But I want to suggest you one very generic solution:

Writing:
send: data
(self paired) ifTrue: [
session sendSomeData: (ReferenceStream streamedRepresentationOf: data)]

Reading:
read
(self unread) ifTrue: [
^ReferenceStream unStream: session receiveData].
^nil.
As you can see these are two methods that use an instance variable session that is a socket connected to another peer. Also paired is a method that tells if the peer is paired e.i. if connection is established
(session isNil not) and: (session isConnected)
Also don't try to read if you have nothing to read or your VM will stuck once again.
What's awesome about this implementation option is that now you can send anything you want over the internet. Like point, collection, custom class. The only thing you need to care about is that it should be declared in receiver's image too.

One last thing that you want to do is Close a Socket when you're done.
The best way is to use
socket closeAndDestroy.
That will first try to close socket and then terminate and destroy it.

One more thing:
Remember the Listener implementation? Here is a full code that I suggest:
process := [
|socket|
socket := Socket newTCP.
[
[session isNil] whileTrue:
[
socket listenOn: port backlogSize: 50 interface: ip.
session := socket waitForAcceptFor: 60.
Processor yield.
]
socket closeAndDestroy.
] fork.
Note that we are storing out forked block in a variable so we can always terminate it when we need it to.
We also close out "meeting" Socket because we want to work only with one pier. And we yield a processor time every minute so other processes with the same priority can work to.


Walkie-Talkie Project

All this stuff is composed into a lib that you can use within your project for Peer to Peer needs. The project can be found here: http://ss3.gemstone.com/ss/SleepyCoders.html.
Also you can check out all the code more precisely this way.

Feel free to share comment and request more stuff.
And let the Smalltalk make your Day!

1 comment:

  1. Whereas when we were young it was incredible to have a walkie-talkie unit that would fit in our hands, now they make walkie talkie headsets, walkie-talkie watches, and even walkie-talkies throat mics. two way radios

    ReplyDelete