forked from SquareBracketAssociates/EnterprisePharo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
WebSockets.pillar
343 lines (264 loc) · 16.8 KB
/
WebSockets.pillar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
{
"metadata" : {
"title": "WebSockets",
"attribution": "Sven Van Caekenberghe with Luc Fabresse"
},
"headingLevelOffset":2
}
@cha:webSockets
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
HTTP, one of the main technologies of the internet, defines a communication protocol between a client and a server where the initiative of the communication
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:
- 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.
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.
!The WebSocket Protocol
Zinc WebSockets implements *RFC 6455>http://tools.ietf.org/html/rfc6455*, not any of the previous development versions.
For an introduction, both *the WebSocket Wikipedia article>http://en.wikipedia.org/wiki/WebSocket* and *websocket.org>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
accepting the connection upgrade request. When the request conforms to the specification, a 101 Switching Protocols response is sent. This response also
contains a couple of special HTTP headers. From that point on, the HTTP conversation over the network socket stops and the WebSocket protocol begins.
WebSocket messages consist of one or more frames with minimal encoding. Behind the scenes, a number of control frames are used to properly close the WebSocket
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 repository.
There is also an accompanying 'Zinc-WebSocket-Tests' package containing the unit tests.
The ==ConfigurationOfZincHTTPComponents== has a =='WebSocket'== group that you can load separately.
Here is the loading code snippet:
[[[
Gofer new
smalltalkhubUser: 'SvenVanCaekenberghe' project: 'ZincHTTPComponents';
package: 'ConfigurationOfZincHTTPComponents';
load.
(Smalltalk globals at: #ConfigurationOfZincHTTPComponents) project latestVersion load: 'WebSocket'.
]]]
!Using Client Side WebSockets
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.
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*.
The Zinc WebSockets API is really simple: you use ==sendMessage:==,
==readMessage== and finally ==close== on an open socket.
Here is a client-side example talking to a public echo service:
[[[
| webSocket |
webSocket := ZnWebSocket to: 'ws://echo.websocket.org'.
[ webSocket
sendMessage: 'Pharo Smalltalk using Zinc WebSockets !';
readMessage ] ensure: [ webSocket close ].
]]]
Note that ==readMessage== is blocking. It always returns a complete ==String== or ==ByteArray==, possible assembled out of multiple frames. Inside
==readMessage== control frames will be handled automagically. Reading and sending are completely separate and independent.
For sending very large messages, there are ==sendTextFrames:== and ==sendByteFrames:== that take a collection of ==String==s or ==ByteArray==s to be sent as
different frames of the same message. At the other end, these will be joined together and seen as a single message.
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:
[[[
| webSocket |
webSocket := ZnWebSocket to: 'wss://echo.websocket.org'.
[ webSocket
sendMessage: 'Pharo Smalltalk using Zinc WebSockets & Zodiac !';
readMessage ] ensure: [ webSocket close ].
]]]
Of course, your image has to contain Zodiac and your VM needs access to the proper plugin. That should not be a problem with the latest Pharo releases.
!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.
==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 section.
In essence, we should go in a loop, reading a message and sending it back. Here is the code:
[[[
ZnServer startDefaultOn: 1701.
ZnServer default delegate: (ZnWebSocketDelegate handler:
[ :webSocket |
[ | message |
message := webSocket readMessage.
webSocket sendMessage: message ] repeat ]).
]]]
We start a default server on port 1701 and replace its delegate with
an instance of ==ZnWebSocketDelegate==. This instance will pass each correct web socket
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:
[[[
ZnServer stopDefault.
]]]
The code above works but will eventually encounter two ==NetworkErrors==:
[[[
ConnectionTimedOut
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:
[[[
ZnServer startDefaultOn: 1701.
ZnServer default delegate: (ZnWebSocketDelegate handler:
[ :webSocket |
webSocket runWith: [ :message |
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 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.
[[[
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' ] ]).
]]]
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.
To test the service, you can use a client-side web socket, like we did in the previous section.
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
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).
]]]
Now, you can try the following URLs in your Web browser:
- *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==.
!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
[
self crLog: 'Started status streaming'.
[
webSocket sendMessage: self status.
1 second asDelay wait.
webSocket isConnected ] whileTrue
]
on: ConnectionClosed
do: [ self crLog: 'Ignoring connection close' ].
self crLog: 'Stopping status streaming'
]]]
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 at
the URL *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 which 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
[
self register: webSocket.
webSocket runWith: [ :message |
self crLog: 'Received message: ', message printString.
self distributeMessage: message ]
]
on: ConnectionClosed
do: [
self crLog: 'Connection close, cleaning up'.
self unregister: webSocket ]
]]]
Distributing a message basically means iterating over each client:
[[[
ZnWebSocketChatroomHandler>>distributeMessage: message
clientWebSockets do: [ :each |
each sendMessage: message ].
]]]
Here is code to start this Web chat app:
[[[
ZnServer stopDefault.
ZnServer startDefaultOn: 1701.
ZnServer default logToTranscript.
ZnServer default delegate
map: 'ws-chatroom-client'
to: [ :request | ZnResponse ok: (ZnEntity html: ZnWebSocketChatroomHandler clientHtml) ];
map: 'ws-chatroom'
to: (ZnWebSocketDelegate map: 'ws-chatroom' to: ZnWebSocketChatroomHandler new).
]]]
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.
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 Zinc WebSocket Implementation
All code resides in the =='Zinc-WebSocket-Core'== package.
The wire level protocol, the encoding and decoding of frames is 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 the class ==ZnWebSocketDelegate==.
Two kinds of exceptions, ==ZnWebSocketFailed== and ==ZnWebSocketClosed== and a shared ==ZnWebSocketUtils== class round out the core code.
!Live Demo
There is a live demo available at *http://websocket.stfx.eu* with the basic Zinc-WebSocket demos: echo, status & chatroom.
A starting point to learn how these demos were set up is
the method ==installExamplesInServer:== in the ==ZnWebSocketDelegate class==.
Setting up a production demo is complicated by the fact that most proxies and load balancers, most notably the market leader Apache, do not (yet) deal correctly with the WebSocket protocol. It is thus easier to organize things such that your client directly talks to your Smalltalk image.
!Conclusion
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.
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.