WebSocket client API
Pavel Rappo
pavel.rappo at oracle.com
Sat Oct 17 18:56:27 UTC 2015
> On 16 Oct 2015, at 22:11, Simone Bordet <simone.bordet at gmail.com> wrote:
>
> Hi,
>
> On Fri, Oct 16, 2015 at 12:35 PM, Pavel Rappo <pavel.rappo at oracle.com> wrote:
>> Hi,
>>
>> Here's a second update on the WebSocket API:
>>
>> webrev: http://cr.openjdk.java.net/~prappo/8087113/webrev.02/
>> javadoc: http://cr.openjdk.java.net/~prappo/8087113/javadoc.02/
>>
>> Main differences from the previous version:
>>
>> * WebSocket has become an abstract class
>
> What is the rationale behind this ?
> Just to avoid subclassing ?
> Why subclassing is negated ?
> I think it's best it remains an interface: it would allow people to
> write wrappers, use java.lang.reflect.Proxy, etc.
This argument can be used for pretty much any class out there. You see, it's a
question of whether subtyping of X should be allowed or disabled, if X has not
been designed with it in mind.
>> * New methods in Builder: headers, connectTimeout
>
> I still don't understand the headers() signature what benefits brings.
> What problems would have a chainable: Builder header(name, value) ?
> Clear semantic and no exceptions about an odd number of String passed in.
The reason was that other Builder's methods (now only 2) have a "replace"
semantics rather than "add":
* <p> If a particular intermediate method is not called, an appropriate
* default value (or behavior) is used. A repeated call to an intermediate
* method overwrites the previous value (or overrides the previous
* behaviour), given no exception is thrown.
If Builder.header was with "add" semantics, it wouldn't be possible to remove
added headers. But now I probably agree with you. In this case it would be
better for the API to provide an "add" behaviour, mostly because it's more
compile time checking friendly.
> I am not sure that passing the listener to the Builder constructor is right.
> There are applications that just stream in one sense, so there would
> be no listener needed, so it appears strange that this is a required
> constructor parameter rather than just another builder method.
Maybe you're right. For now let's remember this argument as (A) to return to it
later.
>> * WebSocket.Builder no longer accepts HttpRequest.Builder; only HttpClient
>> * One Listener instead of many onXXX handlers
>
> I tried to write a few examples with this API, and it's a bit cumbersome to use.
> Below my feedback:
>
> 1. I think there is a mistake in onText(CharBuffer, boolean). Should
> not be onText(CharSequence, boolean) ?
> I don't think CharBuffer can handle properly UTF-8.
I'm not sure I understand what you mean by "handle properly UTF-8". First of
all, CharBuffer implements CharSequence. Secondly, if a user decides to create a
ByteBuffer out of a Text message, CharBuffer is the way to go.
It's a native class for charset decoding/encoding:
java.nio.charset.CharsetEncoder#encode(java.nio.CharBuffer).
No additional conversions are needed. On the other hand, if the user needs a
String, then simple CharBuffer.toString would do. In my opinion, using
CharBuffer directly is the best of two worlds.
> 3. The absence of the WebSocket parameter from onXXX() methods makes
> the API cumbersome to use.
> It forces applications to write this boilerplate over and over:
>
> new WebSocket.Builder("ws://localhost:8080/path", new WebSocket.Listener()
> {
> public WebSocket webSocket;
> public LongConsumer controller;
>
> @Override
> public void onOpen(WebSocket webSocket, LongConsumer flowController)
> {
> this.webSocket = webSocket;
> this.controller = flowController;
> flowController.accept(1);
> }
>
> ...
> }
>
> The WebSocket and LongConsumer needs to be saved from onOpen() to be
> used in other methods, and it's boilerplate code that needs to be
> written all the time.
>
> I suggest to consider to modify the signature of the onXXX() methods to:
>
> CompletableFuture<?> onText(WebSocket, CharSequence, boolean)
>
> and to merge again LongConsumer back into WebSocket. That would make
> the API soo much easier to use.
Easier? In some cases, probably. On the other WebSocket.request(long) is an
internal part of the API that only the Listener should talk to (the same is with
Flow.Subscriber and Flow.Subscription). Moreover, it would be strange (remember
argument (A)?) that one could request something not having any means of
listening to it.
Do we have any good reasons to merge flowController with WebSocket other than
boiler-plate eliminating? Because we just might lose good level of separation
of concerns.
> 4. WebSocket.Listener should have all methods implemented with a
> default implementation. It's just too much to have to implement them
> all when all I want is to receive text messages.
> If WebSocket is passed as parameter to onXXX() methods and
> LongConsumer is merged back into WebSocket, it would be trivial to
> write a correct default implementation of the Listener interface
> (which is now impossible).
Summing up (A) and the passage above, I would provide some AbstractListener that
would take care of a boilerplate code.
>> * Completion handlers with custom contexts are gone;
>
> Finally :)
>
>> * send methods return CompletableFuture<WebSocket>
>
> This parameter is typically not used.
It's too early to refer to something in this API as "typically" :-)
Or you mean for CF in general?
> When an application calls sendXXX() it already has the WebSocket
> reference in scope, so it would be available anyway.
You're right.
> completely stateless
> listeners that would come handy when you are opening a large number of
> connections (you can only use one instance, rather than one per
> connection).
Good argument! We may try to accomplish it a bit differently. But I'm against of
merging flowController with WebSocket.
> Method onOpen() still has reason to exist, of course.
>
>> * onXXX methods in Listener return CompletableFuture<?> as a means of
>> asynchronous completion signalling
>
> The downside of using CF is the allocation of CF instances that the
> application is forced to make to comply with the API.
> Given that this is a low-level API, I have a preference for using
> Consumer<Throwable> in all cases.
In terms of composability those approaches are pretty much the same:
void onText(CharBuffer text, boolean isLast, Consumer<Throwable> handler) {
Consumer<Throwable> throwableConsumer = (t) -> {
if (t == null) flowController.accept(1);
};
sendText(text, isLast, throwableConsumer.andThen(handler::accept));
}
or
CompletableFuture<?> onText(CharBuffer text, boolean isLast) {
return ws.sendText(text, isLast).thenRun(() -> flowController.accept(1));
}
The difference might become serious if we think about one-shot vs reusable
objects. In general. Not about CF vs handler. Reusable handlers are more prone
to bugs as far as I can see now. Because they are inherently same objects but
being used in different time (context). Thus their .accept(Throwable) would mean
different thing in different time. Hence more caution would be needed, especially
on the receiving side, when the app gets an implementation's handler.
Implementation's code will have to become a lot more sophisticated in this case,
because it will now have to correlate a handler to an invocation. (I know nobody
cares about the implementation. I'm just saying.)
As for the API, well CompletableFuture is surely more nice compared to a bare
Consumer. But let me try to implement both things and we could choose later.
We probably don't want to double the number of sending methods, do we? :)
>> * StatusCode is now ClosureCode
>
> "Closure" typically means something different to a programmer.
> I would rename to CloseCode or similar.
I dunno. RFC 6455 uses "closure" all over the the place, e.g.:
As defined in Sections 5.5.1 and 7.4, a Close control frame may
contain a status code indicating a reason for closure.
I think we should ask for an opinion from others. But hey, it's not a
com.sun.tools.javac package we're designing a WebSocket for. I'm sure there
won't be any ambiguity.
> Given that exceptions are relayed via methods (and not try/catch), I
> find no use for WebSocketException subclasses.
What exclusive things does try-catch provide we couldn't emulate with good old
instanceof/getClass()? Well, maybe just a tiny bit of help from a compiler. Or
you say with method-relayed exceptions there's no need to know their type at
all?
> There may be a need for WebSocketException in case of protocol errors
> or spec violations, but I don't see how
> WebSocketException.AsynchronousClose or WebSocketException.Handshake
> are any useful.
Handshake provides a java.net.http.HttpResponse (Joakim [*] has noticed it might
be important). And you say you don't see how a user would benefit from knowing
some of their send operations have been terminated due to asynchronous close?
> WebSocketException.AsynchronousClose seems a duplication of
> java.io.AsynchronousCloseException, and WebSocketException.Handshake
> can just be relayed as a WebSocketException.
You mean java.nio.channels.AsynchronousCloseException? Well yes, but it's not
reusable. The wording and the package confine this exception to channels and
buffers.
> I would rather drop the inner classes and just keep WebSocketException only.
>
> The subclasses also break an "implicit" naming convention where
> exceptions class names always have the "Exception" suffix. The JDK
> consistently use this naming convention apart for some 1.0 class for
> historical reasons (e.g. ThreadDeath).
Good argument. Will return to it later. Thanks!
> Thanks !
>
> --
> Simone Bordet
> http://bordet.blogspot.com
> ---
> Finally, no matter how good the architecture and design are,
> to deliver bug-free software with optimal performance and reliability,
> the implementation technique must be flawless. Victoria Livschitz
-------------------------------------------------------------------------------
[*] http://mail.openjdk.java.net/pipermail/net-dev/2015-August/009078.html
More information about the net-dev
mailing list