Permalink
Browse files

add support for W3C Server-Sent Events

Server-Sent Events is a W3C working draft allowing servers to send simple
events to a client. See the documentation in www/server_sent_events.yaws
for a full description.
  • Loading branch information...
1 parent 9efbaad commit c4fd143ce4b63fb542efeb6b7d43b3564ce1f271 @vinoski vinoski committed Jun 1, 2012
Showing with 319 additions and 20 deletions.
  1. +17 −3 doc/yaws.tex
  2. +2 −1 examples/src/Makefile
  3. +82 −0 examples/src/server_sent_events.erl
  4. +1 −0 src/Makefile
  5. +1 −1 src/yaws_server.erl
  6. +69 −0 src/yaws_sse.erl
  7. +14 −13 www/TAB.inc
  8. +19 −0 www/server_sent_events.html
  9. +112 −0 www/server_sent_events.yaws
  10. +2 −2 www/stream.yaws
View
@@ -1563,9 +1563,16 @@ \section{Stream content}
We call one of the following functions to send data:
\begin{itemize}
-\item \verb+yaws_api:stream_process_deliver(Socket, IoList)+ sends
- data \verb+IoList+ using socket \verb+Socket+ without chunking the
- data.
+\item \verb+yaws_api:stream_process_deliver(Socket, IoList)+ sends data
+ \verb+IoList+ using socket \verb+Socket+ without chunking the data. To
+ ensure chunking is not in effect, return
+
+\begin{verbatim}
+{header, {transfer_encoding, erase}}
+\end{verbatim}
+
+ in a list with the \verb+streamcontent_from_pid+ directive (which must
+ always be last in such a list).
\item \verb+yaws_api:stream_process_deliver_chunk(Socket, IoList)+
sends data \verb+IoList+ using socket \verb+Socket+ but converts
@@ -1649,6 +1656,13 @@ \section{All out/1 return values}
\item \verb+{header, H}+ Accumulates a HTTP header. Used by for
example the \verb+yaws_api:setcookie/2-6+ function.
+\item \verb+{header, {H, erase}}+ A specific case of the previous
+ directive; use this to remove a specific header from a response. For
+ example, streaming applications and applications using server-sent events
+ (see \url{http://www.w3.org/TR/eventsource/ }) should use
+ \verb+{header, {transfer_encoding, erase}}+ to turn off chunked encoding
+ for their responses.
+
\item \verb+{allheaders, HeaderList}+ Will clear all previously
accumulated headers and replace them.
View
@@ -11,7 +11,8 @@ endif
MODULES= advanced_echo_callback \
authmod_gssapi \
- basic_echo_callback
+ basic_echo_callback \
+ server_sent_events
EBIN_FILES=$(MODULES:%=../ebin/%.$(EMULATOR))
ERLC_FLAGS+=-Werror $(DEBUG_FLAGS)
@@ -0,0 +1,82 @@
+%%%----------------------------------------------------------------------
+%%% File : server_sent_events.erl
+%%% Author : Steve Vinoski <vinoski@ieee.org>
+%%% Purpose : Server-Sent Events example
+%%% Created : 1 June 2012 by Steve Vinoski <vinoski@ieee.org>
+%%%----------------------------------------------------------------------
+-module(server_sent_events).
+-behaviour(gen_server).
+
+-include("yaws_api.hrl").
+
+%% API
+-export([out/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {
+ sock,
+ yaws_pid,
+ timer
+ }).
+
+out(A) ->
+ case (A#arg.req)#http_request.method of
+ 'GET' ->
+ case yaws_api:get_header(A#arg.headers, accept) of
+ undefined ->
+ {status, 406};
+ Accept ->
+ case string:str(Accept, "text/event-stream") of
+ 0 ->
+ {status, 406};
+ _ ->
+ {ok, Pid} = gen_server:start(?MODULE, [A], []),
+ yaws_sse:headers(Pid)
+ end
+ end;
+ _ ->
+ {status, 405}
+ end.
+
+init([Arg]) ->
+ process_flag(trap_exit, true),
+ {ok, #state{sock=Arg#arg.clisock}}.
+
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({ok, YawsPid}, #state{sock=Socket}=State) ->
+ ok = inet:setopts(Socket, [{active, once}]),
+ {ok, Timer} = timer:send_interval(1000, self(), tick),
+ {noreply, State#state{yaws_pid=YawsPid, timer=Timer}};
+handle_info({discard, _YawsPid}, State) ->
+ %% nothing to do
+ {stop, normal, State};
+handle_info(tick, #state{sock=Socket}=State) ->
+ Time = erlang:universaltime(),
+ Data = yaws_sse:data(httpd_util:rfc1123_date(Time)),
+ yaws_sse:send_events(Socket, Data),
+ {noreply, State};
+handle_info({tcp_closed, _}, State) ->
+ {stop, normal, State#state{sock=closed}};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, #state{sock=Socket, yaws_pid=YawsPid, timer=Timer}) ->
+ case Timer of
+ undefined ->
+ ok;
+ _ ->
+ timer:cancel(Timer)
+ end,
+ yaws_api:stream_process_end(Socket, YawsPid),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
View
@@ -56,6 +56,7 @@ MODULES=yaws \
yaws_shaper \
yaws_dime \
yaws_exhtml \
+ yaws_sse \
$(BITSMODS)
View
@@ -1520,7 +1520,7 @@ body_method(CliSock, IPPort, Req, Head) ->
get_chunked_client_data(CliSock, yaws:is_ssl(SC));
_ ->
<<>>
- end;
+ end;
Len when is_integer(PPS) ->
Int_len = list_to_integer(Len),
if
View
@@ -0,0 +1,69 @@
+%%%----------------------------------------------------------------------
+%%% File : yaws_sse.erl
+%%% Author : Steve Vinoski <vinoski@ieee.org>
+%%% Purpose : Support for Server-Sent Events
+%%% Created : 31 May 2012 by Steve Vinoski <vinoski@ieee.org>
+%%%----------------------------------------------------------------------
+-module(yaws_sse).
+-author('vinoski@ieee.org').
+
+-export([headers/1,
+ event/0, event/1,
+ data/0, data/1, data/2,
+ id/0, id/1,
+ retry/0, retry/1,
+ comment/1,
+ send_events/2, send_events/3
+ ]).
+
+headers(StreamPid) ->
+ [{status, 200},
+ {header, {"Cache-Control", "no-cache"}},
+ {header, {"Connection", "Keep-Alive"}},
+ {header, {transfer_encoding, erase}},
+ {streamcontent_from_pid, "text/event-stream", StreamPid}].
+
+event() ->
+ <<"event\n">>.
+event(EventName) ->
+ [<<"event:">>, EventName, <<"\n">>].
+
+data() ->
+ <<"data\n">>.
+data(Data) ->
+ [<<"data:">>, Data, <<"\n">>].
+%% The version below trims out all embedded newlines. If you send data
+%% containing newlines using the version above, your events will be
+%% misinterpreted at the client. If the result of trimming includes
+%% any empty strings or binaries, they are dropped and not sent.
+data(Data0, [trim]) ->
+ Bin = iolist_to_binary(Data0),
+ Tokens = case catch binary:split(Bin, <<"\n">>, [global, trim]) of
+ {'EXIT', {undef, [{binary,split,__}|_]}} ->
+ %% handle older releases of Erlang
+ Lst = binary_to_list(Bin),
+ [Tok || Tok <- string:tokens(Lst, "\n")];
+ Bins ->
+ [B || B <- Bins, B /= <<>>]
+ end,
+ [data(Data) || Data <- Tokens].
+
+id() ->
+ <<"id\n">>.
+id(Id) when is_integer(Id) ->
+ [<<"id:">>, integer_to_list(Id), <<"\n">>];
+id(Id) ->
+ [<<"id:">>, Id, <<"\n">>].
+
+retry() ->
+ <<"retry\n">>.
+retry(ReconnectionTime) ->
+ [<<"retry:">>, integer_to_list(ReconnectionTime), <<"\n">>].
+
+comment(Comment) ->
+ [<<":">>, Comment, <<"\n">>].
+
+send_events(Socket, Events) ->
+ send_events(Socket, Events, fun yaws_api:stream_process_deliver/2).
+send_events(Socket, Events, SendFun) ->
+ SendFun(Socket, [Events, <<"\n">>]).
View
@@ -27,37 +27,38 @@ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h4> Yaws </h4>
<div class="%%index%%"> <a href="index.yaws" id="index" >Top Page</a> </div>
<div class="%%configuration%%"> <a href="configuration.yaws" id="configuration">Build Config and Run</a></div>
-<div class="%%dynamic%%"> <a href="dynamic.yaws" id="dynamic" >Dynamic content</a> </div>
+<div class="%%dynamic%%"> <a href="dynamic.yaws" id="dynamic" >Dynamic Content</a> </div>
<div class="%%download%%"> <a href="http://yaws.hyber.org/download/" id="download">Download </a> </div>
<div class="%%contact%%"> <a href="contact.yaws" id="contact">Contact </a> </div>
<div class="%%doc%%"> <a href="doc.yaws" id="doc">Documentation</a> </div>
<div class="%%wiki%%"> <a href="http://wiki.github.com/klacke/yaws" id="wiki">Wiki</a> </div>
<h4> Examples </h4>
-<div class="%%json_intro%%"> <a href="/json_intro.yaws">AJAX/Json RPC</a></div>
+<div class="%%json_intro%%"> <a href="/json_intro.yaws">AJAX/JSON RPC</a></div>
<div class="%%appmods%%"> <a href="/appmods.yaws">Appmods</a> </div>
<div class="%%arg%%"> <a href="/arg.yaws">Arg</a> </div>
-<div class="%%privbind%%"> <a href="/privbind.yaws">Binding to privileged ports</a></div>
+<div class="%%privbind%%"> <a href="/privbind.yaws">Binding to Privileged Ports</a></div>
<div class="%%bindings%%"> <a href="/bindings.yaws">Bindings</a> </div>
<div class="%%cgi%%"> <a href="/cgi.yaws">CGI</a></div>
-<div class="%%session%%"> <a href="/session.yaws">Cookie sessions</a> </div>
+<div class="%%session%%"> <a href="/session.yaws">Cookie Sessions</a> </div>
<div class="%%cookies%%"> <a href="/cookies.yaws">Cookies</a> </div>
-<div class="%%dynamic%%"> <a href="/dynamic.yaws">Dynamic content</a> </div>
+<div class="%%dynamic%%"> <a href="/dynamic.yaws">Dynamic Content</a> </div>
<div class="%%embed%%"> <a href="/embed.yaws">Embedding Yaws</a></div>
-<div class="%%upload0%%"> <a href="/upload0.yaws">File upload</a> </div>
+<div class="%%upload0%%"> <a href="/upload0.yaws">File Upload</a> </div>
<div class="%%form%%"> <a href="/form.yaws">Forms</a> </div>
-<div class="%%haxe_intro%%"> <a href="/haxe_intro.yaws">haXe remoting</a></div>
+<div class="%%haxe_intro%%"> <a href="/haxe_intro.yaws">haXe Remoting</a></div>
<div class="%%pcookie%%"> <a href="/pcookie.yaws">Persistent Cookies</a> </div>
-<div class="%%query%%"> <a href="/query.yaws">Query part of url</a></div>
+<div class="%%query%%"> <a href="/query.yaws">Query Part of URL</a></div>
<div class="%%redirect%%"> <a href="/redirect.yaws">Redirect</a> </div>
-<div class="%%ssi%%"> <a href="/ssi.yaws">Server side includes</a> </div>
+<div class="%%sse%%"> <a href="/server_sent_events.yaws">Server-Sent Events</a> </div>
+<div class="%%ssi%%"> <a href="/ssi.yaws">Server Side Includes</a> </div>
<div class="%%simple%%"> <a href="/simple.yaws">Simple</a> </div>
<div class="%%soap_intro%%"> <a href="/soap_intro.yaws">SOAP with Yaws</a></div>
-<div class="%%stream%%"> <a href="/stream.yaws">Streaming data</a> </div>
+<div class="%%stream%%"> <a href="/stream.yaws">Streaming Data</a> </div>
<div class="%%websockets%%"> <a href="/websockets.yaws">Web Sockets</a> </div>
-<a href="/shoppingcart/index.yaws">Tiny shopping cart</a>
-<div class="%%yapp_intro%%"> <a href="/yapp_intro.yaws">Yaws applications</a></div>
-<div class="%%logger_mod%%"> <a href="/logger_mod.yaws">Write your own logger</a></div>
+<a href="/shoppingcart/index.yaws">Tiny Shopping Cart</a>
+<div class="%%yapp_intro%%"> <a href="/yapp_intro.yaws">Yaws Applications (yapps)</a></div>
+<div class="%%logger_mod%%"> <a href="/logger_mod.yaws">Write Your Own Logger</a></div>
<h4> Misc </h4>
<div class="%%internals%%"> <a href="/internals.yaws">Internals</a> </div>
@@ -0,0 +1,19 @@
+<!-- HTML document served to start the Server Side Events demo
+ See examples/src/server_side_events.erl for the server -->
+<html>
+<head>
+<script type="text/javascript">
+ var es = new EventSource('/sse');
+ es.onmessage = function(event) {
+ document.querySelector('#date-time').innerHTML = event.data;
+ }
+</script>
+</head>
+<body>
+<center>
+<h1>Yaws Server-Sent Events Time of Day</h1>
+<h2 id="date-time" name="date-time">
+</h2>
+</center>
+</body>
+</html>
View
@@ -0,0 +1,112 @@
+<erl>
+
+out(A) ->
+ {ssi, "TAB.inc", "%%",[{"sse", "choosen"}]}.
+
+
+</erl>
+
+<div id="entry">
+
+<h1>Server-sent events</h1>
+
+ <p>HTTP is a client-server protocol &mdash; the client makes a request
+ and the server replies with a response. For some applications, though,
+ the request-reply model is limiting or unsuitable. These applications
+ tend to want server-to-client notification capabilities. While such
+ notifications can be simulated using polling, and web-based polling can
+ be much more efficient than one would think due to intermediary
+ caching, it's still less efficient and less timely than a notification
+ model.
+ </p>
+
+ <p>Yaws users have a few options for notification-oriented applications:</p>
+
+ <ul><li><p>Yaws supports an older technique called "long polling" or
+ "Comet" where the client sends a request that the server sits on and
+ doesn't answer until it actually has an event for the client. The problem
+ with long polling is that it requires the client and server applications
+ to be bound tightly to each other via the specialized ad hoc long-polling
+ protocol they share.</p></li>
+
+ <li><p><a href="websockets.yaws">The WebSockets protocol</a> (<a
+ href="http://tools.ietf.org/html/rfc6455">RFC 6455</a>) allows web client
+ and server to upgrade their TCP connection from using HTTP to using some
+ other protocol they agree on. The protocol they choose can be
+ bidirectional and can transmit whatever data transfer formats they wish
+ to use. WebSockets afford applications a great deal of freedom and
+ flexibility, but they also require client and server to agree on
+ specialized protocols, framing, and data formats to be able to
+ communicate successfully.</p></li>
+
+ <li><p><a href="http://www.w3.org/TR/eventsource/">Server-Sent Events</a>
+ (SSE) is a W3C working draft that unlike long polling is on a path to
+ standardization and unlike WebSockets is pretty simple. Despite being a
+ workng draft, it's already fairly widely used. With SSE, a client sends a
+ standard HTTP request asking for an event stream, and the server responds
+ initially with a standard HTTP response and holds the connection
+ open. When appropriate, the server sends standard text-based event data
+ back to the client as part of the initial response, and continues to do
+ so until either end closes the connection. Clients can disconnect and
+ later reconnect, indicate the last event they received, and pick up new
+ events from that point.</p></li> </ul>
+
+ <p>Currently, Chrome, Firefox, Opera, and Safari support SSE. Older
+ browsers do not support SSE directly, but they can be made to do so with
+ suitable JavaScript packages.</p>
+
+ <h2>Writing a Yaws SSE application</h2>
+
+ <p>Yaws supports SSE through its <a href="stream.yaws">streaming
+ capabilities</a>. SSE applications typically consist of an entry point
+ page and an appmod. The entry page returns HTML and JavaScript that acts
+ as the SSE client, with the JavaScript invoking the appmod's
+ <code>out/1</code> function that creates a streaming process responsible
+ for sending events back to the client. The appmod uses the
+ <code>yaws_sse</code> module to properly format and send its event
+ data.</p>
+
+ <p>Yaws supplies an example that uses SSE to return the server's time of
+ day clock to the client. Each second, the server sends a new event to the
+ client updating its time of day, which the client dynamically displays in
+ a web page.</p>
+
+ <p>First, the entry HTML page is here: <a
+ href="server_sent_events.html">server_sent_events.html</a>. It presents
+ a page title and a placeholder for the server date string. It also
+ supplies a bit of JavaScript that receives events from Yaws, using the
+ browser's <code>EventSource</code> JavaScript class to receive them. It
+ then pulls the data out of the event and displays it dynamically in the
+ HTML.</p>
+
+ <p>Next, the server appmod code is here: <a
+ href="https://github.com/klacke/yaws/blob/master/examples/src/server_sent_events.erl">server_sent_events.erl</a>. Its
+ <code>out/1</code> function create a gen_server event generation process,
+ returning the pid in a <code>streamcontent_from_pid</code> directive to
+ Yaws along with suitable headers. Note that it obtains the desired
+ <code>out/1</code> return value via the <code>yaws_sse:headers/1</code>
+ function. Its gen_server is fairly simple in that it creates a timer
+ that, once per second, generates a time of day string and sends it as an
+ event to the client formatted via the <code>yaws_sse:data/1</code>
+ function.</p>
+
+ <p>The <code>yaws_sse</code> module supplies all the SSE primitives
+ required for formatting event data, event identifiers, and event retry
+ settings. See the <a href="http://www.w3.org/TR/eventsource/">Server-Sent
+ Events</a> working draft for more details on using these features.</p>
+
+ <p>The <code>yaws_sse</code> module also supplies functions for
+ formatting and sending event data on a socket. If you're using the
+ <code>yaws_sse</code> module outside of a Yaws streaming application, you
+ should use the arity 3 version of <code>yaws_sse:send_events</code> and
+ pass <code>fun yaws:gen_tcp_send/2</code> as the third argument.</p>
+
+ <p><strong>Note:</strong> be aware that because the W3C Server-Sent
+ Events spec is still a working draft, any future changes in it might
+ cause API-incompatible changes in how Yaws supports it.</p>
+
+</div>
+
+<erl>
+out(A) -> {ssi, "END2",[],[]}.
+</erl>
Oops, something went wrong.

0 comments on commit c4fd143

Please sign in to comment.