With much of the world in lockdown at the moment, my friends and I wanted a way to play our favourite game, Celebrity, over video chat.
This seemed like a fun coding project, so I put together a little web app. The functionality it would need to provide would be the ability for everyone to put names into a virtual hat, and for everyone to then take turns drawing names from the hat at random.
My experience is in desktop application programming, and I have essentially none in web development. My goal was to get something minimally functional as quickly as possible, with no thought given to code elegance or maintainability, and to learn a little bit along the way.
I settled on a pattern which was sufficient for this goal, but is unlikely to scale well for larger projects. This pattern is illustrated at the bottom of the post, together with my main questions, but I have some more specific minor questions along the way.
The complete code is on github, but hopefully there's enough detail in this post for you to answer my questions without looking at all of it.
Some decisions I took straight away:
- The game would run on the server, so that there would be a single authority on the game's current state
- The server would be written in Java, since it's the language I'm most familiar with
- The client would use HTML forms + buttons, and Javascript, since my background knowledge suggested this would be the quickest and simplest path to take.
And the key bits of knowledge I was missing:
- How do I set up an HTTP server?
- How do I send data back and forth between browser and server, such as the celebrity names, whose turn it is, when a player has started/ended their turn, and what a player is doing on their turn?
- How do I create a webpage whose content changes dynamically in response to user actions or messages from the server?
- How do I keep track of individual clients and their sessions?
Setting up an HTTP Server
A bit of Googling told me there were many ways to do this, and I was a bit overwhelmed by choice. It looks like probably the most standard way to do something robust and scaleable would be to use Apache, but I saw some suggestions that this could be overkill for simple projects, and I didn't get as far as learning how Apache would run my own code. In the end, I went with the solution in this answer, using the com.sun.net.httpserver.HttpServer
class, since the Hello World example in that answer worked immediately.
What would be the disadvantages of implementing a larger and feature-richer server using this class?
I found I had to write my own code to find the HTML and Javascript files, map their names to URLs the users could access, and read these files and send their content when the associated URL was requested. Is this normal? Isn't there a way to just say "serve all files in this directory"?
Client-Server Communication
It quickly became clear that the standard way to send data from client to server is to create an XMLHttpRequest
in Javascript, and send it using a GET
or POST
request. All my client->server communication was handled this way, except for submission of forms, since the form has a built-in mechanism to submit GET
or POST
requests.
After looking at some examples, I settled on the following pattern, which was sufficient to achieve what I wanted. The client sent a request like so (Javascript):
xmlhttprequest.open("POST", "test", true);
xmlhttprequest.send("param=paramValue¶m2=paramValue2");
And the server could receive this with code like the below (Java):
httpServer.createContext( "/test", new HttpHandler() {
public void handle(HttpExchange aExchange) throws IOException {
// get the text "param=paramValue¶m2=paramValue2" from aExchange.getResponseBody()
// process it as required
// send a response as text
}
} );
The response text was then set to the responseText
field of the XMLHttpRequest
, and again could be processed as required. The response text was also sent as an &
-separated list of key-value pairs.
What I found strange about this was that there was no hard requirement to use XML! I always got errors in the Javascript console that the response text was not well-formed XML, but this didn't break anything. But I assume there must be something I've missed that would mean I could avoid parsing the strings myself.
Aren't there Javascript functions and Java methods for creating and reading tree-like or graph-like data structures, which can be written/read as XML under the hood, without the programmer needing to process the message content directly?
Sometimes I found that the
XMLHttpRequest
wasn't sent successfully unless I added a 0.5s wait usingsetTimeout
, before sending the request. I can do more testing and get more detail if necessary, but does anyone know what could be going on there?
For communication server->client, without any initial request from the client, I didn't find a solution. So I settled on each client requesting the entire state of the game every half a second. Since the game state was very small (less than 1 kb in text form), this was sufficient.
What's a better way for the server to send data to the client without any request from the client?
Dynamically Updating The Page
I found two ways to do this, and used both liberally:
- Update the
innerHTML
field of a document element - Lots of hidden
div
s which could be unhidden as needed
I came to prefer the second option, since the final HTML is easier to read - you can see everything that can potentially be displayed. In the future, I would only use the first option for content that isn't known at write-time.
Are there any disadvantages to this approach?
I can see that if I want to keep certain content initially hidden from users, I'd have to avoid this, as they could see it using View Source
. But if they can read Javascript, they could also see anything I'm going to set to an innerHTML
field using a script. So if I really wanted to keep it hidden, I guess I'd have to generate it server-side. Anyway, that wasn't a concern for this game.
Session Handling
I used a very simple solution which probably doesn't count as creating a true session. There is a single HTML page to load, and the server dynamically modifies it by inserting some code to set a cookie with the name-value pair session=<a randomly chosed UUID>
. I found that each further request to the server automatically included this cookie, so I could keep track of identities.
Is that all there is to it?
Putting it all together: Ctrl-F Oriented Programming
The below code snippets illustrate the pattern used throughout the application. They handle the pass
function, in which a user gives up on getting their team to guess the current name, puts the name back in the hat, and draws another one. There's a button defined in HTML, a Javascript function which sends a request to the server to say that the user is passing on a given index in the list of names, a handler in the server to re-shuffle the names and return the new list to the client, and an inner function in the client to handle this response.
The main problem I have with this pattern is that it uses a lot of "Ctrl-F oriented programming". All the following strings need to be kept identical in multiple places, with no automatic check for errors, meaning I need to do a lot of manual checking of the files:
- The button ID
"passButton"
- The Javascript function name
"pass"
- The string
"pass"
used as the second arg toXMLHttpRequest.open
and the string"/pass"
used as the first arg toHttpServer.createContext
- The parameter
"passNameIndex"
used in thePOST
request - The parameter
"nameList"
used in the response to thePOST
request
In such a small application, this isn't a major problem. In a larger one it would surely become a nightmare.
Are there tools to handle this kind of problem? For example, ways to define tightly-coupled client and server code in the same file (e.g. if the server code was also written in Javascript)? Or ways to define such strings in a separate file used by both the server and the client, with a compile-time check that only pre-defined strings are used?
Code snippets follow. HTML defines the button:
<button id="passButton" onclick="pass()">Pass</button>
Javascript sends the request and processes the response:
function pass() {
document.getElementById("passButton").style.display = 'none' // hide button while processing the pass
setTimeout( function() { // timeout mysteriously needed for request to work
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("passButton").style.display = 'block' // restore button
// The game maintains a shuffled list of celebrity names, which is modified in response to the pass.
// Here we process the response by updating the client's shuffled name list to hold the list provided by the server
var arr = toAssocArr(this.responseText)
var nameListString = arr["nameList"];
if ( nameListString != null ) {
nameList = nameListString.split(",");
updateCurrentNameDiv();
}
}
}
// send request
xhttp.onload = function() {}
xhttp.open("POST", "pass", true);
xhttp.send("passNameIndex=" + current_name_index);
}, 500 );
}
Java processes the request and sends the response:
server.createContext( "/pass", new HttpHandler() {
@Override
protected void handle(HttpExchange aExchange) throws IOException {
// From cookie, get Session, then Player, then Game
String sessionID = HttpExchangeUtil.getSessionID(aExchange);
Session session = SessionManager.getSession(sessionID);
if ( session != null ) {
Player player = session.getPlayer();
Game game = player.getGame();
// Convert input string like "passNameIndex=5" to a LinkedHashMap
LinkedHashMap<String, String> requestBody = HttpExchangeUtil.getRequestBodyAsMap(aExchange);
String passNameIndexString = requestBody.get("passNameIndex");
if ( passNameIndexString != null ) {
// parse the provided passNameIndex, process it, and send the new shuffled name list as a response
try {
int passNameIndex = Integer.parseInt(passNameIndexString);
game.setPassOnNameIndex( passNameIndex );
sendResponse(aExchange, HTTPResponseConstants.OK, "nameList=" + String.join(",", game.getShuffledNameList()));
}
catch ( NumberFormatException e ) {
e.printStackTrace();
}
}
}
// No error handling for if session/player/game isn't found, can deal with that later
}
} );
As I said, essentially the whole application followed this pattern. My main questions are:
- For experienced web developers with a similar goal (i.e. something quick 'n' dirty which just manages to work), what would you have done similarly or differently?
- If you were aiming for something bigger, like a long-term project which would scale to a large number of users, and with new features continually being added, what would you have done differently?
Miscellaneous Questions
If I go bigger on my next project, what security issues do I need to consider? As far as I can see, there's no possibility of code injection via the HTML forms, because the input is encoded by default, but have I missed something?
The complete code is on github. To answer the misc question, you probably just need to check the headers and forms in celebrity.html.