On Sat, Dec 28, 2013 at 11:06 AM, Tobias Oberstein
<
tobias.o...@gmail.com> wrote:
>>> To be honest, I agree on that. A "pull style" API for WebSocket feels
>>> unnatural to me. But it's a matter of taste, sure.
>> Does a pull-style HTTP server also feel unnatural to you?
> In short, yes.
>
> The way I most often do "classic" Web stuff on Python is via Flask:
>
> @app.route('/')
> def page_home():
> return render_template('home.html')
>
> The decorators route URLs to callbacks which render Jinja templates.
>
> This feels natural.
You got me there. :-)
What I was thinking of was actually a lower level of the HTTP
infrastructure, where you have to write something that parses the HTTP
protocol (and things like form input or other common request content
types). I'm personally pretty happy with the pull style HTTP parsing I
wrote as an example
(
http://code.google.com/p/tulip/source/browse/examples/fetch3.py#112
-- note that this is a client but the server-side header parsing is
about the same).
I thought about this a bit more and I think in the end it comes down
to one's preferred style for writing parsers. Take a parser for a
language like Python -- you can write it in pull style (the lexer
reads a character or perhaps a line at a time, the parser asks the
lexer for a token at a time) or in push style, using a parser
generator (like CPython's parser does). Actually, even there, you can
use one style for the lexer and another style for the parser.
Maybe it's just six of one, half a dozen of the other. Here's an
observation to ponder (for those who haven't made up their mind yet
:-). In either style, you're probably implementing some kind of state
machine.
Using push style, the state machine ends up being represented
explicitly in the form of state variables, e.g. "am I parsing the
status line", "am I parsing the headers", "have I seen the end of the
headers", in addition to some buffers holding a representation of the
stuff you've already parsed (completed headers, request
method/path/version) and the stuff you haven't parsed yet (e.g. the
next incomplete line). Typically those have to be represented as
instance variables on the Protocol (or some higher-level object with a
similar role).
Using pull style, you can often represent the state implicitly in the
form of program location; e.g. an HTTP request/response parser could
start with a readline() call to read the initial request/response,
then a loop reading the headers until a blank line is found, perhaps
an inner loop to handle continuation lines. The buffers may be just
local variables.
I've noticed in the past that some people see state machines
everywhere, and others don't see them even if the problem at hand begs
for one. :-) Personally I probably fall a little on the latter side of
the spectrum -- I have a strong antipathy for Boolean local variables
and often try to get rid of them by rewriting the logic. I also feel
that a lot of programs written in push style end up not handling
errors particularly well -- the nadir of this is your typical
JavaScript embedded in a web page which usually just hangs silently
when the web server times out or sends an unexpected response.
I've only written a small amount of Android code but I sure remember
that it felt nearly impossible to follow the logic of a moderately
complex Android app -- whereas in pull style your abstractions nicely
correspond to e.g. classes or methods (or just functions), in Android
even the simplest logic seemed to be spread across many different
classes, with the linkage between them often expressed separately
(sometimes even in XML or some other dynamic configuration that
requires the reader to switch languages). But I'll add that this was
perhaps due to being a beginner in the Android world (and I haven't
taken it up since).
> Doing something like
>
> while True:
> request = yield from http.block_until_request()
> if request.url == "/":
> request.write(render_template('home.html'))
>
> would seem strange to me.
Yeah, at that level it makes little sense. :-) It seems clear to me
that pull-style and push-style are often combined at different levels
of a software stack.
> In fact, Autobahn provides a protocol layer above WebSocket for RPC and
> PubSub, and the RPC lets you expose Python functions that can be called via
> WebSocket (eg from browser JavaScript):
>
> @exportRpc("com.myapp.myfun")
> def myfun(a, b):
> return a + b
That's nice.
> and in JS:
>
> session.call("com.myapp.myfun", 2, 3).then(
> function (res) {
> console.log(res); // prints 5
> }
> );
I can't read that. :-(
> session.call returns a Promise.
>
> Btw: Promises are coming natively to browsers .. they are specified in
> ECMAScript6.
Well, in the browser world there's little choice but to continue on
the push-based path that was started over a decade ago. That doesn't
mean it's the best programming paradigm. :-)
>> I only vaguely recall what Websockets is used for, but isn't is a
>> cumbersome implementation (constrained by HTTP architecture) for an
>> elegant abstraction (long-living bidirectional streams) which are
>> typically used to send requests "upstream" (i.e. from the server to
>> the client) and send responses back?
> WebSocket is essentially a HTTP compatible opening handshake, that when
> finished, establishes a bidirectional, full-duplex, reliable, message based
> channel.
So would it make sense to build a (modified) transport/protocol
abstraction on top of that for asyncio? It seems the API can't be the
same as the standard asyncio transport/protocol, because message
framing needs to be preserved, but you could probably start with (or
use a variant of) DatagramTransport and DatagramProtocol.
>> I would find it totally natural to have loop in the client that pulls
>> one request from the stream (blocking until it is ready), processes
>> it, and sends a response back -- and similar, in the server, to
>> occasionally send a request to the stream and block until you have the
>> response. (The latter could all be done by a coroutine designed to
>> send one request and receive one response.)
> If I understand right, that API to WebSocket is more like what Aymeric has:
>
> while True:
> msg = yield from websocket.recv()
> // do something with msg, which is a WebSocket message
Yeah, probably.
>>> I don't have plans to implement a pull-style API, since .. yeah,
>>> unnatural;)
Understood, and no problem.
>> Please reconsider.
> One problem is that this would be totally intrusive into my existing
> codebase _and_ API. Another one: Autobahn has multiple implementations in
> other languages (JS and Android currently) - and those APIs are event-driven
> style also, and having APIs similar across enviroments is a plus. Another
> (personal) issue: the synchronous style feels weird to me;) Maybe I am
> spoiled already.
Different strokes for different folks.
>> It's a holy war that asyncio stays out of at the low level by
>> supporting either. But at the high level asyncio's opinion is that in
>> terms of coding convenience, pull-style is better, because it's more
>> familiar to Python programmers. Asyncio (and e.g. StreamReader) put in
>> the work to bridge the interface gap for you.
> Thanks alot for pointing this out! I now understand better. Asyncio at
> high-level is opinionated towards "pull-style". This is fine - and important
> to recognize.
Yup.
> Regarding Autobahn: I think it's already quite cool that we can now support
> Twisted and asyncio at the same time - even if "push style" only ..
Yeah, asyncio uses push-style at the lower levels specifically for
improved interoperability with other frameworks.
> Thanks for your hints and comments!
You're welcome. It's an interesting discussion for me as well.