[REBOL] The REBOL async:// tutorial - take 1
From: maarten:vrijheid at: 3-Mar-2004 15:48
See below. Enjoy it. Republish it.
==================================================
The REBOL async:// tutorial - Take 1
Maarten Koopmans, code samples and root protocol by Gabriele Santilli
What is async:// anyway? It is an asynchronous TCP root protocol that can
be used to drive REBOLs hidden async TCP features. This allows network code
to be processed automagically upon certain network events, such as getting
a connection, ready to read/write, and closing a conection.
Once you have set the code to be called upon these events, things will go
by themself if you're in an event loop. This is much like GUI events are
processed automagically when you call 'view.
The first thing you need to do is get the async:// root protocol by
Gabriele. Download it at http://www.rebol.it/giesse/async-protocol.r
Note that this version supports binary transfers only, /lines support is
being worked on.
Now let's start with a simple client script, downloading images in the
background in REBOL/View. First the code:
handler: func [port [port!] state [word! error!] /local tmp cmd] [
if error? :state [print mold disarm state return true]
switch state [
connect [
; do HTTP request
insert port {GET /fg/anen.jpg HTTP/1.0^M^JHost:
www.3dwallpaper.com^M^J^M^J}
false
]
read [false]
write [false]
close [
; get data
data: copy port
close port
;print copy/part data find data "^M^J^M^J"
data: to binary! find/tail data "^M^J^M^J"
other/image: attempt [load data]
other/text: ""
show other
false
]
]
]
port: open async://www.3dwallpaper.com:80
port/awake: :handler
view layout [
across me: box 100x100 random 255.255.255 0:00:00.5 feel [
engage: func [f a e] [
if a = 'time [
me/color: random 255.255.255
show me
]
]
]
other: box 100x100 255.255.255 "Downloading image..." Return
Area 208x100 "You can type here while downloading."
]
Here is how it works: you open an async port using normal URI syntax in the
middle of the script. You can see this in the lines:
port: open async://www.3dwallpaper.com:80
port/awake: :handler
The second line sets the callback function. This function is the crucial
part for async:// based implementations, as it provides code that can be
executed for all the events. So, let's take a look a the handler function!
It starts with the following piece of code:
handler: func [port [port!] state [word! error!] /local tmp cmd] [
if error? :state [print mold disarm state return true]
As you can see, we get two parameters: The port that we can read/write to
and the state. The state can be an error! or a word!. If it is an error we
need to clean up and the above code shows briefly one way how this can
be done on
the second line. If it is a word it can have any of four values:
- 'connect, signalling that we have just acquired a connection (client-side
only)
- 'read , signalling that we can have data to read
- 'write , signalling that we can write data (again)
- and 'close , signalling that the other side has closed the connection.
So what we do next in our handler is a switch based on the state, executing
different code for each possible state.
switch state [
connect [
; do HTTP request
insert port {GET /fg/anen.jpg HTTP/1.0^M^JHost:
www.3dwallpaper.com^M^J^M^J}
false
]
read [false]
write [false]
close [
; get data
data: copy port
close port
;print copy/part data find data "^M^J^M^J"
data: to binary! find/tail data "^M^J^M^J"
other/image: attempt [load data]
other/text: ""
show other
true
]
]
As you see, we simply do a HTTP GET for an image, and write it to file and
the view it. Upon 'connect we immediately make our request. Why not in the
'write event? The 'write event is triggered everything we have something
written and we canw rite more. So the first time we always have to write
from another event!!!
Now, once the data is inserted we return false. This indicates that we
shouldn't be removed from the system/ports/wait-list list, where all ports
are stored (at least async:// and event:// ones). As you can see we did
return true on error.
The read and write states COULD be used to do some reading from data or
write more, but as this is a very simple request-response, that's not
necessary, so they just return false. So... the other side returns the data
and closes the connection. Upon close we simple read it ALL (again this
could have been done in 'read using a temporary buffer) and close the port.
We update the image and.... done!
Now for a simple server:
First we add a listening server port to the system/ports/wait-list, like:
either error? try [listen: open/no-wait tcp://:8000] [
port: open async://localhost:8000
port/awake: do handler
] [
listen/awake: func [l /local p] [
print "Got connection."
p: first listen
remove find system/ports/wait-list listen
port: make port! [scheme: 'async sub-port: p]
open port
port/awake: do handler
false
]
insert tail system/ports/wait-list listen
port: none
]
As you can see, its awake function convert the accepted port to an async
one and sets the handler. So what is the handler then?
handler: [ use [ buffer ][
buffer: copy []
func [port [port!] state [word! error!] /local tmp cmd] [
if error? :state [print mold disarm state return true]
switch state [
connect [print "Connected." false]
read [
append buffer copy port
while [tmp: find buffer newline] [
cmd: copy/part buffer tmp
remove/part buffer next tmp
do-cmd cmd
]
false
]
write [false]
close [print "Peer closed connection." close port true]
]
]
]
]
The first thing to notice is the fact that we use 'use to create a context
that returns a function! value. This function (and only this particluar
value) has access to its buffer. By doing the handler block in the server
part above, every accepted port gets a copy of theis function value with
its own "static" buffer space. A very simple but effective trick.
Now, on to the events. 'Connect does nothing, as we are already connected.
This happened when the awake function of our TCP server port was called. We
chose 'write and 'close also to do nothing. Why? We only want to receive a
command and then do it. The reading is done in 'read, we append what we
have read to the buffer. If we see a newline, we execute it and clear the
buffer until the newline. And then we return false. The fact that we return
false implies that we may read more than one connection.
Also note that 'close does nothing, but *if* a port is closed, it also
closes. Of course you can use 'close for all sorts of clean-up. Now if you
this code
do-cmd: func [cmd] [
other/color: attempt [load cmd]
show other
]
view layout [
across me: box 100x100 random 255.255.255 0:00:01 feel [
engage: func [f a e] [
if a = 'time [
me/color: random 255.255.255
if port [insert port join me/color newline]
show me
]
]
]
other: box 100x100 random 255.255.255 Return
Area 208x100
]
halt
to the above, you have a simple GUI with a command server.
You are now ready to play with async:// and enhance the code adding
substates to 'read or 'write to implement advanced protocols. Its just more
of the same. I hope you keep the use [ ] [ ] trick in mind for server
handlers, I found it a very easy way to add extra substates this way.
Now.... start coding!