Just want to run this design problem that I have at my end past you fellow gophers.
Its a little bit complicated, so please bear with me if the description is a little verbose .... its important to give all the details up front in this case, even if that does make for boring reading.
Your opinions will be most welcome !
Background Info & Glossary :
Golang's net/rpc package allows a Go program to make function calls against another (remote) Go program.
The function call is in the form : rpcClient.Call(nameOfRemoteFn string, params Type, returnedValues Type)
The rpc calls work fine over a number of different connection types, including websockets
The rpc calls use Go's own encoding/gob binary format by default (it does support custom formats as well, but that is hard work)
GopherJS is a Go -> JS transpiler that allows you to write browser apps in Go, and it supports :
- full range of Go types
- goroutines
- channels
- rpc, with gob encoding
- DOM manipulation
- 3rd party JS libs
Situation :
- Multi User game style of application, with a requirement for a constant volume of bi-directional data flow between FrontEnd and BackEnd
- Standard "web application" architecture, with a Go server at the backend, which handles standard HTTP requests with no problems.
- Standard SPA "web application" at the front end, also written in Go (GopherJS)
- On startup, the front end loads itself up and uses vanilla HTTP protocols to communicate with the backend. This includes loading resources, images, and basic navigation through some publicly available pages. So far so good.
- When the user at the front end decides to Login to the guts of the system, the following steps happen :
- FrontEnd opens a new websocket connection to the BackEnd, and then registers itself as an rpc client on that connection
- BackEnd receives new websocket, and then registers a "Login" service on that websocket using net/rpc
- FrontEnd renders a login form. User enters login credentials, then clicks "submit"
- On Login submit, FrontEnd makes a call to the "Login" service via rpc, using the login creds entered on that form
- .... <binary frames to and fro with gob encoding magic here> ....
- On fail, the socket is dropped, and the user has to try again. GOTO 1
- On successful login - FrontEnd receives a whole lot of data in the form of a Go struct, and then gets on with it
At this stage, the BackEnd knows which user is associated with that socket connection, so it can treat all data over that connection as validated, and can easily associate other application level context with that socket from here on.
BackEnd then registers any other functions needed for that user as rpc services on that socket. Note that depending on who it is that logs in, the set of functions can be very different in each case, and this (as it turns out) is an exceptionally elegant way of managing access to the BackEnd functions on a per-user basis. (ie - no more need for cookies, JWT tokens, decryption, per-message validation of REST requests, blah blah blah == very fast and secure comms with minimal code)
At this stage, the FrontEnd now has all the "world state data" sitting in memory, in the form of Go Types, and a whole set of Go functions that it can call directly on the BackEnd. Very nice !
Numerous Problems :
1) As things happen in the "game world", the server will need to send updated world-state data to each connected client.
Such updates from the BackEnd to the FrontEnd do not need to be replied to .... so they can be simple messages (or complete RPC calls to the FrontEnd ... doesnt really matter)
So the FrontEnd now needs to look for random messages from the BackEnd, and act on them when they happen, without interfering with the outgoing RPC calls
This is a classic Client/Server problem, just raising its head once again, in a new context.
Looking at the RPC over the wire, it goes roughly like this :
- Client sends some binary frames over the connection
- <some delay>
- Server sends some binary frames over the same connection in reply
... so any additional data sent over the same connection between steps 1 & 3 will break things
2) Network timeouts and Keepalives
In order to keep the socket alive, (in general, and when used through Nginx or similar) Pings and KeepAlive packets may be needed.
If so, keepalive packets will probably be generated from a separate goroutine that has no knowledge of the state of any other RPC calls that may be in transit
Whatever keepalive strategy is used, it cant interfere with the RPC calls that might be in transit
3) Loss of connection
If the connection drops out, the FrontEnd will (automatically) re-connect, and the application state will be restored. I have that covered, but thought I should mention it.
Numerous Possible Solutions :
- Write a new layer of middleware that sits between the websocket and net/rpc, to magically re-order any network frames in the case of some intermediate message corrupting the RPC call ?
- Dont use net/rpc in this case, fall back to an async message passing protocol. That would work ..... but still, I would <like> to use rpc for this if I can, because coding should be about fun and discovery !
- Open 2 sockets - one that is strictly RPC to the BackEnd, and another socket that is just for world state updates to the FrontEnd. .... That would work too, but sounds ugly
What do you gophers think ?
Am I missing the obvious here ?