Network programming with SFML and Node.js: Part 3

 

 

In part 1 we look at a very basic example of sending information across the wire from SFML to NodeJS.  In part 2 we looked at JSON encoding high score data, that is then sent via UDP socket to a Node server.  Now we complete the process, by having Node return high score data back to your C++ SFML app.  By the end of this section, you will have all the code you need for a functioning ( if somewhat fragile ) high score client and server.

 

 

First we look at the C++ code.  Things are very similar to part 2, except code has been refactored a bit for re-use.  Lets look now.

 

 

#include "SFML/Network.hpp" #include "JSON.h" #include <iostream> void MakePacket(const wchar_t* action, const wchar_t * name, const float score, sf::Packet &packet) { JSONObject data; data[L"action"] = new JSONValue(action); data[L"name"] = new JSONValue(name); data[L"score"] = new JSONValue(score); JSONValue * val = new JSONValue(data); data.clear(); std::wstring dataString = val->Stringify(); delete val; std::string notSoWide; notSoWide.assign(dataString.begin(),dataString.end()); packet.Append(notSoWide.c_str(),notSoWide.length()); } int main(int argc, char* argv[]) { sf::IPAddress ip("127.0.0.1"); sf::SocketUDP socket; sf::Packet packet; unsigned short port = 1000; unsigned short respPort = 1001; if(argc == 1) { //No arguments means program should retrieve scores and print them MakePacket(L"GetScores",L"",0.0f,packet); socket.Bind(respPort); socket.Send(packet,ip,port); char buffer[512]; // The buffer to store raw response data in sf::IPAddress respIP; // The ip address where the response came from size_t respSize; // The amount of data actually written to buffer // Now receive a response. This is a blocking call, meaning your program // will hang until a response is received. socket.Receive(buffer,512,respSize,respIP,port); socket.Close(); std::string respString(buffer,respSize); // Now lets turn the string back into JSON JSONValue * jsonHighScores = JSON::Parse(respString.c_str()); if(!jsonHighScores->IsObject()) { std::cout << "Something went wrong, not good."; return -1; } JSONObject root = jsonHighScores->AsObject(); if(root.find(L"Scores") != root.end() && root[L"Scores"]->IsArray()) { JSONArray scores = root[L"Scores"]->AsArray(); std::cout << "Current high scores:" << std::endl; for(int i = 0; i < scores.size();i++) { JSONObject curObj = scores[i]->AsObject(); std::wcout << "Name:" << curObj[L"Name"]->AsString(); std::cout << " High Score:" << curObj[L"Score"]->AsNumber() << std::endl; } } delete jsonHighScores; } else if(argc == 3) { MakePacket(L"AddScore", std::wstring(argv[1],argv[1] + strlen(argv[1])).c_str(), atof(argv[2]), packet); if(socket.Send(packet,ip,port) != sf::Socket::Done) { std::cout << "An error ocurred sending packet" << std::endl; } socket.Close(); } else { std::cout << "Invalid usage, proper format is player name then score, for example:" << std::endl; std::cout << "Or run with no arguments to get a list of scores returned" << std::endl; std::cout << "Scoreboard "Player Name" 42" << std::endl; return -1; } return 0; }

Click here to download Scoreboard.cpp

 

 

We have reorganized slightly to use a set of if/else’s based on the number of parameters passed in.  The common code between the two handled conditions has been moved to the method MakePacket(), which contains nothing new from last part.  It’s the section where argc == 1 ( which means there were no parameters specified, as argc at 1 represents the executables name ) that we are interested in.  If the user runs the application from the command line with no parameters, we want to fetch the high scores from the server.

 

 

The request process is the same, although we are passing the action GetScores instead.  One key difference is we Bind our port.  Think of this action as say “Yo!  This port is ours!”.  Only one application per computer can have access to a given port.  This is why we run our server on 1000, but then bind our response port on 1001, since client and server on running on the same machine.  Unlike when we add a score, for GetScores we want to listen for a response, which is what we do with socket.Receive().  Keep in mind, this action blocks, so your program wont be able to continue until this is done.  There are alternatives ( like Selector ) if this behavior is undesirable.

 

 

Now assuming Receive() worked correctly ( which in the Real World™ you shouldn’t!), buffer will be full of our JSON encoded string, while respSize will represent the amount of buffer that was actually used.  Using these two pieces of information, lets create a string from only the meaningful bits ( the rest of the buffer will be full of gibberish ).  We now turn our string back into JSON ( in production code, I would extend the JSON library to work with standard strings ), and check to see if it is a valid JSON object, error out if it’s not.  Now it’s a matter of parsing out the JSON into meaningful form.  Remember that a JSONObject is actually just a STL map of key/value pairs, so we find our array of type JSONArray named Scores.  Each item in that array is in turn another map, so we loop through them all find the value for “Name” and “Score”, turn them back into their native type and display them out to the console.  And that’s about it.

 

 

Now lets take a look at the server side of things.  Here we made much less invasive changes, so lets just look at what has changed.  To see the fully modified server.js click here.  I no doubt forgot a small change here or there, I always do!

 

 

First we add another condition to the switch statement as follows:

 

case "GetScores": console.log("Get Scores called"); // Turn highscores back into a JSON encoded string var highScoreAsString = JSON.stringify(highScores); // Create a buffer to hold that string var responseBuffer = new Buffer(highScoreAsString.length); // Write the string to the buffer responseBuffer.write(highScoreAsString); // Send it back to the client, using the addressing information // passed in via rinfo server.send(responseBuffer,0,responseBuffer.length, rinfo.port,rinfo.address, function(err, sent){ if(err) console.log("Error sending response"); else console.log("Responded to client at " + rinfo.address + ":" + rinfo.port ); }); break;

 

 

The code is pretty much documented, the nutshell version is, we turn our high score information into a JSON encoded string, make a buffer large enough for the string, copy the string to the buffer, then call send to the address and port the request came from, as defined in rinfo.

 

 

Then, mostly because we can, we use Node to create an extremely simple high score web server, so if you visit this site with your browser you can get a current list of high scores.  Look how laughably easy that task is!

 

 

// Now, lets show off a bit, with the worlds simplest web server. // Dynamically creates webpage showing current high scores. var webServer = require('http'); webServer.createServer(function(req,res){ res.writeHead(200, {'Content-Type': 'text/html'}); res.write("<html><body><h1>High Scores</h1><ul>"); for(i=0;i < highScores.Scores.length;++i) { res.write(highScores.Scores[i].Name + "&nbsp;&nbsp;" + highScores.Scores[i].Score + "<br />"); } res.write("</ul></body></html>"); res.end(); }).listen(80);

 

 

 

And… that’s it!  So now, lets take a look at things in action.  Open a command prompt and launch your server.js in node.  Then in another command prompt run scoreboard.exe, like this:

 

 

Here is using Scoreboard from the command line:

image

 

 

And here is the server handling these requests:

 

image

 

 

Finally, fire up a web browser and hit 127.0.0.1 ( assuming you don’t have another webserver running on your machine ):

 

image

 

 

It ain’t pretty, but it is a fully functioning high score server.  All you need to do is add a layer of security, harden things a bit with an iota of fault tolerance, pretty things up a bit and you are set.

 

 

As always, you can download the complete project zip here.

Programming Node


Scroll to Top