This repository was archived by the owner on May 28, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
Copy pathsse.xml
1103 lines (1025 loc) · 65.1 KB
/
sse.xml
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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0"?>
<!--
DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
Copyright (c) 2012-2017 Oracle and/or its affiliates. All rights reserved.
The contents of this file are subject to the terms of either the GNU
General Public License Version 2 only ("GPL") or the Common Development
and Distribution License("CDDL") (collectively, the "License"). You
may not use this file except in compliance with the License. You can
obtain a copy of the License at
https://oss.oracle.com/licenses/CDDL+GPL-1.1
or LICENSE.txt. See the License for the specific
language governing permissions and limitations under the License.
When distributing the software, include this License Header Notice in each
file and include the License file at LICENSE.txt.
GPL Classpath Exception:
Oracle designates this particular file as subject to the "Classpath"
exception as provided by Oracle in the GPL Version 2 section of the License
file that accompanied this code.
Modifications:
If applicable, add the following below the License Header, with the fields
enclosed by brackets [] replaced by your own identifying information:
"Portions Copyright [year] [name of copyright owner]"
Contributor(s):
If you wish your version of this file to be governed by only the CDDL or
only the GPL Version 2, indicate your decision by adding "[Contributor]
elects to include this software in this distribution under the [CDDL or GPL
Version 2] license." If you don't indicate a single choice of license, a
recipient has the option to distribute your version of this file under
either the CDDL, the GPL Version 2 or to extend the choice of license to
its licensees as provided above. However, if you add GPL Version 2 code
and therefore, elected the GPL Version 2 license, then the option applies
only if the new code is made subject to such option by the copyright
holder.
-->
<!DOCTYPE chapter [<!ENTITY % ents SYSTEM "jersey.ent" > %ents; ]>
<chapter xmlns="http://docbook.org/ns/docbook"
version="5.0"
xml:lang="en"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xlink="http://www.w3.org/1999/xlink"
xsi:schemaLocation="http://docbook.org/ns/docbook http://docbook.org/xml/5.0/xsd/docbook.xsd
http://www.w3.org/1999/xlink http://www.w3.org/1999/xlink.xsd"
xml:id="sse">
<title>Server-Sent Events (SSE) Support</title>
<section>
<title>What are Server-Sent Events</title>
<para>
In a standard HTTP request-response scenario a client opens a connection, sends a HTTP request to the server (for
example a HTTP &lit.http.GET; request), then receives a HTTP response back and the server closes the connection once
the response is fully sent/received. The initiative <emphasis>always</emphasis> comes from a client when the client
requests all the data. In contrast, <emphasis>Server-Sent Events (SSE)</emphasis> is a mechanism that allows server
to asynchronously push the data from the server to the client once the client-server connection is established by the
client. Once the connection is established by the client, it is the server who provides the data and decides
to send it to the client whenever new "chunk" of data is available. When a new data event occurs on the server,
the data event is sent by the server to the client. Thus the name Server-Sent Events. Note that at high level there
are more technologies working on this principle, a short overview of the technologies supporting server-to-client
communication is in this list:
<variablelist>
<varlistentry>
<term>Polling</term>
<listitem>
<para>
With polling a client repeatedly sends new requests to a server. If the server has no new data,
then it send appropriate indication and closes the connection. The client then waits a bit and sends
another request after some time (after one second, for example).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Long-polling</term>
<listitem>
<para>
With long-polling a client sends a request to a server. If the server has no new data,
it just holds the connection open and waits until data is available. Once the server has data
(message) for the client, it uses the connection and sends it back to the client. Then the connection
is closed.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Server-Sent events</term>
<listitem>
<para>
SSE is similar to the long-polling mechanism, except it does not send only one message per connection.
The client sends a request and server holds a connection until a new message is ready, then it sends
the message back to the client while still keeping the connection open so that it can be used
for another message once it becomes available. Once a new message is ready, it is sent back to the
client on the same initial connection. Client processes the messages sent back from the server
individually without closing the connection after processing each message.
So, SSE typically reuses one connection for more messages (called events). SSE also defines a
dedicated media type that describes a simple format of individual events sent from the server to the
client. SSE also offers standard javascript client API implemented most modern browsers. For more
information about SSE, see the
<link xlink:href='http://www.w3.org/TR/2009/WD-eventsource-20091029/'>SSE API specification</link>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>WebSocket</term>
<listitem>
<para>
WebSocket technology is different from previous technologies as it provides a real full duplex
connection. The initiator is again a client which sends a request to a server with a special HTTP
header that informs the server that the HTTP connection may be "upgraded" to a full duplex TCP/IP
WebSocket connection. If server supports WebSocket, it may choose to do so. Once a WebSocket
connection is established, it can be used for bi-directional communication between the client and the
server. Both client and server can then send data to the other party at will whenever it is needed.
The communication on the new WebSocket connection is no longer based on HTTP protocol and can be
used for example for for online gaming or any other applications that require fast exchange of small
chunks of data in flowing in both directions.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
<section>
<title>When to use Server-Sent Events</title>
<para>
As explained above, SSE is a technology that allows clients to subscribe to event notifications that originate on
a server. Server generates new events and sends these events back to the clients subscribed to receive the
notifications. In other words, SSE offers a solution for a one-way publish-subscribe model.
</para>
<para>
A good example of the use case where SSE can be used is a simple message exchange RESTful service. Clients
&lit.http.POST; new messages to the service and subscribe to receive messages from other clients.
Let's call the resource <literal>messages</literal>. While &lit.http.POST;ing a new message to this resource involves
a typical HTTP request-response communication between a client and the <literal>messages</literal> resource,
subscribing to receive all new message notifications would be hard and impractical to model with a sequence of
standard request-response message exchanges. Using Server-sent events provides a much more practical approach here.
You can use SSE to let clients subscribe to the <literal>messages</literal> resource via standard &lit.http.GET;
request (use a SSE client API, for example javascript API or Jersey Client SSE API) and let the server broadcast
new messages to all connected clients in the form of individual events (in our case using Jersey Server SSE API).
Note that with Jersey a SSE support is implemented as an usual JAX-RS resource method. There's no need to do anything
special to provide a SSE support in your Jersey/JAX-RS applications, your SSE-enabled resources are a standard part of
your RESTful Web application that defines the REST API of your application. The following chapters describes SSE
support in Jersey in more details.
</para>
</section>
<section xml:id="jaxrs-sse-api-overview">
<title>Server-Sent Events API</title>
<para>
In previous JAX-RS versions, no standard API for server-sent events was defined. The SSE support bundled with
Jersey was Jersey-specific. With JAX-RS 2.1, situation changed and SSE API is well defined in the
<literal>javax.ws.rs.sse</literal>
package.
</para>
<para>Following chapters will describe the new SSE API. For backwards compatibility reasons, the original
Jersey-specific API remains valid and will be described in
<xref linkend="overview-jersey-specific"/>
</para>
<para>
Jersey contains support for SSE for both - server and client. SSE in Jersey is implemented as an extension
supporting a new media type using existing "chunked" messages support. However, in contrast to the original API,
the instances of SSE related classes are not to be obtained manually by invoking constructors, nor to be directly
returned from the resource methods.
Actually, the implementing classes in the <literal>jersey.media.sse.internal</literal> package should never be needed
to be imported. The only API to be used is directly in the JAX-RS package (<literal>javax.ws.rs.sse</literal>).
Only builders in the API along with dependency injection should be used and provides access to the entire
functionality.
</para>
<para>
In order to take advantage of the SSE support, the <literal>jersey-media-sse</literal> module has to be on classpath.
In maven, this can be achieved by adding the dependency to the <emphasis>SSE media type module</emphasis>:
<example xml:id="sse-dependency-jaxrs">
<title>Adding the SSE dependency</title>
<programlisting language="xml" linenumbering="numbered"><![CDATA[<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-sse</artifactId>
</dependency>]]></programlisting>
</example>
The &lit.jaxrs.core.Feature; defined in the module is (forced) auto-discoverable, which means having the module on
classpath is sufficient, no need to further register it in the code.
</para>
</section>
<section>
<title>Implementing SSE support in a JAX-RS resource (with JAX-RS SSE API)</title>
<section>
<title>Simple SSE resource method</title>
<example xml:id="example-simple-sse-jaxrs">
<title>Simple SSE resource method</title>
As mentioned above, the SSE related are not instantiated directly. In this case, Jersey takes care of the
dependencies and injects the &jaxrs21.sse.SseEventSink; (represents the output) and &jaxrs21.sse.Sse; (provides
factory methods for other SSE related types, in this case it is used to retrieve the event builder).
<programlisting language="java" linenumbering="numbered">...
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import javax.ws.rs.sse.OutboundSseEvent;
...
@Path("events")
public static class SseResource {
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
public void getServerSentEvents(@Context SseEventSink eventSink, @Context Sse sse) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
// ... code that waits 1 second
final OutboundSseEvent event = sse.newEventBuilder()
.name("message-to-client")
.data(String.class, "Hello world " + i + "!")
.build();
eventSink.onNext(event);
}
}).start();
}
}
</programlisting>
</example>
<para>
The code above defines the resource deployed on URI "/events". This resource has a single
<literal>@GET</literal>
resource method which <emphasis>returns void</emphasis>. This is an important difference
against the original API. It is Jersey's responsibility to bind the injected <literal>SseEventSink</literal> to
the output chain.
</para>
<para>
After the <literal>SseEventInput</literal> is "returned" from the method, the Jersey runtime recognizes that this
is a &lit.jersey.server.ChunkedOutput; extension and does not close the client connection immediately. Instead, it
writes the HTTP headers to the response stream and waits for more chunks (SSE events) to be sent. At this point
the client can read headers and starts listening for individual events.
</para>
<para>
In the<xref linkend="example-simple-sse-jaxrs"/>, the resource method creates a new thread that sends a
sequence of 10 events. There is a 1 second delay between two subsequent events as indicated in a comment. Each
event is represented by <literal>javax.ws.rs.sse.OutboundSseEvent</literal> type and is built with a help of a
provided <literal>Builder</literal>. The <literal>Builder</literal> is obtain via the injected instance
(actually, it is a singleton) of <literal>javax.ws.rs.sse.Sse</literal> (the
<literal>newEventBuilder()</literal>
method. The <literal>OutboundSseEvent</literal> implementation reflects the standardized format of
SSE messages and contains properties that represent <literal>name</literal> (for named events),
<literal>comment</literal>, <literal>data</literal> or <literal>id</literal>. The code also sets the
event data media type using the <literal>mediaType(MediaType)</literal> method on the
<literal>eventBuilder</literal>. The media type, together with the data type set by the
<literal>data(Class, Object></literal>
method (in our case <literal>String.class</literal>), is used
for serialization of the event data. Note that the event data media type will not be written to any headers as
the response <literal>Content-type</literal> header is already defined by the &lit.jaxrs.Produces; and set to
<literal>"text/event-stream"</literal>
using constant from the &lit.jaxrs.core.MediaType;.
The event media type is used for serialization of event <literal>data</literal>. Event data media type and Java
type are used to select the proper &jaxrs.ext.MessageBodyWriter; for event data serialization and are passed
to the selected writer that serializes the event <literal>data</literal> content. In our case the string
<literal>"Hello world " + i + "!"</literal>
is serialized as <literal>"text/plain"</literal>. In event
<literal>data</literal>
you can send any Java entity and associate it with any media type that you would be able
to serialize with an available &lit.jaxrs.ext.MessageBodyWriter;. Typically, you may want to send e.g. JSON data,
so you would fill the <literal>data</literal> with a JAXB annotated bean instance and define media type to JSON.
<note>
<para>
If the event media type is not set explicitly, the <literal>"text/plain"</literal> media type is used
by default.
</para>
</note>
</para>
<para>
Once an outbound event is ready, it can be written to the <literal>EventSink</literal>. At that point the event
is serialized by internal &lit.jersey.sse.OutboundEventWriter; which uses an appropriate
&lit.jaxrs.ext.MessageBodyWriter; to serialize the <literal>"Hello world " + i + "!"</literal> string. You can
send as many messages as you like. At the end of the thread execution the response is closed which also closes
the connection to the client. After that, no more messages can be sent to the client on this connection. If the
client would like to receive more messages, it would have to send a new request to the server to initiate a
new SSE streaming connection.
</para>
<para>
A client connecting to our SSE-enabled resource will receive the following data from the entity stream:
<screen language="text" linenumbering="unnumbered">event: message-to-client
data: Hello world 0!
event: message-to-client
data: Hello world 1!
event: message-to-client
data: Hello world 2!
event: message-to-client
data: Hello world 3!
event: message-to-client
data: Hello world 4!
event: message-to-client
data: Hello world 5!
event: message-to-client
data: Hello world 6!
event: message-to-client
data: Hello world 7!
event: message-to-client
data: Hello world 8!
event: message-to-client
data: Hello world 9!
</screen>
Each message is received with a delay of one second.
</para>
<note>
<para>
If you have worked with streams in JAX-RS, you may wonder what is the difference between
&jersey.server.ChunkedOutput; and &jaxrs.core.StreamingOutput;.
</para>
<para>
&lit.jersey.server.ChunkedOutput; is Jersey-specific API. It lets you send "chunks" of data without closing
the client connection using series of convenient calls to <literal>ChunkedOutput.write</literal> methods
that take POJO + chunk media type as an input and then use the configured JAX-RS
&lit.jaxrs.ext.MessageBodyWriter; providers to figure out the proper way of serializing each chunk POJO
to bytes. Additionally, &lit.jersey.server.ChunkedOutput; writes can be invoked multiple times on the same
outbound response connection, i.e. individual chunks are written in each write, not the full response entity.
</para>
<para>
&lit.jaxrs.core.StreamingOutput; is, on the other hand, a low level JAX-RS API that works with bytes
directly. You have to implement &lit.jaxrs.core.StreamingOutput; interface yourself. Also, its
<literal>write(OutputStream)</literal>
method will be invoked by JAX-RS runtime only once per response
and the call to this method is blocking, i.e. the method is expected to write the entire entity body
before returning.
</para>
</note>
</section>
<section>
<title>Broadcasting with Jersey SSE</title>
<para>
JAX-RS SSE API defines &jersey.sse.SseBroadcaster; which allows to broadcast individual events to multiple
clients. A simple broadcasting implementation is shown in the following example:
<example>
<title>Broadcasting SSE messages (with JAX-RS 2.1 API)</title>
<programlisting language="java" linenumbering="numbered">...
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import javax.ws.rs.sse.SseBroadcaster;
...
@Singleton
@Path("broadcast")
public static class BroadcasterResource {
private Sse sse;
private SseBroadcaster broadcaster;
public BroadcasterResource(@Context final Sse sse) {
this.sse = sse;
this.broadcaster = sse.newBroadcaster();
}
@POST
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public String broadcastMessage(String message) {
final OutboundSseEvent event = sse.newEventBuilder()
.name("message")
.mediaType(MediaType.TEXT_PLAIN_TYPE)
.data(String.class, message)
.build();
broadcaster.broadcast(event);
return "Message '" + message + "' has been broadcast.";
}
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
public void listenToBroadcast(@Context SseEventSink eventSink) {
this.broadcaster.subscribe(eventSink);
}
}
</programlisting>
</example>
Let's explore the example together. The <literal>BroadcasterResource</literal> resource class is annotated with
&jee6.inject.Singleton; annotation which tells Jersey runtime that only a single instance of the resource
class should be used to serve all the incoming requests to <literal>/broadcast</literal> path. This is needed as
we want to keep an application-wide single reference to the private <literal>broadcaster</literal> field so that
we can use the same instance for all requests. Clients that want to listen to SSE events first send a
&lit.http.GET; request to the <literal>BroadcasterResource</literal>, that is handled by the
<literal>listenToBroadcast()</literal>
resource method.
The method is injected with a new <literal>SseEventSink</literal> representing the connection to the
requesting client and registers this <literal>eventSink</literal> instance with the singleton
<literal>broadcaster</literal>
by calling its <literal>subscribe()</literal> method.
The method then, as already explained returns <literal>void</literal> and Jersey runtime is responsible for
binding the injected <literal>EventSink</literal> instance so as it would have been returned from the resource
method (note that really returning the <literal>EventSink</literal> from the resource method will cause
failure) and to bind the <literal>eventSink</literal> instance with the requesting client and send the
response HTTP headers to the client. The client connection remains open and the client is now waiting ready to
receive new SSE events. All the events are written to the <literal>eventSink</literal> by
<literal>broadcaster</literal>
later on. This way developers can conveniently handle sending new events to
all the clients that subscribe to them.
</para>
<para>
When a client wants to broadcast new message to all the clients listening on their SSE connections,
it sends a &lit.http.POST; request to <literal>BroadcasterResource</literal> resource with the message content.
The method <literal>broadcastMessage(String)</literal> is invoked on
<literal>BroadcasterResource</literal>
resource with the message content as an input parameter. A new SSE outbound event is built in the standard way
and passed to the broadcaster. The broadcaster internally invokes <literal>write(OutboundEvent)</literal> on all
registered <literal>EventSink</literal>s. After that the method just returns a standard text response
to the &lit.http.POST;ing client to inform the client that the message was successfully broadcast. As you can see,
the <literal>broadcastMessage(String)</literal> resource method is just a simple JAX-RS resource method.
</para>
<!-- TODO continue here -->
<para>
In order to implement such a scenario, you may have noticed, that the
<literal>SseBroadcaster</literal>
is not mandatory to complete the use case. Individual <literal>EventSink</literal>s can be just stored in
a collection and iterated over in the <literal>broadcastMessage</literal> method. However, the
<literal>SseBroadcaster</literal>
internally identifies and handles also client disconnects. When a client
closes the connection, the broadcaster detects this and removes the stale connection from the internal collection
of the registered <literal>EventSink</literal>s as well as it frees all the server-side resources associated with
the stale connection.
Additionally, the <literal>SseBroadcaster</literal> is implemented to be thread-safe, so that clients can connect
and disconnect in any time and <literal>SseBroadcaster</literal> will always broadcast messages to the most recent
collection of registered and active set of clients.
</para>
</section>
</section>
<section xml:id="sse-client-jaxrs">
<title>Consuming SSE events within Jersey clients</title>
<para>
On the client side, push programming model is used (event consumer / client) gets asynchronously notified about
incoming events by subscribing custom listener to <literal>javax.ws.rs.sse.SseEventSource</literal>. This happens by
invoking one of its <literal>subscribe()</literal> methods.
</para>
<para>
The usage of <literal>SseEventSource</literal> is shown in the following example.
<example xml:id="sse-event-source-example">
<title>Consuming SSE events with SseEventSource</title>
<programlisting language="java" linenumbering="numbered">import javax.ws.rs.sse.SseEventSource;
...
Client client = ClientBuilder.newBuilder().build();
WebTarget target = client.target("http://example.com/events");
SseEventSource sseEventSource = SseEventSource.target(target).build();
sseEventSource.subscribe((event) -> System.out.println(event.getName() + "; "
+ event.readData(String.class)));
sseEventSource.open();
// do other stuff, block here and continue when done
sseEventSource.close();
</programlisting>
</example>
In this example, the client code connects to the server where the <literal>SseResource</literal> from the
<xref linkend="example-simple-sse-jaxrs"/>
is deployed. The &jaxrs.client.Client; instance
is created (and initialized with &jersey.sse.SseFeature; automatically). Then the &jaxrs.client.WebTarget; is built.
In this case a request to the web target is not made directly in the code, instead, the web target instance
is used to initialize a new &javax.ws.rs.sse.SseEventSource.Builder; instance that is used to build a new
&javax.ws.rs.sse.SseEventSource;. The choice of <literal>build()</literal> method is important, as it tells
the <literal>SseEventSource.Builder</literal> to create a new <literal>SseEventSource</literal> that is not
automatically connected to the <literal>target</literal>. The connection is established only later by manually
invoking the <literal>sseEventSource.open()</literal> method. A custom
<literal>java.util.function.Consumer<InboundSseEvent></literal>
implementation is used to listen to and
process incoming SSE events. The method readData(Class) says that the
event data should be de-serialized from a received &javax.ws.rs.sse.InboundSseEvent; instance into a
<literal>String</literal>
Java type. This method call internally executes &jaxrs.ext.MessageBodyReader; which
de-serializes the event data. This is similar to reading an entity from the &jaxrs.core.Response; by
<literal>readEntity(Class)</literal>. The method <literal>readData</literal> can throw a
&jaxrs.ProcessingException;.
</para>
<para>
After a connection to the server is opened by calling the <literal>open()</literal> method on the event source,
the <literal>eventSource</literal> starts listening to events. When an event comes, the listener will be executed
by the event source. Once the client is done with processing and does not want to receive events the connection by
calling the <literal>close()</literal> method on the event source.
</para>
<para>
The listener from the example above will print the following output:
<screen language="text" linenumbering="unnumbered">message-to-client; Hello world 0!
message-to-client; Hello world 1!
message-to-client; Hello world 2!
message-to-client; Hello world 3!
message-to-client; Hello world 4!
message-to-client; Hello world 5!
message-to-client; Hello world 6!
message-to-client; Hello world 7!
message-to-client; Hello world 8!
message-to-client; Hello world 9!
</screen>
</para>
<para>
There are other events than the incoming data that also may occur. The <literal>SseEventSource</literal> for
instance always signals, that it has finished processing events, or there might also be an error while processing the
messages. <literal>SseEventSource</literal>. There are total of four overloaded
<literal>subscribe()</literal>
methods defined in the API.
</para>
<para>
<example xml:id="sse-event-source-subscribe-methods">
<title>SseEventSource subscribe() methods</title>
<programlisting language="java" linenumbering="numbered">// 1. basic one - the one we used in the example
void subscribe(Consumer<InboundSseEvent> onEvent);
// 2. with an error callback
void subscribe(Consumer<InboundSseEvent> onEvent, Consumer<Throwable> onError);
// 3. with an error callback and completion callback
void subscribe(Consumer<InboundSseEvent> onEvent, Consumer<Throwable> onError, Runnable onComplete)
// 4. complete one - with error callback, completion callback an onSubscribe callback
void subscribe(Consumer<SseSubscription> onSubscribe, Consumer<InboundSseEvent> onEvent, Consumer<Throwable>
onError,
Runnable
onComplete);
</programlisting>
</example>
<para>
Few notes to the <literal>subscribe()</literal> methods:
<itemizedlist>
<listitem>
<para>
All the overloaded methods have the <literal>onEvent</literal> handler. As shown in the example,
this parameter is used to consume the SSE events with data.
</para>
</listitem>
<listitem>
<para>
Except the basic one-arg method, all the others contain an <literal>onError</literal> handler. In
case of error, the <literal>SseEventSource</literal> invokes the <literal>onError</literal> method
of all its subscribers, that registered the handler. This makes it possible to react to the error
conditions in a custom manner.
</para>
</listitem>
<listitem>
<para>
Another possible argument is the <literal>onComplete</literal> handler. If registered (used an
appropriate <literal>subscribe()</literal> method, that has the
<literal>onComplete</literal>
argument), it is invoked (for all the subscribers) every time when the
<literal>SseEventSource</literal>
terminates normally. Either <literal>onComplete</literal> or
<literal>onError</literal>
should be called every time.
</para>
</listitem>
<listitem>
<para>
The complete <literal>subscribe()</literal> method adds the <literal>onSubscribe()</literal> callback.
This gives the subscriber a tool to manage the load and do a back-pressure by incrementaly
requesting only certain amount of items. When <literal>SseEventSource</literal> registers a new
subscriber, it calls its <literal>onSubscribe</literal> handler and hands over the
<literal>javax.ws.rs.sse.SseSubscription</literal>
instance. This class only has two methods -
<literal>request(long)</literal>
for asking for a certain amount of events (often used as
<literal>request(Long.MAX_VALUE)</literal>
when no back-pressure is needed) and
<literal>cancel()</literal>
to stop receiving further events.
</para>
</listitem>
<listitem>
<para>
When using the full-arg version of <literal>subscribe()</literal>, it is the caller's
responsibility to manage the amount of data it can handle. The
<literal>sseSubscription.request()</literal>
method <emphasis>MUST</emphasis> be called, otherwise
the subscriber will not receive ANY data. Furthermore, in the current
<literal>SseEventSource</literal>
implementation, such a subscriber will block a threadm and will
occasionally lead to overflow of an internal buffer in <literal>SseEventSource</literal>. As
mentioned, calling <literal>subscription.request(Long.MAX_VALUE)</literal>, e.g. in the registered
<literal>onSubscribe</literal>
handler is sufficient (and is also a default behaviour for all the
other overloaded methods).
</para>
</listitem>
</itemizedlist>
</para>
</para>
<section xml:id="sse-event-source-reconnect">
<title>
<literal>SseEventSource</literal>
reconnect support
</title>
<para>
The &javax.ws.rs.sse.SseEventSource; implementation supports automated recuperation
from a connection loss, including negotiation of delivery of any missed events based on the last received
SSE event <literal>id</literal> field value, provided this field is set by the server and the negotiation
facility is supported by the server. In case of a connection loss, the last received SSE event
<literal>id</literal>
field value is sent in the <literal>Last-Event-ID</literal> HTTP request
header as part of a new connection request sent to the SSE endpoint. Upon a receipt of such reconnect request,
the SSE endpoint that supports this negotiation facility is expected to replay all missed events.
</para>
<note>
<para>
Note, that SSE lost event negotiation facility is a best-effort mechanism which does not provide
any guarantee that all events would be delivered without a loss. You should therefore not
rely on receiving every single event and design your client application code accordingly.
</para>
</note>
<para>
By default, when a connection the the SSE endpoint is lost, the event source will use a default delay
before attempting to reconnect to the SSE endpoint. The SSE endpoint can however control the client-side
retry delay by including a special <literal>retry</literal> field value in any event sent to the client.
Jersey &javax.ws.rs.sse.SseEventSource; implementation automatically tracks any received SSE event
<literal>retry</literal>
field values set by the endpoint and adjusts the reconnect delay accordingly,
using the last received <literal>retry</literal> field value as the new reconnect delay.
</para>
<para>
In addition to handling the standard connection losses, Jersey &javax.ws.rs.sse.SseEventSource; automatically
deals with any <literal>HTTP 503 Service Unavailable</literal> responses received from the SSE endpoint,
that include a <literal>Retry-After</literal> HTTP header with a valid value. The
<literal>HTTP 503 + Retry-After</literal>
technique is often used by HTTP endpoints as a means of
connection and traffic throttling. In case a <literal>HTTP 503 + Retry-After</literal> response is received
in return to a connection request from SSE endpoint, Jersey <literal>SseEventSource</literal> will automatically
schedule a reconnect attempt and use the received <literal>Retry-After</literal> HTTP header value as a
one-time override of the reconnect delay.
</para>
</section>
</section>
<!--TODO - describes the Jersey-specific legacy API, change and only mention the differences -->
<section xml:id="overview-jersey-specific">
<title>Jersey-specific Server-Sent Events API</title>
<important>
<para>
Prior to JAX-RS 2.1, server-sent events was not standardized and was optional and implementation-specific.
Jersey provided its own, specific version of SSE implementation, that remains valid and functional to achieve
backwards compatibility. This implementation is a Jersey-specific extension of JAX-RS (2.0) standard. It works
with common JAX-RS resources the same way as the JAX-RS 2.1 based implementation does.
</para>
<para>
Both implementations are compatible, which means client based on Jersey-specific SSE implementation can "talk"
to server resource implemetned using JAX-RS 2.1 based implementation and vice versa.
</para>
</important>
<para>
This chapter briefly describes the Jersey-specific support for SSE, focusing on the differences against the new SSE
implementation described in
<xref linkend="overview-jaxrs"/>
</para>
<para>
The API contains support SSE support for both - server and client. To use the Jersey-specific SSE API, you need to
add the dependency to the
</para>
<para>
In order to add support for this SSE implementation, you also need to include the dependency to the
<emphasis>SSE media type module</emphasis>
the same way as for the JAX-RS SSE implementation.
<example xml:id="sse.dependency">
<title>Add <literal>jersey-media-sse</literal> dependency.
</title>
<programlisting language="xml" linenumbering="numbered"><![CDATA[<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-sse</artifactId>
</dependency>]]></programlisting>
</example>
<note>
<para>
Prior to Jersey 2.8, you had to manually register &jersey.sse.SseFeature; in your application.
(The &lit.jersey.sse.SseFeature; is a feature that can be registered for both, the client and the server.)
Since Jersey 2.8, the feature gets automatically discovered and registered when Jersey SSE module is
put on the application's classpath. The automatic discovery and registration of SSE feature can be suppressed
by setting &jersey.sse.SseFeature.DISABLE_SSE; property to <literal>true</literal>.
The behavior can also be selectively suppressed in either client or server runtime by setting
&jersey.sse.SseFeature.DISABLE_SSE_CLIENT; or &jersey.sse.SseFeature.DISABLE_SSE_SERVER; property
respectively.
</para>
</note>
</para>
<section>
<title>Implementing SSE support in a JAX-RS resource</title>
<section>
<title>Simple SSE resource method</title>
<para>
<example xml:id="example-simple-sse">
<title>Simple SSE resource method</title>
<programlisting language="java" linenumbering="numbered">...
import org.glassfish.jersey.media.sse.EventOutput;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.glassfish.jersey.media.sse.SseFeature;
...
@Path("events")
public static class SseResource {
@GET
@Produces(SseFeature.SERVER_SENT_EVENTS)
public EventOutput getServerSentEvents() {
final EventOutput eventOutput = new EventOutput();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
// ... code that waits 1 second
final OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
eventBuilder.name("message-to-client");
eventBuilder.data(String.class, "Hello world " + i + "!");
final OutboundEvent event = eventBuilder.build();
eventOutput.write(event);
}
} catch (IOException e) {
throw new RuntimeException("Error when writing the event.", e);
} finally {
try {
eventOutput.close();
} catch (IOException ioClose) {
throw new RuntimeException("Error when closing the event output.", ioClose);
}
}
}
}).start();
return eventOutput;
}
}
</programlisting>
</example>
The code above defines the resource deployed on URI <literal>"/events"</literal>. This resource has a single
&jaxrs.GET; resource method which returns as an entity &jersey.sse.EventOutput; - an extension of generic
Jersey
&jersey.server.ChunkedOutput; API for output chunked message processing.
</para>
<para>
In the<xref linkend="example-simple-sse"/>, the resource method creates a new thread that sends a sequence of
10 events. There is a 1 second delay between two subsequent events as indicated in a comment. Each event is
represented by &lit.jersey.sse.OutboundEvent; type and is built with a help of an outbound event
<literal>Builder</literal>. The &lit.jersey.sse.OutboundEvent; reflects the standardized format of SSE
messages
and contains properties that represent <literal>name</literal> (for named
events), <literal>comment</literal>, <literal>data</literal> or <literal>id</literal>. The code also sets the
event data media type using the <literal>mediaType(MediaType)</literal> method on the
<literal>eventBuilder</literal>. The media type, together with the data type set by the
<literal>data(Class, Object></literal>
method (in our case <literal>String.class</literal>), is used
for serialization of the event data. Note that the event data media type will not be written to any headers as
the response <literal>Content-type</literal> header is already defined by the &lit.jaxrs.Produces; and set to
<literal>"text/event-stream"</literal>
using constant from the &lit.jersey.sse.SseFeature;.
The event media type is used for serialization of event <literal>data</literal>. Event data media type and
Java
type are used to select the proper &jaxrs.ext.MessageBodyWriter; for event data serialization and are passed
to the selected writer that serializes the event <literal>data</literal> content. In our case the string
<literal>"Hello world " + i + "!"</literal>
is serialized as <literal>"text/plain"</literal>. In event
<literal>data</literal>
you can send any Java entity and associate it with any media type that you would be able
to serialize with an available &lit.jaxrs.ext.MessageBodyWriter;. Typically, you may want to send e.g. JSON
data,
so you would fill the <literal>data</literal> with a JAXB annotated bean instance and define media type to
JSON.
<note>
<para>
If the event media type is not set explicitly, the <literal>"text/plain"</literal> media type is used
by default.
</para>
</note>
</para>
<para>
Once an outbound event is ready, it can be written to the <literal>eventOutput</literal>. At that point the
event
is serialized by internal &lit.jersey.sse.OutboundEventWriter; which uses an appropriate
&lit.jaxrs.ext.MessageBodyWriter; to serialize the <literal>"Hello world " + i + "!"</literal> string. You can
send as many messages as you like. At the end of the thread execution the response is closed which also closes
the connection to the client. After that, no more messages can be sent to the client on this connection. If
the
client would like to receive more messages, it would have to send a new request to the server to initiate a
new SSE streaming connection.
</para>
<para>
A client connecting to our SSE-enabled resource will receive the exact same output as in the corresponding
example
in the JAX-RS implementation example.
<screen language="text" linenumbering="unnumbered">event: message-to-client
data: Hello world 0!
event: message-to-client
data: Hello world 1!
...
</screen>
</para>
</section>
<section>
<title>Broadcasting</title>
<para>
Jersey SSE server API defines &jersey.sse.SseBroadcaster; which allows to broadcast individual events to
multiple
clients. A simple broadcasting implementation is shown in the following example:
<example>
<title>Broadcasting SSE messages</title>
<programlisting language="java" linenumbering="numbered">...
import org.glassfish.jersey.media.sse.SseBroadcaster;
...
@Singleton
@Path("broadcast")
public static class BroadcasterResource {
private SseBroadcaster broadcaster = new SseBroadcaster();
@POST
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public String broadcastMessage(String message) {
OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
OutboundEvent event = eventBuilder.name("message")
.mediaType(MediaType.TEXT_PLAIN_TYPE)
.data(String.class, message)
.build();
broadcaster.broadcast(event);
return "Message '" + message + "' has been broadcast.";
}
@GET
@Produces(SseFeature.SERVER_SENT_EVENTS)
public EventOutput listenToBroadcast() {
final EventOutput eventOutput = new EventOutput();
this.broadcaster.add(eventOutput);
return eventOutput;
}
}
</programlisting>
</example>
The example is similar to its relevant JAX-RS counterpart. The <literal>listenToBroadcast()</literal> resource
method creates a new &lit.jersey.sse.EventOutput; representing the connection to the requesting client
and registers this <literal>eventOutput</literal> instance with the singleton <literal>broadcaster</literal>,
using its <literal>add(EventOutput)</literal> method. The method then returns the
<literal>eventOutput</literal>
which causes Jersey to bind the <literal>eventOutput</literal> instance with the requesting client and send
the
response HTTP headers to the client. The client connection remains open and the client is now waiting ready to
receive new SSE events. All the events are written to the <literal>eventOutput</literal> by
<literal>broadcaster</literal>
later on.
</para>
<para>
When a client wants to broadcast new message to all the clients listening on their SSE connections,
it sends a &lit.http.POST; request to <literal>BroadcasterResource</literal> resource with the message
content.
The method <literal>broadcastMessage(String)</literal> is invoked on
<literal>BroadcasterResource</literal>
resource with the message content as an input parameter. A new SSE outbound event is built in the standard way
and passed to the broadcaster. The broadcaster internally invokes <literal>write(OutboundEvent)</literal> on
all
registered &lit.jersey.sse.EventOutput;s. After that the method just return a standard text response
to the &lit.http.POST;ing client to inform the client that the message was successfully broadcast.
</para>
</section>
</section>
<section>
<title>Consuming SSE events with Jersey clients</title>
<para>
On the client side, Jersey exposes APIs that support receiving and processing SSE events using two programming
models:
<simplelist>
<member>Pull model - pulling events from a &jersey.sse.EventInput;, or
</member>
<member>Push model - listening for asynchronous notifications of &lit.jersey.sse.EventSource;
</member>
</simplelist>
The push model is similar to what is implemented in the JAX-RS SSE API. The pull model does not have a direct
counterpart in the JAX-RS API and has to be implemented by the developer, if required.
</para>
<section>
<title>Reading SSE events with &lit.jersey.sse.EventInput;
</title>
<para>
The events can be read on the client side from a &jersey.sse.EventInput;. See the following code:
<programlisting language="java" linenumbering="numbered">Client client = ClientBuilder.newBuilder()
.register(SseFeature.class).build();
WebTarget target = client.target("http://localhost:9998/events");
EventInput eventInput = target.request().get(EventInput.class);
while (!eventInput.isClosed()) {
final InboundEvent inboundEvent = eventInput.read();
if (inboundEvent == null) {
// connection has been closed
break;
}
System.out.println(inboundEvent.getName() + "; " + inboundEvent.readData(String.class));
}
</programlisting>
In this example, a client connects to the server where the <literal>SseResource</literal> from the
<xref linkend="example-simple-sse"/>
is deployed. At first, a new JAX-RS/Jersey
<literal>client</literal>
instance is created with a &lit.jersey.sse.SseFeature; registered. Then a &jaxrs.client.WebTarget; instance is
retrieved from the <literal>client</literal> and is used to invoke a HTTP request. The returned response
entity
is directly read as a &lit.jersey.sse.EventInput; Java type, which is an extension of Jersey
&lit.jersey.client.ChunkedInput; that provides generic support for consuming chunked message payloads. The
code in the example then process starts a loop to process the inbound SSE events read from the
<literal>eventInput</literal>
response stream. Each chunk read from the input is a &lit.jersey.sse.InboundEvent;.
The method <literal>InboundEvent.readData(Class)</literal> provides a way for the client to indicate what Java
type
should be used for the event data de-serialization. In our example, individual events are de-serialized as
<literal>String</literal>
Java type instances. This method internally finds and executes a proper
&jaxrs.ext.MessageBodyReader; which is the used to do the actual de-serialization. This is similar to reading
an
entity from the &jaxrs.core.Response; by <literal>readEntity(Class)</literal>. The method
<literal>readData</literal>
can also throw a &jaxrs.ProcessingException;.
</para>
<para>
The &lit.null; check on <literal>inboundEvent</literal> is necessary to make sure that the chunk was properly
read and connection has not been closed by the server. Once the connection is closed, the loop terminates and
the program completes execution. The client code produces the following console output:
<screen language="text" linenumbering="unnumbered">message-to-client; Hello world 0!
message-to-client; Hello world 1!
message-to-client; Hello world 2!
message-to-client; Hello world 3!
message-to-client; Hello world 4!
message-to-client; Hello world 5!
message-to-client; Hello world 6!
message-to-client; Hello world 7!
message-to-client; Hello world 8!
message-to-client; Hello world 9!
</screen>
</para>
</section>
<section>
<title>Asynchronous SSE processing with &lit.jersey.sse.EventSource;
</title>
<para>
The main Jersey-specific SSE client API component used to read SSE events asynchronously is
&jersey.sse.EventSource;. The usage of the &lit.jersey.sse.EventSource; is shown on the following example.
<example xml:id="sse.ex.client.eventListener">
<title>Registering &lit.jersey.sse.EventListener; with &lit.jersey.sse.EventSource;
</title>
<programlisting language="java" linenumbering="numbered">Client client = ClientBuilder.newBuilder()
.register(SseFeature.class).build();
WebTarget target = client.target("http://example.com/events");
EventSource eventSource = EventSource.target(target).build();
EventListener listener = new EventListener() {
@Override
public void onEvent(InboundEvent inboundEvent) {
System.out.println(inboundEvent.getName() + "; " + inboundEvent.readData(String.class));
}
};
eventSource.register(listener, "message-to-client");
eventSource.open();
...
eventSource.close();
</programlisting>
</example>
In this example, the client code again connects to the server where the <literal>SseResource</literal> from
the
<xref linkend="example-simple-sse"/>
is deployed. The &jaxrs.client.Client; instance
is again created and initialized with &jersey.sse.SseFeature;. Then the &jaxrs.client.WebTarget; is built.
In this case a request to the web target is not made directly in the code, instead, the web target instance
is used to initialize a new &jersey.sse.EventSource.Builder; instance that is used to build a new
&lit.jersey.sse.EventSource;. The choice of <literal>build()</literal> method is important, as it tells
the &lit.jersey.sse.EventSource.Builder; to create a new &lit.jersey.sse.EventSource; that is not
automatically
connected to the <literal>target</literal>. The connection is established only later by manually invoking