Skip to content

Commit

Permalink
add support for W3C Server-Sent Events
Browse files Browse the repository at this point in the history
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
vinoski committed Jun 25, 2012
1 parent 9efbaad commit c4fd143
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 20 deletions.
20 changes: 17 additions & 3 deletions doc/yaws.tex
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion examples/src/Makefile
Expand Up @@ -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)
Expand Down
82 changes: 82 additions & 0 deletions examples/src/server_sent_events.erl
@@ -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}.
1 change: 1 addition & 0 deletions src/Makefile
Expand Up @@ -56,6 +56,7 @@ MODULES=yaws \
yaws_shaper \
yaws_dime \
yaws_exhtml \
yaws_sse \
$(BITSMODS)


Expand Down
2 changes: 1 addition & 1 deletion src/yaws_server.erl
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions src/yaws_sse.erl
@@ -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">>]).
27 changes: 14 additions & 13 deletions www/TAB.inc
Expand Up @@ -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>
Expand Down
19 changes: 19 additions & 0 deletions www/server_sent_events.html
@@ -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>
112 changes: 112 additions & 0 deletions www/server_sent_events.yaws
@@ -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>

0 comments on commit c4fd143

Please sign in to comment.