|Starting up a simple file server using HttpServerStreams|
January 23, 2007, 7:31:28 pm
I thought people might be interested in how to use the HttpServerStreams package in public store. What is it? It's a HTTP/1.1 streaming server. It's pretty simple to use, so I'll show you how you build up from completely raw stuff to high level stuff. Let's get started:
server := HttpServerStreams.Server at: 'http://localhost:8080' asURI. server isDebug: true. server responseBlock: [:request | request response contentType: 'text/plain'; bodyBlock: [:stream | stream asStringStream nextPutAll: 'Hello World']]. server start
So this is pretty basic. It starts a server on localhost:8080 which will always respond with the message 'Hello World' no matter what you throw at it. Nothing special there - except for the way it is done.
First we give it a block to deal with incoming requests - this is our responseBlock:. It takes a request argument which has a #response object. On the response object we set the contentType: and any other response headers we want to set up. Then, we do something extra special - we give it a bodyBlock:
What does this mean? It is a streaming block - we're given a stream back to the client and we can write whatever we want on it. In the example above, I use StreamWrappres to wrap up the binary 'stream' as a asStringStream. This lets me put Hello World on the stream without calling asByteArray.
Now - on to a more useful example. Let's make an unsafe http file server - the dumbest one we can imagine.
server := HttpServerStreams.Server at: 'http://localhost:8080' asURI. server responseBlock: [:request | | filename | filename := Filename fromComponents: request url path. request response contentType: filename guessMimeType; bodyBlock: [:writeStream | filename getReadStreamDo: [:readStream :encodedReadStream :mediaType :hasChanged | writeStream nextPutStream: readStream bufferSize: 32768]]]. server start
In the example above, I turned off isDebug so that if the filename doesn't exist, the client will receive an error response automagically saving us the trouble of testing if the file exists and sending back a nice 404 message :)
We make the filename object from our url path - this is where it is dangerous, users might be able to access files they shouldn't. I'd recommend you prepend another path on to the filename to avoid this sort of problem. But since we're hacking around on localhost only, this'll do for now.
We use the #guessMimeType method to set our contentType and then the rest of the magic happens in the bodyBlock again. We're going to stream back the file - which means we can comfortably send gigs of data back to the client without breaking a sweat in the smalltalk image.
The getReadStreamDo: method is part of URIResources which lets us act as a streaming client. You can also use HttpClientStreams in public store to do streaming HTTP/1.1 downloads off the internet. You'll also be able to use the NetClients library in VisualWorks 7.5 to do streaming downloads as well. Kudos to the Net team at Cincom for that work.
So I'm using it here to stream off disk. It gives me back a raw binary read stream as well as a pre-wrapped encoded read stream. This is based off the character set of the content that we're reading. We can ignore that stream in this example - we're transporting binary to binary.
Then StreamWrappers gives us another cool method here - nextPutStream:bufferSize:. This transfers data from one stream to the next in chunks of 'bufferSize'. In this case I picked 32k as an arbitrary value.
You should now be able to grab things from your image directory, such as http://localhost:8080/visual.sou
Now finally, one more level is available in this framework. Apart from the various richness in the client and server response and request objects, there's also a servlets library you can tack on the front of the Server class. Load up HttpServerServlets and you can make subclasses of the class Servlet.
Implement doGet: or doPost: - methods like TRACE, HEAD, OPTIONS etc are handled for you automagically. In your doGet: method you can write code such as:
MyServlet>>doGet: request request response contentType: 'text/plain'; bodyBlock: [:stream | self streamOutMyUIOn: stream]
At that point we're back to the same handling as we had before, except we have a new layer doing URL indirection based on servlet name. We can start this kind of server as:
server := HttpServlets.ServletServer at: 'http://localhost:8080' asURI. server addServlet: (MyServlet named: 'me'). server start
Now we can go to http://localhost:8080/me/ and get our response back. The servlets server also handles sessions, session timeouts etc. There's plenty of stuff there on the server side and just as much on the client side of HttpStreams too. Take a look some time if this sort of thing interests you.