diff --git a/DeploymentWeb/DeployForProduction.pillar b/DeploymentWeb/DeployForProduction.pillar index 92ea8cd..7e0477f 100644 --- a/DeploymentWeb/DeployForProduction.pillar +++ b/DeploymentWeb/DeployForProduction.pillar @@ -218,4 +218,29 @@ $ sudo ln -s /etc/monit/monitrc.d/apache2 /etc/monit/conf.d/apache2 $ sudo monit reload ]]] -% LocalWords: monit +!!! Put an HTTP server in front of your web application + +You may wonder why it is a good idea to put a web server like Apache or Nginx in front of your Pharo web application? Mature web server fully implements standards and commoditized functionalities, e.g. virtual hosting handling (multiple domains on the same IP address), URL rewriting, etc. They are also more stable, have built-in mechanism for binding to privileged ports (below 1024) as root and then executing as a non-privileged user, and more robust to attacks. They can also be used to serve static content and to display a maintenance page. + +Here is a simple Apache configuration that can be used to redirect the incoming traffic (internet) on the default HTTP port (80) to your Pharo web application running on the local interface on the port 8080. +[[[language=apache + + ServerName mydomain.com + #ServerAlias anothercooldomain.org + + ProxyPreserveHost On + ProxyRequests Off + + Order allow,deny + Allow from all + + ProxyPass / http://127.0.0.1:8080/ + ProxyPassReverse / http://127.0.0.1:8080/ + + ErrorLog /var/log/apache2/myapp-error.log + CustomLog /var/log/apache2/myapp-access.log combined + +]]] +The first section declares the full server name (including the domain), the second one activates the proxy to forward the traffic to your Pharo web application, and the last one creates dedicated log files. + +% LocalWords: monit, diff --git a/NeoJSON/NeoJSON.pier b/NeoJSON/NeoJSON.pier index 1d86fa4..9d02dc9 100644 --- a/NeoJSON/NeoJSON.pier +++ b/NeoJSON/NeoJSON.pier @@ -106,8 +106,8 @@ To automatically convert keys to symbols, pass ==true== to ==propertyNamesAsSymb [[[language=smalltalk (NeoJSONReader on: ' { "x" : 1, "y" : 2 } ' readStream) - propertyNamesAsSymbols: true; - next + propertyNamesAsSymbols: true; + next ]]] The result of this expression is a dictionary with ==#x== and ==#y== as keys. @@ -128,116 +128,105 @@ NeoJSONWriter toString: { Float pi. true. false. 'string' }. NeoJSONWriter toString: { #a -> '1' . #b -> '2' } asDictionary. ]]] -Above expressions return a compact string (/i.e./, with neither indentation nor new lines). To get a nicely formatted output, use ==toStringPretty:== like this: +Above expressions return a compact string (''i.e.'', with neither indentation nor new lines). To get a nicely formatted output, use ==toStringPretty:== like this: [[[language=smalltalk NeoJSONWriter toStringPretty: #(1 2 3). ]]] -NeoJSONWriter can output either in a compact format (the default) or in a pretty printed format. - -SD: examples how to do that. - - -In order to use the generic mode, you have to convert your domain objects to and from Dictionaries and SequenceableCollections. This is relatively easy but not -very efficient, depending on the use case. +In order to use the generic mode, you have to convert your domain objects to and from ==Dictionary== and ==SequenceableCollection==. This is relatively easy but not very efficient, depending on the use case. !! Schemas and Mappings -NeoJSON allows for the optional specification of schemas and mappings to be used when writing and/or when reading. A ==NeoJSONMapper== holds a number of -schemas. Each schema is identified by either a class or a symbol. Each schema specifies a mapping, an object that will help in doing the actual reading or -writing. +NeoJSON allows for the optional specification of schemas and mappings to be used when writing or reading. -The most common mapping deals with objects that define a number of named properties or attributes. These can be defined based on instance variables (optionally -derived by reflection), accessors (getter/setter pairs) or even blocks. Such an object mapping is identified by a Smalltalk class, which is also used to create -new instances. Each property mapping can have an optional value schema to be used recursively when reading and/or writing property values. +When writing, mappings are used when arbitrary objects are seen. For example, in order to write an array of points, you could do as follows: -The less common custom mapping holds a generic reader and/or writer block to deal with special cases such as specific collection types with an optional schema -for the elements, or a direct mapping of semi primitive types such as Date or DateAndTime. +[[[language=smalltalk +String streamContents: [ :stream | + (NeoJSONWriter on: stream) + prettyPrint: true; + mapInstVarsFor: Point; + nextPut: (Array with: 1@3 with: -1@3) ]. +]]] -A mapping can be specified explicitly on a mapper, or can be resolved using the #neoJsonMapping: class method. +Collections are handled automatically, like in the generic case. -Here are some examples of mappings: +When reading, a mapping is used to specify what Pharo object to instantiate and how to instantiate it. Here is a very simple case, reading a map as a point: [[[ -mapper mapAllInstVarsFor: Point. - -mapper for: TestObject do: [ :mapping | - mapping mapInstVars: #(id name). - (mapping mapInstVar: #timestamp to: 'created-at') valueSchema: DateAndTime. - (mapping mapInstVar: #points) valueSchema: #ArrayOfPoints. - (mapping mapInstVar: #bytes) valueSchema: ByteArray ]. - -mapper for: DateAndTime customDo: [ :mapping | - mapping decoder: [ :string | DateAndTime fromString: string ]. - mapping encoder: [ :dateAndTime | dateAndTime printString ] ]. - -mapper for: #ArrayOfPoints customDo: [ :mapping | - mapping listOfElementSchema: Point ]. - -mapper for: #DictionaryOfPoints customDo: [ :mapping | - mapping mapWithValueSchema: Point ]. +(NeoJSONReader on: ' { "x" : 1, "y" : 2 } ' readStream) + mapInstVarsFor: Point; + nextAs: Point. +]]] -mapper for: ByteArray customDo: [ :mapping | - mapping listOfType: ByteArray ] -The classes NeoJSONReader and NeoJSONWriter are subclasses of NeoJSONMapper. When writing, mappings are used when arbitrary objects are seen. For example, in -order to be able to write an array of points, you could do as follows: +Since JSON lacks a universal way to specify the class of an object, we have to specify the target schema that we want to use as an argument to ==nextAs:==. -String streamContents: [ :stream | - (NeoJSONWriter on: stream) - prettyPrint: true; - mapInstVarsFor: Point; - nextPut: (Array with: 1@3 with: -1@3) ]. +To define the schema of the elements in a list, write something like the following: +[[[ +(NeoJSONReader on: ' [{ "x" : 1, "y" : 2 }, { "x" : 3, "y" : 4 }] ' readStream) + mapInstVarsFor: Point; + for: #ArrayOfPoints customDo: [ :mapping | mapping listOfElementSchema: Point ]; + nextAs: #ArrayOfPoints. ]]] -Collections are handled automatically, like in the generic case. When reading, a mapping is used as a binding or an explicit type specifying what Pharo objects -that you want to read. Here is a very simple case, reading a map as a point: +The above expression returns an array of 2 points. As you can see, the argument to ==nextAs:== can be a class (as seen previously) be also any symbol, provided the mapper knows about it. +To get an ==OrderedCollection== instead of an array as output, use the ==listOfType:== message: -[[[ -(NeoJSONReader on: ' { "x" : 1, "y" : 2 } ' readStream) - mapInstVarsFor: Point; - nextAs: Point. +[[[language=smalltalk +(NeoJSONReader on: ' [ 1, 2 ] ' readStream) + for: #Collection customDo: [ :mapping | mapping listOfType: OrderedCollection ]; + nextAs: #Collection. ]]] -Since JSON lacks a universal way to specify the class of an object/map, we have to specify the target schema that we want to use as an argument to ==nextAs:==. +To specify how values in a map should be instantiated, use the ==mapWithValueSchema:==: -With custom mappings, it is possible to +[[[language=smalltalk +(NeoJSONReader on: ' { "point1" : {"x" : 1, "y" : 2 } }' readStream) + mapInstVarsFor: Point; + for: #DictionaryOfPoints + customDo: [ :mapping | mapping mapWithValueSchema: Point ]; + nextAs: #DictionaryOfPoints. +]]] --define the schema of the elements of a list --define the schema of the elements of a list as well as the class of the list --define the schema of the values of a map In fact, NeoJSONCustomMapping can be extended to implement even more specialized mappings. +The above expression returns a ==Dictionary== with 1 key-value pair =='point1' -> (1@2)==. -Finally, here is a more complex example, reading a list of maps as an array of points: +You can go beyond pre-defined messages and specify a decoding block: -[[[ -(NeoJSONReader on: '[ { "x" : 1, "y" : 2 }, { "x" : 3, "y" : 4 } ]' readStream) - mapInstVarsFor: Point; - for: #ArrayOfPoints customDo: [ :mapping | - mapping listOfElementSchema: Point ]; - nextAs: #ArrayOfPoints. +[[[language=smalltalk +(NeoJSONReader on: ' "2015/06/19" ' readStream) + for: DateAndTime + customDo: [ :mapping | mapping decoder: [ :string | DateAndTime fromString: string ] ]; + nextAs: DateAndTime. ]]] -NeoJSON deals efficiently with mappings: the minimal amount of intermediary structures are created, which is quite different from the generic case. - -!!Internals +The above expression returns an instance of ==DateAndTime==. The message ==encoder:== is used to do the opposite, ''i.e.'' convert from a Smalltalk object to JSON: -On modern hardware, NeoJSON can write or read in the tens of thousands of small objects per second. Several benchmarks are included in the unit tests package. +[[[language=smalltalk +String streamContents: [ :stream | + (NeoJSONWriter on: stream) + for: DateAndTime customDo: [ :mapping | mapping encoder: #printString ]; + nextPut: DateAndTime now ]. +]]] +The above expression returns a string representing the current date and time. +NeoJSON deals efficiently with mappings: the minimal amount of intermediary structures are created. +On modern hardware, NeoJSON can write or read tens of thousands of small objects per second. Several benchmarks are included in the unit tests package. !!Emitting null values When mapping my objects with NeoJSONWriter, it is not writing the properties whose values are null. [[[ - NeoJSONPropertyMapping>>#writeObject: anObject on: jsonMapWriter - | value | - value := getter value: anObject. - value - ifNotNil: [ jsonMapWriter writeKey: propertyName value: value as: valueSchema ] + NeoJSONPropertyMapping>>#writeObject: anObject on: jsonMapWriter + | value | + value := getter value: anObject. + value + ifNotNil: [ jsonMapWriter writeKey: propertyName value: value as: valueSchema ] ]]] It is a good default (saves space, and cpu cycles). But in there are cases where the consumer of the JSON objects expect the objects to have all the attributes diff --git a/WebSockets/WebSockets.pier b/WebSockets/WebSockets.pier index ab89c4d..ddc8ceb 100644 --- a/WebSockets/WebSockets.pier +++ b/WebSockets/WebSockets.pier @@ -1,14 +1,14 @@ !WebSockets @cha:webSockets -Sven Van Caekenberghe +% Sven Van Caekenberghe -September 2012 - Updated January 2013 -(This is a draft) +% September 2012 - Updated January 2013 +% (This is a draft) -The WebSocket protocol defines a full-duplex single socket connection over which messages can be sent between a client and a server. It simplifies much of the -complexity around bi-directional web communication and connection management. WebSocket represents the next evolutionary step in web communication compared to -Comet and Ajax. +The WebSocket protocol defines a full-duplex single socket connection over which messages can be sent between a client and a server. It simplifies much of the complexity around bi-directional web communication and connection management. +WebSocket represents the next evolutionary step in Web communication compared to Comet and Ajax. +And of course, Zinc HTTP Components supports Web sockets as you will discover throughout this chapter. !!An Introduction to WebSockets @@ -16,26 +16,21 @@ HTTP, one of the main technologies of the internet, defines a communication prot lies with the client and each interaction consists of a client request and a server response. When correctly implemented and used, HTTP is enormously scaleable and very flexible. -With the arrival of advanced Web applications mimicking regular desktop applications with rich user interfaces, as well as mobile Web applications, it became -clear that HTTP was not suitable or not a great fit for two use cases: +With the arrival of advanced Web applications mimicking regular desktop applications with rich user interfaces, as well as mobile Web applications, it became clear that HTTP was not suitable or not a great fit for two use cases: -when the server wants to take the initiative and send the client a message -when the client wants to send (many) (possibly asynchronous) short messages with little overhead -In the HTTP protocol, the server cannot take the initiative to send a message, the only workaround is for the client to do some form of polling. For short -messages, the HTTP protocol adds quite a lot of overhead in the form of meta data headers. For many applications, the response (and the delay waiting for it) -are not needed. Previously, Comet and Ajax were used as (partial) solutions to these use cases. +- when the server wants to take the initiative and send the client a message. In the HTTP protocol, the server cannot take the initiative to send a message, the only workaround is for the client to do some form of polling. +- when the client wants to send (many) (possibly asynchronous) short messages with little overhead. For short messages, the HTTP protocol adds quite a lot of overhead in the form of meta data headers. For many applications, the response (and the delay waiting for it) are not needed. -The WebSocket protocol defines a reliable communication channel between two equal parties, typically, but not necessarily, a Web client and a Web server, over -which asynchronous messages can be send with very little overhead. Messages can be any String or ByteArray. Overhead is just a couple of bytes. There is no such -thing as a direct reply or a synchronous confirmation. +Previously, Comet and Ajax were used as (partial) solutions to these use cases. +The WebSocket protocol defines a reliable communication channel between two equal parties, typically, but not necessarily, a Web client and a Web server, over which asynchronous messages can be send with very little overhead. +Messages can be any String or ByteArray. Overhead is just a couple of bytes. There is no such thing as a direct reply or a synchronous confirmation. -Using WebSockets, a server can notify a client instantly of interesting events, and clients can quickly send small notifications to a server, possibly -multiplexing many virtual communications channels over a single network socket. +Using WebSockets, a server can notify a client instantly of interesting events, and clients can quickly send small notifications to a server, possibly multiplexing many virtual communications channels over a single network socket. !!The WebSocket Protocol -Zinc WebSockets implements RFC 6455 http://tools.ietf.org/html/rfc6455, not any of the previous development versions. For an introduction to, both -http://en.wikipedia.org/wiki/WebSocket and http://www.websocket.org are good starting points. +Zinc WebSockets implements RFC 6455 *http://tools.ietf.org/html/rfc6455*, not any of the previous development versions. +For an introduction to, both *http://en.wikipedia.org/wiki/WebSocket* and *http://www.websocket.org* are good starting points. As a protocol, WebSocket starts with an initial setup handshake that is based on HTTP. The initiative for setting up a WebSocket lies with the client, who is sending a so called connection upgrade request. The upgrade request contains a couple of special HTTP headers. The server begins as a regular HTTP server @@ -47,7 +42,10 @@ and to manage keeping alive the connection using ping and pong frames. !!Source Code -The code implementing Zinc WebSockets resides in a single package called 'Zinc-WebSocket-Core' in the Zinc HTTP Components repositories. There is also an accompanying 'Zinc-WebSocket-Tests' package containing the unit tests. The ==ConfigurationOfZincHTTPComponents== has a group called =='WebSocket'== that you can load separately. Here the loading code snippet: +The code implementing Zinc WebSockets resides in a single package called 'Zinc-WebSocket-Core' in the Zinc HTTP Components repository. +There is also an accompanying 'Zinc-WebSocket-Tests' package containing the unit tests. +The ==ConfigurationOfZincHTTPComponents== has a group called =='WebSocket'== that you can load separately. +Here the loading code snippet: [[[ Gofer new @@ -57,18 +55,19 @@ Gofer new (Smalltalk globals at: #ConfigurationOfZincHTTPComponents) project latestVersion load: 'WebSocket'. ]]] -!!!Using Client Side WebSockets +!!Using Client Side WebSockets -An endpoint for a WebSocket is specified using a URL +An endpoint for a WebSocket is specified using an URL: [[[ ws://www.example.com:8088/my-app ]]] -Two new schemes are defined, ==ws://== for regular WebSockets and ==wss://== for the secure (TLS/SSL) variant. The ==host:port== and path specification should be familiar. +Two new schemes are defined, ==ws://== for regular WebSockets and ==wss://== for the secure (TLS/SSL) variant. The ==host:port:== and path specification should be familiar. -Zinc WebSockets supports the usage of client side WebSockets of both the regular and secure variants (the secure variant requires Zodiac TLS/SSL). The API is -really simple, once you open the socket, you use ==sendMessage:== and ==readMessage:== and finally ==close==. +Zinc WebSockets supports the usage of client side WebSockets of both the regular and secure variants. +Note that the secure variant requires loading the Zodiac TLS/SSL Pharo project *http://smalltalkhub.com/#!/~SvenVanCaekenberghe/Zodiac*. +Zinc WebSockets API is really simple, once you opened the socket, you use ==sendMessage:== and ==readMessage:== and finally ==close==. Here is a client side example taking to a public echo service: @@ -89,7 +88,7 @@ different frames of the same message. At the other end, these will be joined tog In any non-trivial application, you will have to add your own encoding and decoding to messages. In many cases, JSON will be the obvious choice as the client end is often JavaScript. A modern, standalone JSON parser and writer is NeoJSON. -To use secure web sockets, just use the proper URL scheme ==wss://== as in the following example: +To use secure Web sockets, just use the proper URL scheme ==wss://== as in the following example: [[[ | webSocket | @@ -103,15 +102,13 @@ Of course, your image has to contain Zodiac and your VM needs access to the prop !!Using Server Side WebSockets -Since the WebSocket protocol starts off as HTTP, it is logical that a ZnServer with a special delegate is the starting point. ==ZnWebSocketDelegate== implements -the standard ==handleRequest:== to check if the incoming request is a valid WebSocket connection upgrade request. If so, the matching 101 switching protocols -response is constructed and sent. From that moment on, the network socket stream is handed over to a new, server side ==ZnWebSocket== object. +Since the WebSocket protocol starts off as HTTP, it is logical that a ZnServer with a special delegate is the starting point. ==ZnWebSocketDelegate== implements the standard ==handleRequest:== to check if the incoming request is a valid WebSocket connection upgrade request. If so, the matching 101 switching protocols response is constructed and sent. From that moment on, the network socket stream is handed over to a new, server side ==ZnWebSocket== object. - ==ZnWebSocketDelegate== has two properties. An optional prefix implements a specific path, like ==/my-ws-app==. The required handler is any object implementing - ==value:== with the new web socket as argument. +==ZnWebSocketDelegate== has two properties. An optional prefix implements a specific path, like ==/my-ws-app==. +The required handler is any object implementing ==value:== with the new Web socket as argument. -Let's implement the echo service that we connected to as a client in the previous subsection. In essence, we should go in a loop, reading a message and sending -it back. Here is the code: +Let's implement the echo service that we connected to as a client in the previous section. +In essence, we should go in a loop, reading a message and sending it back. Here is the code: [[[ ZnServer startDefaultOn: 1701. @@ -126,7 +123,7 @@ We start a default server on port 1701 and replace its delegate with a ==ZnWebSo request on to its handler. In this example, a block is used as handler. The handler is given a new connected ==ZnWebSocket== instance. For the echo service, we go into a repeat loop, reading a message and sending it back. -Finally, you can stop the server using +Finally, you can stop the server using: [[[ ZnServer stopDefault. @@ -142,49 +139,68 @@ ConnectionClosed (or its more specific subclass ZnWebSocketClosed) The ==readMessage== call blocks on the socket stream waiting for input until its timeout expires, which will be signaled with a ConnectionTimedOut exception. In most applications, you should just keep on reading, essentially ignoring the timeout for an infinite wait on incoming messages. -This behavior is implemented in the ==ZnWebSocket>>runWith:== convenience method: it enters a loop reading messages and passing them to a block, continuing on -timeouts. This simplifies our example: +This behavior is implemented in the ==ZnWebSocket>>runWith:== convenience method: it enters a loop reading messages and passing them to a block, continuing on timeouts. This simplifies our example: [[[ ZnServer startDefaultOn: 1701. ZnServer default delegate: (ZnWebSocketDelegate handler: [ :webSocket | webSocket runWith: [ :message | - message := webSocket readMessage. webSocket sendMessage: message ] ]). ]]] That leaves us with the problem of ==ConnectionClosed==. This exception can occur at the lowest level when the underlying network connection closes unexpectedly, -or at the WebSocket protocol level when the other end sends a close frame. In either case we have to deal with it as a server. In our trivial echo example, we -can catch and ignore any ConnectionClosed exception. - -There is a handy shortcut method on the class side of ==ZnWebSocket== that helps to quickly set up a server implementing a WebSocket service. +or at the WebSocket protocol level when the other end sends a close frame. In either case we have to deal with it as a server. In our trivial echo example, we can catch and ignore any ConnectionClosed exception or log it as follows: + +% There is a handy shortcut method on the class side of ==ZnWebSocket== that helps to quickly set up a server implementing a WebSocket service. +% BUG in Pharo4 (fixed in bleedingEdge Zinc) +% [[[ +% ZnWebSocket +% startServerOn: 8080 +% do: [ :webSocket | +% [ +% webSocket runWith: [ :message | +% message := webSocket readMessage. +% webSocket sendMessage: message ] ] +% on: ConnectionClosed +% do: [ self crLog: 'Ignoring connection close, done' ] ]. +% ]]] +% Don't forget to inspect the above code so that you have a reference to the server to close it, as this will not be the default server. [[[ -ZnWebSocket - startServerOn: 8080 - do: [ :webSocket | - [ - webSocket runWith: [ :message | - message := webSocket readMessage. - webSocket sendMessage: message ] ] - on: ConnectionClosed - do: [ self crLog: 'Ignoring connection close, done' ] ]. -]]] +ZnServer stopDefault. +ZnServer startDefaultOn: 1701. +ZnServer default delegate: (ZnWebSocketDelegate handler: + [ :webSocket | + [ webSocket runWith: [ :message | + webSocket sendMessage: message ] + ] on: ConnectionClosed + do: [ self crLog: 'Ignoring connection close, done' ] ]). +]]] + -Don't forget to inspect the above code so that you have a reference to the server to close it, as this will not be the default server. +Although using a block as handler is convenient, for non-trivial examples a regular object implementing ==value:== will probably be better. +You can find such an implementation in ==ZnWebSocketEchoHandler==. -Although using a block as handler is convenient, for non-trivial examples a regular object implementing ==value:== will probably be better. You can find such an -implementation in ==ZnWebSocketEchoHandler==. +[[[ +ZnServer stopDefault. +ZnServer startDefaultOn: 1701. +ZnServer default + delegate: (ZnWebSocketDelegate + handler: ZnWebSocketEchoHandler new). +]]] -The current process (thread) as spawned by the server can be used freely by the handler code, for as long as the web socket connection lasts. The responsibility -for closing the connection lies with the handler, although a close from the other side will be handled correctly. +The current process (thread) as spawned by the server can be used freely by the handler code, for as long as the web socket connection lasts. +The responsibility for closing the connection lies with the handler, although a close from the other side will be handled correctly. -To test our echo service, you could connect to it using a client side web socket, like we did in the previous subsection. This is what the unit test -==ZnWebSocketTests>>testEcho== does. Another solution is to run some JavaScript code in a web browser. You can find the necessary HTML page containing JavaScript -code invoking the echo service on the class side of ==ZnWebSocketEchoHandler==. The following setup will serve this code: +To test our echo service, you could connect to it using a client side web socket, like we did in the previous subsection. +This is what the unit test ==ZnWebSocketTests>>testEcho== does. +Another solution is to run some JavaScript code in a web browser. +You can find the necessary HTML page containing JavaScript code invoking the echo service on the class side of ==ZnWebSocketEchoHandler==. +The following setup will serve this code: [[[ +ZnServer stopDefault. ZnServer startDefaultOn: 1701. ZnServer default logToTranscript. ZnServer default delegate @@ -196,17 +212,18 @@ ZnServer default delegate to: (ZnWebSocketDelegate map: 'ws-echo' to: ZnWebSocketEchoHandler new). ]]] -Now, you can try the following URLs: +Now, you can try the following URLs in your Web browser: -[[[ -http://localhost:1701/ws-echo-client-remote -http://localhost:1701/ws-echo-client -]]] +- *http://localhost:1701/ws-echo-client-remote* +- *http://localhost:1701/ws-echo-client* The first one will connect to ==ws://echo.websocket.org== as a reference, the second one will connect to our implementation at ==ws://localhost:1701/ws-echo==. -Another simple example is available in ==ZnWebSocketStatusHandler== where a couple of Smalltalk image statistics are emitted every second for an efficient live -view in your browser. In this scenario, the server accepts each incoming web socket connection and starts streaming to it, not interested in any incoming messages. Here is the core loop: +!!Building a Pharo Statistics Web Page + +Another simple example is available in ==ZnWebSocketStatusHandler== where a couple of Smalltalk image statistics are emitted every second for an efficient live view in your browser. +In this scenario, the server accepts each incoming web socket connection and starts streaming to it, not interested in any incoming messages. +Here is the core loop: [[[ ZnWebSocketStatusHandler>>value: webSocket @@ -221,8 +238,31 @@ ZnWebSocketStatusHandler>>value: webSocket self crLog: 'Stopping status streaming' ]]] -The last example, ==ZnWebSocketChatroomHandler==, implements the core logic of a chatroom: clients can send messages to the server who distributes them to all -connected clients. In this case, the handler has to manage a collection of all connected client web sockets. Here is the core loop: +Here is code to setup all examples: + +[[[ +ZnServer stopDefault. +ZnServer startDefaultOn: 1701. +ZnServer default logToTranscript. +ZnServer default delegate + map: 'ws-status-client' + to: [ :request | ZnResponse ok: (ZnEntity html: ZnWebSocketStatusHandler clientHtml) ]; + map: 'ws-status' + to: (ZnWebSocketDelegate map: 'ws-status' to: ZnWebSocketStatusHandler new). +]]] + +Visit *http://localhost:1701/ws-status-client* to see statistics (uptime, memory, ...) of your running Pharo lively refreshed in a Web page. + +What happened is that your Web browser contacted the Zinc server on *http://localhost:1701/ws-status-client* and got back the HTML and Javascript code in ==ZnWebSocketStatusHandler class>>clientHtml==. +The execution of this Javascript code by the Web browser sent an HTTP request to the Zinc server on *http://localhost:1701/ws-status* asking for an upgrade to up a Web socket connection. Then, the ==ZnWebSocketStatusHandler== object can send status updates through this Web socket connection directly to the Javascript code that refreshes the HTML page content. + + +!!Building a Web Chat + +Another available example is ==ZnWebSocketChatroomHandler==. +It implements the core logic of a chatroom: clients can send messages to the server who distributes them to all connected clients. +In this case, the handler has to manage a collection of all connected client Web sockets. +Here is the core loop: [[[ ZnWebSocketChatroomHandler>>value: webSocket @@ -237,7 +277,7 @@ ZnWebSocketChatroomHandler>>value: webSocket self unregister: webSocket ] ]]] -Distributing the message is as simple as iterating over each client (ignoring some details): +Distributing a message is as simple as iterating over each client (ignoring some details): [[[ ZnWebSocketChatroomHandler>>distributeMessage: message @@ -245,69 +285,54 @@ ZnWebSocketChatroomHandler>>distributeMessage: message each sendMessage: message ]. ]]] -Here is code to setup all examples: + +Here is code to start this Web chat app: [[[ +ZnServer stopDefault. ZnServer startDefaultOn: 1701. ZnServer default logToTranscript. ZnServer default delegate - map: 'ws-echo-client-remote' - to: [ :request | ZnResponse ok: (ZnEntity html: ZnWebSocketEchoHandler clientHtmlRemote) ]; - map: 'ws-echo-client' - to: [ :request | ZnResponse ok: (ZnEntity html: ZnWebSocketEchoHandler clientHtml) ]; - map: 'ws-echo' - to: (ZnWebSocketDelegate map: 'ws-echo' to: ZnWebSocketEchoHandler new); map: 'ws-chatroom-client' to: [ :request | ZnResponse ok: (ZnEntity html: ZnWebSocketChatroomHandler clientHtml) ]; map: 'ws-chatroom' - to: (ZnWebSocketDelegate map: 'ws-chatroom' to: ZnWebSocketChatroomHandler new); - map: 'ws-status-client' - to: [ :request | ZnResponse ok: (ZnEntity html: ZnWebSocketStatusHandler clientHtml) ]; - map: 'ws-status' - to: (ZnWebSocketDelegate map: 'ws-status' to: ZnWebSocketStatusHandler new). + to: (ZnWebSocketDelegate map: 'ws-chatroom' to: ZnWebSocketChatroomHandler new). ]]] -Visit any of the following URLs: +Visit *http://localhost:1701/ws-chat-client* to access your Web chat application. +You can open multiple tabs on this same URL to simulate multiple users on the Chat. -[[[ -http://localhost:1701/ws-echo-client -http://localhost:1701/ws-status-client -http://localhost:1701/ws-chat-client -]]] - -Inside your Pharo image, you can also send chat messages, much like a moderator: +You can also send chat messages directly from Pharo, much like a moderator: [[[ (ZnServer default delegate prefixMap at: 'ws-chatroom') handler distributeMessage: 'moderator>>No trolling please!'. ]]] -!!A Quick Tour of the Implementation -All code resides in the =='Zinc-WebSocket-Core'== package. The wire level protocol, the encoding and decoding of frames can be found in ==ZnWebSocketFrame==. -The key methods are ==writeOn:== and ==readFrom:== as well as the instance creation protocol. Together with the testing protocol and #printOn: these should give -enough information to understand the implementation. +!!A Quick Tour of Zinc WebSocket Implementation + +All code resides in the =='Zinc-WebSocket-Core'== package. +The wire level protocol, the encoding and decoding of frames can be found in ==ZnWebSocketFrame==. +The key methods are ==writeOn:== and ==readFrom:== as well as the instance creation protocol. Together with the testing protocol and ==printOn:== these should give enough information to understand the implementation. - ==ZnWebSocket== implements the protocol above frames, either from a server or a client perspective. The key methods are ==readMessage== and ==readFrame==, - sending is quite simple. Client side setup can be found on the class side of ==ZnWebSocket==. Server side handling of the setup is implemented in ==ZnWebSocketDelegate==. +==ZnWebSocket== implements the protocol above frames, either from a server or a client perspective. The key methods are ==readMessage== and ==readFrame==, sending is quite simple. +Client-side setup can be found on the class side of ==ZnWebSocket==. +Server-side handling of the setup is implemented in ==ZnWebSocketDelegate==. Two exceptions, ==ZnWebSocketFailed== and ==ZnWebSocketClosed== and a shared ==ZnWebSocketUtils== class round out the core code. !!Live Demo -There is a live demo available with the basic Zinc-WebSocket demos: echo, status & chatroom. - -[[[ -http://websocket.stfx.eu -]]] +There is a live demo available at *http://websocket.stfx.eu* with the basic Zinc-WebSocket demos: echo, status & chatroom. -Have a look at ==ZnWebSocketDelegate class>>installExamplesInServer:== as a starting point to learn how this demo was set up. +Have a look at ==ZnWebSocketDelegate class>>installExamplesInServer:== as a starting point to learn how these demos were set up. -Setting up a production demo is complicated by the fact that most proxies and load balancers, most notable market leader Apache, do not (yet) deal correctly with -the WebSocket protocol. It is thus easiest to organize things so that your client talk directly to your Smalltalk image. +Setting up a production demo is complicated by the fact that most proxies and load balancers, most notable market leader Apache, do not (yet) deal correctly with the WebSocket protocol. It is thus easier to organize things such that your client directly talk to your Smalltalk image. The implementation of Zinc WebSockets as an add-on to Zinc HTTP Components was made possible in part through financial backing by Andy Burnett of Knowinnovation Inc. and ESUG. -!! Conclusion +!!Conclusion -WebSockets integrate smoothly with Zinc to form another part of the Pharo web stack. \ No newline at end of file +WebSockets integrate smoothly with Zinc HTTP Components to form another part of the Pharo Web stack. +It provides support for building modern single page Web applications in Pharo.