Hi there. I'm Moishe Lettvin, an engineer at Google. I've been working on Google Talk for almost 3 years now, and since I started working here I've been interested in making the web in general more "immediate".
Lately I've been thinking about adding realtime communication to gadgets. This proposal is a result of that thinking, with HUGE help from Lev Epshteyn, David Byttow, Brandon Beck and others here at Google. Lev & David specifically suggested that I share this proposal with this group.
Thanks, and thanks in advance for any feedback & ideas you may have.
-Moishe
---
Opensocial has come a long way towards enabling developers to create
rich social interactions on the web's social networking sites, but a
lot of these still have a "play by mail" feel. We're proposing
enhancing the opensocial feature set by offering a more realtime model
of communication - which will enable a whole new class of applications
not really seen anywhere on the web. The initial proposal (which needs
to be fleshed out) is to create a stack that would let developers
connect their users in near real time - similar to an IM session but
with a more domain specific client. Specifically, such a stack would
facilitate creation of a large variety of multi-player apps such as a
shared whiteboard, a realtime translation console, shared map viewing,
various games, and so on.
At this time the proposal below and the APIs are yet to be solidified
(especially on the server side), and we are looking for kindred spirits
to help us nail down requirements and protocols, as well as to gauge
the level of interest in the developer community. Some Containers may
already have a lot of the inner plumbing in place to make this happen.
Obviously, any input at all would be greatly welcome. We're in the
early stages of defining this API; this proposal is meant primarily as
a jumping off-point. It's subject to change and we want to hear ideas.
We propose the creation of two spec endpoints:
-
a client-side JS API for joining and communicating in realtime sessions
-
an HTTP-based arbiter server API for moderating such communications
The gadget talks to the arbiter bot using a defined javascript
API. The arbiter receives HTTP requests based on these API calls and
responds with messages to send to the various gadgets it has knowledge
of. In the middle is a Container-implemented relay, responsible for
translating the javascript API calls into HTTP requests and
distributing messages received via HTTP response to javascript
callbacks in the various gadgets. One characteristic of this model is
that messages are distributed only
as a result of API calls from a gadget -- there's no way for the relay
to get a message from the arbiter except as the result of a request.
This makes certain use cases (eg., a timer-based game where the client
code is untrusted) more complicated to implement but greatly simplifies
the API conceptually and practically.
To make it more concrete, imagine a developer working on a simple chess
game. They would request JS API via a require feature (also supplying a
URL for their arbiter server):
<Require feature="realtime">
<Param name="arbiter">http://example.com/arbiter</Param>
</Require>
This would make available to the client JS the
opensocial.realtime namespace, which would be used to open a session:
var gameKey = gadgets.views.getParams()['gameKey'];
opensocial.realtime.requestSession(gameKey, function(session, result) {
mySession = session;
mySession.setListener(onGameMessage);
startGame();
});
A client joins a Session by supplying a session key - an identifier
(which is implicitly scoped to the application) that uniquely
identifies a Session. Any clients passing the same Session key to
requestSession() will be joined to the same Session (with Sessions
lazily created as needed). In the case of our chess game, the key was
supplied via a View param, but other ways of obtaining it are possible.
The JS API Session object would allow client code to send out JSON
messages, and register a callback to fire when messages come in (in our
case, imagine the
move variable to represent one chess move, black or white):
function onGameMessage(message, sender) {
if (message.type == GAME_MOVE) {
processMove(message.move);
}
}
function sendMove(move) {
var message = {};
message.type = GAME_MOVE;
message.move = move;
mySession.send(message, function(result) {
if (result != opensocial.realtime.SUCCESS) {
undoMove(move);
}
});
}
Interaction with arbiter
The
arbiter functions a the hub through which all session information
flows. In this API, there is no concept of sending a message directly
to another user: messages go only from a gadget to the arbiter, or from
the arbiter to a gadget. The arbiter may -- in fact, in many cases will
-- end up forwarding a message sent by one user to another, but
ultimately everything goes via the arbiter.
Thus,
the arbiter is called when a user wants to join a session, leave a
session, or send a message. These messages all contain a session
identifier, a user identifier, and a message identifier. The arbiter
will respond with a status message indicating success or failure, and
optionally with other information to be relayed to the calling or other
gadgets. Again, the Container-implemented relay is responsible for
distributing these messages and translating them into javascript
callbacks.
It
is assumed that the arbiter will have access to and maintain the
relevant state of the Session - for example in our game of chess, that
state would comprise of the board, the position of all the pieces,
which player plays for black and white, etc. Every message sent to the
arbiter on behalf of the client must have identifying information (such
as the client's Person ID - however it may need to be more flexible
than that to support anonymous play if that is desired)
So,
continuing our example from above, the chess developer wants to keep
some game logic -- determining wins, complex rules like repeating
cycles, and the like -- on the server to simplify gadget code, and
ensure the data is not compromised by a rogue client.
The arbiter receives HTTP requests with parameters corresponding
opensocial.realtime method parameters and JSON corresponding to
javascript objects passed to opensocial.realtime methods. It responds
with XML describing methods and parameters for opensocial.realtime and
JSON for developer-defined objects to pass to those methods.
When the gadget calls opensocial.realtime.requestSession, the arbiter server will receive an HTTP request:
POST /arbiter HTTP 1.1
container=www.igoogle.com
action=RequestSession
sender=a
sessionid=gameKey
The
container
parameter specifies the container making the request. This should be
the url of the container's home page, and will be used by the arbiter
to disambiguate sender id's.
The
action parameter specifies the action for the server to take. Some possible actions are RequestSession, LeaveSession, and SendMessage.
The
sender
parameter identifies the user requesting the action. This is an
OpenSocial Person ID. We will also use OpenSocial Person ID's to
address messages from the server. It is the responsibility of the
Container to determine if the server should be allowed to send a
message to the given user, probably based on if they've installed the
application.
The
sessionid parameter identifies the session
that this action is associated with. Some sessionids may have special
values (for instance, an arbiter may implement a 'lobby' sessionid that
relays global game state to users, including (for instance) sessionids
of available games).
The servlet that handles the POST will verify that the sender is allowed to join the specified session, and return a response:
HTTP/1.x 200 OK
<?xml version="1.0" encoding="UTF-8"?>
<requestsession id='sessionId' status='joined'>
<participant>a</participant>
<participant>b</participant>
</session>
The
Container-implemented relay will call the callback function specified
in the requestSession call with the information from this response.
Now that the game is joined to a session,
it can make a move by sending a message to the arbiter. For purposes of
illustration, the player with id 'a' is playing white, and id 'b' is
playing black. User 'a' moves his bishop to e5, resulting in a call to
the 'send' method on the session object created above. The container
translates this into a request to the arbiter:
sender=a
sessionid=gameKey
message={type: 'MOVE', move: location: 'Be5'}
The arbiter verifies that this is a valid move. It also sees that this
move places the black player in check, and modifies and re-broadcasts
the message because of this (note the addition of the '+' in the
location string).
HTTP/1.x 200 OK
<?xml version="1.0" encoding="UTF-8"?>
<message to='a'>
{type: 'MOVE', move: 'Be5+'}
</message>
<message to=otherId>
{type: 'MOVE', move: 'Be5+'}
</message>
The 200 response code indicates that the bot accepted the message. The
container calls the callback passed to sendMessage with a SUCCESS
result. The contents of the response from the arbiter indicate that
messages should be sent to both members of the session. It's up to the
Container to distribute these messages to the appropriate users -- it
will call the functions specified in the session.setListener calls with
objects corresponding to the deserialized JSON of the contents of the
<message> elements.
The black player then tries unsuccesfully to move out of check by capturing the bishop:
sender=b
sessionid=gameKey
message={type: 'MOVE', move: 'Ke5'}
Unfortunately the bishop is protected by a pawn, so the arbiter rejects the move:
HTTP/1.x 400 Bad Request
<error>
{description: 'Illegal move, you will move into check.'}
</error>
<message to='a'>
{type: 'ATTEMPTED_MOVE', attempt: 'Ke5'}
</message>
The container calls the callback passed to sendMessage with a "Bad
Request" result. Note that even this response could contain messages
destined for other users, so the container needs to process these as it
would a success response.
Note that the above is illustrated
using XML to wrap messages to/from the HTTP server, with JSON
representing objects created by the gadget itself. Another possibility
is to use JSON exclusively.