Sven Van Caekenberghe - September 2010 - Last updated April 2011
Warning: this guide is mostly obsolete now, refer to the up to date documentation on the Zn homepage - May 2012
Zinc HTTP Components is an open-source Smalltalk framework to deal with the HTTP networking protocol. This is a new project (started September 1st 2010) that is currently under heavy development. Our long term goals are very ambitious: we want to reach the level of functionality, scope, architectural clarity and maturity of the Apache HTTP Components library. Our short term goal is to offer working HTTP client functionality to support fundamental features in a Smalltalk image. For the time being, Pharo is our reference platform.
This document is an high-level, code-oriented introduction, overview and tutorial.
Back to the project's main page
The primary HTTP client is currently available on the class side of ZnClient. It implements one-shot GET, PUT, POST, DELETE and HEAD requests with optional basic authentication. These methods return ZnResponse objects. There are also some special variants that retrieve and return parsed images of types GIF, JPEG and PNG. When they need data (as for PUT and POST requests) they expect a ZnEntity (actually, any object implementing #contentType, #contentLength and #writeOn: should do). Proxy support is handled transparently (see ZnUtils class>>#socketStreamToUrl:) and is using the settings in your Smalltalk image (HTTPSocket). There are also two more advanced clients: ZnHttpClient and ZnFixedClient. Here are some examples:
ZnClient get: 'http://caretaker.wolf359.be/small.html'. ZnClient getPng: 'http://www.pharo-project.org/images/pharo.png'. ZnClient put: 'http://myserver.com:8080/myresource/1' data: (ZnEntity with: #(1 2 3 4)). ZnClient head: 'http://myserver.com:8080/myresource/1'. ZnClient delete: 'http://myserver.com:8080/myresource/1' username: 'john' password: 'secret'. | entity | (entity := ZnApplicationFormUrlEncodedEntity new) at: 'token' put: 'F437B3D0'; at: 'username' put: 'john'; at: 'password' put: 'secret'. ZnClient post: 'http://somewhere.com/login' data: entity.
Most HTTP client access in a Smalltalk image (Pharo or Squeak) goes through a set of class methods on HTTPSocket. This could be seen as an API (although not a very good one). The class ZnHTTPSocketFacade implements this API using Zinc HTTP Components. This includes the weird error handling (returning Strings!).
A special package called 'Zinc-Patch-HTTPSocket' has a set of Monticello extension methods that (destructively) overwrite these API methods of HTTPSocket and route each method to ZnHTTPSocketFacade and thus to Zinc HTTP Components. Loading this package thus switches HTTP client access in your image to a new implementation. This means that Monticello for example will now operate using Zinc HTTP Components for HTTP repositories.
The primary HTTP server is currently implemented in the class ZnServer. On its own, this server responds with a welcome page on /, and various debugging and testing handlers (goto /help for an overview) and responds with file not found for everything else.
The server can do basic authentication: just supply an object that responds to #authenticateRequest:do: to accept or reject requests to #authenticator:. See for example ZnBasicAuthenticator. To extend the server, you can set its delegate: this is an object that responds to #handleRequest: that you pass to #delegate:. The argument is a ZnRequest, the result should be a ZnResponse. By default, the delegate is set to ZnDefaultServerDelegate. The current default server is ZnMultiThreadedServer and handles multiple request/response cycles on the same connection.
Although simple, the current server already handles three use cases quite elegantly: static web file serving (through ZnStaticWebServerDelegate), monticello repository serving (through ZnMonticelloServerDelegate) and Seaside 3 (through ZnZincServerAdaptor, use the Seaside Control Panel). Starting a server is simple, here are some examples:
ZnServer startDefaultOn: 8080. "Now surf to http://localhost:8080 Try http://localhost:8080/echo/foo and http://localhost:8080/does-not-exist.html" ZnServer default authenticator: (ZnBasicAuthenticator username: 'foo' password: 'secret'). "Now try again" ZnServer default authenticator: nil. ZnServer default delegate: (ZnValueDelegate with: [ :request | ZnResponse ok: (ZnEntity with: 'You asked for ', request uri printString) ] ). "You will get a simple text/plain response to every request" ZnServer default delegate: ((ZnStaticFileServerDelegate new) prefixFromString: 'MyMac/Global'; directory: (FileDirectory on: '/Library/WebServer/Documents'); yourself). "Now try http://localhost:8080/MyMac/Global/ (provided there is an index.html in /Library/WebServer/Documents)" ZnServer default delegate: ((ZnMonticelloServerDelegate new) directory: (FileDirectory on: '/Users/sven/Tmp/monticello'); yourself). "Now use this as your own Monticello repository" MCHttpRepository location: 'http://localhost:8080' user: '' password: '' ZnServer stopDefault. "Start a Seaside 3 adaptor (or use the Seaside Control Panel)" (ZnZincServerAdaptor port: 8080) start
The main goal of the Zinc HTTP components is to provide a clean and elegant, flexible, easy to use and easy to understand framework for dealing with the HTTP protocol. The foundation should be a good object model, mapping clearly to the concepts of the HTTP protocol.
HTTP is the foundation of data communication for the World Wide Web and functions as request/response protocol between a client and a server. A clients sends a request message to a server over a TCP connection (SocketStream). The server replies by sending a response message back. HTTP messages (ZnMessage) consist of a header and an optional enclosed entity (ZnEntity). The header of a request (ZnRequest) consists of a request line (ZnRequestLine) and a collection of headers (ZnHeaders). The header of a response (ZnResponse) consists of a status line (ZnStatusLine) and a collection of headers (ZnHeaders).
The most frequently used request is a called a GET request, it ask for a web resource (most often an HTML web page). Resources are identified by URL's (Url). Here is some code a client might use to set up a request:
| request | request := ZnRequest get: 'http://caretaker.wolf359.be/small.html'. Transcript cr; show: request requestLine; cr; show: request headers; cr; cr. request writeOn: Transcript. Transcript flush.
If you evaluate the above code, you should see the following output on the Transcript:
a ZnRequestLine(GET /small.html) a ZnHeaders('Accept'->'*/*' 'Host'->'caretaker.wolf359.be' 'User-Agent'->'Zinc HTTP Components 0.1' ) GET /small.html HTTP/1.1 Accept: */* User-Agent: Zinc HTTP Components 0.1 Host: caretaker.wolf359.be
As a subclass of ZnMessage, ZnRequest can be written to a stream using #writeOn: (like in the example above) or can be parsed from a stream using #readFrom:. The most frequently used response in its simplest form is an OK (code 200) response with a static buffered entity containing an HTML page as body or payload. Here is some code a server might use to set up a response:
| entity response | entity := ZnStringEntity html: '<html><head><title>Hi!</title><head><body><h1>Hi there!</h1></body></html>'. Transcript cr; show: entity. response := ZnResponse ok: entity. Transcript cr; show: response statusLine; cr; show: response headers; cr; cr. response writeOn: Transcript. Transcript flush.
If you evaluate the above code, you should see the following output on the Transcript:
a ZnStringEntity( text/html 74B <html><head><title>Hi!</title><head><body><h1>Hi there!</h1></body></html> ) a ZnStatusLine(200 OK) a ZnHeaders('Content-Length'->'74' 'Content-Type'->'text/html' 'Date'->'Tue, 21 Sep 2010 21:30:52 GMT' 'Server'->'Zinc HTTP Components 0.1' ) HTTP/1.1 200 OK Content-Type: text/html Content-Length: 74 Date: Tue, 21 Sep 2010 21:30:52 GMT Server: Zinc HTTP Components 0.1 <html><head><title>Hi!</title><head><body><h1>Hi there!</h1></body></html>
Note how the entity knows its mime type and length. Also, note how request and response headers are similar but not the same. With these basic objects, we can do an elementary HTTP client request.
| request stream response | request := ZnRequest get: 'http://caretaker.wolf359.be/small.html'. stream := ZnUtils socketStreamToUrl: request url. response := [ request writeOn: stream. stream flush. ZnResponse readFrom: stream ] ensure: [ stream close ]. response.
Inspect the above code and if all is well, you should see a 200 OK response, with an entity of type text/html and some HTML content. If you look at the headers, you'll see that an Apache 2 server generated the response.
The above code is the essense of client side HTTP: open a connection, write a request, flush, read a response, close (or repeat). Similary, the essense of server side HTTP is: open a server socket, accept an incoming connection, read a request, generate and write a response, flush and close (or repeat).
HTTP headers (ZnHeaders) are a collection of key - value pairs, much like a Dictionary. The standard access protocol is available: #at:, #at:ifAbsent:, #at:put:. Keys are stored and compared in a canonical format (capitalized subparts) to deal with the fact that HTTP headers keys are case insensitive.
Most of the time, there is only one key - value combination per key. Certain keys can have multiple values that are merged together, for these there is #at:ifPresentMerge:. Certain keys can occur more than once, these are dealt with using #at:add. The getters will then return an Array. The enumeration protocol will handle them transparently.
ZnEntity is an abstract superclass that holds something described by a content type (#contentType) and content length (#contentLength) that is capable of writing itself on a stream (#writeOn:) or reading itself from a stream (#readFrom:).
The most straight forward concrete entity subclasses are ZnStringEntity and ZnByteArrayEntity. These hold static buffered contents of type String and ByteArray respectively. The distinction is made either explicitely or derived from the mime type (ZnMimeType>>#isBinary). These entities are repeatable (you can access their contents multiple times). Like all Zinc HTTP Components' base objects, entities are used in two directions: to receive or to send data. ZnStringEntities use an encoder to convert from Unicode characters to bytes and vice versa. The default encoder is UTF-8 (ZnUTF8Encoder).
A more advanced type of entity is ZnStreamingEntity. Such an entity holds its contents in a read stream. This can be more efficient since no unneeded copies are made and held into memory. These entities are not repeatable (their stream can be read only once).
When you read an response from a stream, you can use #readStreamingFrom: instead of #readFrom:. The response will then hold a streaming entity (provided there is an entity of course). As a user, you must then read the actual bytes or characters from the embedded stream. You must not close this embedded stream. This reading must take place before the original socket stream is closed. In the rare case that you did not read all content, you should call #consumeContent.
Similary, when providing an entity to be written on a stream using #writeOn: you set the streaming entity's stream to an open read stream that you create. At the same time you have to set the content length and type, as with any entity. When the entity is written, your read stream will be copied efficiently to the socket stream and will then be closed.
Certain special content might be handled by special purpose entities. One such entity is ZnApplicationFormUrlEncodedEntity which holds a collection of key - value fields. This entity class is used to deal with POST form data. ZnMultiPartFormDataEntity will do the same for a more complicated type of POST form data.
Not all classes are documented here.
Please have a look at: ZnFixedClient, ZnHttpClient, and the support classes
ZnCookie, ZnCookieJar, ZnCredentials hierarchy, ZnUserAgentSettins and ZnUserAgentSession.
The unit tests are also a good place to learn how to use them.
Zinc HTTP Components is not yet complete and has not yet reached its end goals. The todo list is on the project's main page.