Prerequisites

*If developing on Windows, Docker requires a Windows Pro license.

Overview

The game first calls EnableRTT to enable real-time, bidirectional communication with brainCloud (BC). The game then tries to find or create a lobby. Lobby events are sent through RTT. Once the lobby meets launch requirements, brainCloud creates the Room Server and gives the game the information to connect. Each player will receive a passcode to validate that they belong to this server.

Meanwhile, the Room Server communicates with brainCloud using our S2S library (Server to Server) to get more info about the lobby. The Lobby includes the profile IDs, custom settings, passcodes, etc.

Once the Room Server has loaded the level and created its sockets, it tells brainCloud that it is ready. brainCloud then communicates the ready state to all players. They can then start connecting to the Room Server.

Below is a sequence diagram demonstrating the flow.

You are responsible for writing the Room Server (RS) and the Game. This is what this article will be about. We will call them Server and Client, respectively.

For gameplay communication between the Client and the Server, we will use a simple TCP socket. This article isn't about networking, so we keep this part simple without worrying about best practices.

The full source for this article can be found on GitHub.

Server

brainCloud S2S library

Create a new folder for your Room Server code on your computer. CD to it, and init an empty git repository:

git init

Add the brainCloud S2S library as a git submodule from the root folder.

git submodule add https://github.com/getbraincloud/brainclouds2s-cpp.git

CMake

CMake is a tool that generates your C++ solution files or makefiles. This will make it easier for us when building for Linux inside of our Docker image. If you are already comfortable with a different tool, you're free to use a different one.

Create a file called CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)

# Set up project
project(MyRoomServer)
add_executable(${PROJECT_NAME} main.cpp)
# Add JsonCpp library to help us parse Json Strings
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}
"${CMAKE_CURRENT_SOURCE_DIR}/brainclouds2s-cpp/cmake/")
find_package(JsonCpp REQUIRED)
# Add brainCloud S2S library
add_subdirectory(brainclouds2s-cpp)
# Include paths and libraries
target_include_directories(${PROJECT_NAME} PRIVATE
brainclouds2s-cpp/include
${JSONCPP_INCLUDE_DIRS}
)
target_link_libraries(${PROJECT_NAME} PRIVATE
brainclouds2s
${JSONCPP_LIBRARY}
)

Do not execute CMake yet. This will run inside our build docker script. If you try to compile this on Windows, it will fail.

main.cpp

The steps are as follow:

  1. Get environement variables

  2. Request the Lobby Json from brainCloud through S2S

  3. Setup our TCP socket

  4. Tell brainCloud that we are ready

  5. Accept new connection

  6. Validate the user's passcode

  7. Send success message to the user

Let's take that literally. Our main() function will look like this:

int main(int argc, char** argv) 
{
printf("-- My Room Server 1.0 --\n");

getEnvironementVariables();
requestLobbyJson();
setupTCPSocket();
tellBraincloudWeAreReady();
acceptNewConnection();
validateUserPasscode();
sendResponse();

return 0;
}

Here is the full source.

#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <cinttypes>
#include <string>
#include <sstream>
#include <json/json.h>
#include <brainclouds2s.h>
using namespace std;
static const uint16_t PORT = 7777;
// The packet structure for answering to the user
struct Response
{
uint16_t size;
const char message[10] = "CONNECTED";
};
// Prototypes
void getEnvironementVariables();
void requestLobbyJson();
void setupTCPSocket();
void tellBraincloudWeAreReady();
void acceptNewConnection();
void validateUserPasscode();
void sendResponse();

int main(int argc, char** argv)
{
printf("-- My Room Server 1.0 --\n");
getEnvironementVariables();
requestLobbyJson();
setupTCPSocket();
tellBraincloudWeAreReady();
acceptNewConnection();
validateUserPasscode();
sendResponse();
return 0;
}
// Globals
string serverPort;
string serverHost;
string appId;
string serverSecret;
string serverName;
string lobbyId;
S2SContextRef s2s;
Json::Value lobbyJson;
int serverSocket;
int clientSocket;

void getEnvironementVariables()
{
const char* SERVER_PORT = getenv("SERVER_PORT");
const char* SERVER_HOST = getenv("SERVER_HOST");
const char* APP_ID = getenv("APP_ID");
const char* SERVER_SECRET = getenv("SERVER_SECRET");
const char* SERVER_NAME = getenv("SERVER_NAME");
const char* LOBBY_ID = getenv("LOBBY_ID");
printf("SERVER_PORT: %s\n", SERVER_PORT);
printf("SERVER_HOST: %s\n", SERVER_HOST);
printf("APP_ID: %s\n", APP_ID);
printf("SERVER_SECRET: %s\n", SERVER_SECRET);
printf("SERVER_NAME: %s\n", SERVER_NAME);
printf("LOBBY_ID: %s\n", LOBBY_ID);
if (!SERVER_PORT || !SERVER_HOST || !APP_ID ||
!SERVER_SECRET || !SERVER_NAME || !LOBBY_ID)
{
printf("Invalid parameters\n");
exit(EXIT_FAILURE);
}
serverPort = SERVER_PORT; serverHost = SERVER_HOST; appId = APP_ID; serverSecret = SERVER_SECRET; serverName = SERVER_NAME; lobbyId = LOBBY_ID; }
void requestLobbyJson()
{
// Create S2S context
auto s2sUrl = "https://" + serverHost + ":" +
serverPort + "/s2sdispatcher";
s2s = S2SContext::create(appId, serverName, serverSecret, s2sUrl);
s2s->setLogEnabled(true);
// Build request
Json::Value requestJson;
requestJson["service"] = "lobby";
requestJson["operation"] = "GET_LOBBY_DATA";
requestJson["data"]["lobbyId"] = lobbyId;
string request;
stringstream ss;
ss << requestJson;
request = ss.str();
// Perform the request. For simplicity, here we call the Sync version
// and block until we get the result
auto response = s2s->requestSync(request);
ss.str(response);
ss >> lobbyJson;
if (lobbyJson["status"].asInt() != 200)
exit(EXIT_FAILURE); }
void setupTCPSocket() { // Create a TCP socket serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (!serverSocket) { printf("Failed socket()\n"); exit(EXIT_FAILURE); }
// Forcefully attaching socket to the port 7777
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
if (setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
&opt, sizeof(opt)))
{
printf("setsockopt\n");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(serverSocket, (struct sockaddr*)&address, sizeof(address)) < 0)
{ printf("bind failed\n"); exit(EXIT_FAILURE); }
// Set the socket for listening. For this example, accept 1 connection
if (listen(serverSocket, 1) < 0) { printf("Failed listen()\n"); exit(EXIT_FAILURE); } }
void tellBraincloudWeAreReady() { // Build request Json::Value requestJson; requestJson["service"] = "lobby"; requestJson["operation"] = "SYS_ROOM_READY"; requestJson["data"]["lobbyId"] = lobbyId;
string request;
stringstream ss; ss << requestJson; request = ss.str();
// Perform the request. For simplicity again, we use the sync version.
s2s->requestSync(request);
// At this point, we don't need the S2S context anymore. But if your
// server sends score data to brainCloud during the match or at the end, // you should hold on to is and call runCallbacks() each frame. s2s.reset(); }
void acceptNewConnection()
{ struct sockaddr_in address; int addrlen = sizeof(address);
// Accept new connections
clientSocket = accept(serverSocket, (struct sockaddr *)&address, (socklen_t*)&addrlen); if (clientSocket < 0) { printf("Failed accept()\n"); exit(EXIT_FAILURE); }
printf("New connection\n"); }
void validateUserPasscode()
{ // Read packet from client. char passcode[1024] = {0}; read(clientSocket, passcode, 1024);
// In our example, the first packet will be his passcode.
// This way we can validate if he's supposed to be here.
printf("Client passcode: %s\n", passcode);
for (const auto& memberJson : lobbyJson["data"]["members"])
{ if (memberJson["passcode"].asString() == passcode) return; // Found it }
// Not found
printf("Wrong passcode\n"); exit(EXIT_FAILURE); }
void sendResponse()
{ // Tell the client he's connected Response response; response.size = htons((uint16_t)sizeof(Response)); send(clientSocket, &response, sizeof(response), 0); printf("CONNECTED sent\n"); }

Docker

Now create a file named Dockerfile in the same folder. This file will describe the shell commands to execute while building our docker image. The docker image will contain a small Linux operating system with our compiled code for it.

Let's start by using a Debian image. Name it builder. We will use this one for building only. Doing it this way will give us a small image.

FROM debian:buster-slim as builder

Install required dependencies. We install CMake and a compiler, g++.

# Install app dependencies
RUN apt-get update -y --fix-missing && apt-get install -y \
cmake \
g++ \
libcurl4-openssl-dev \
libjsoncpp-dev

Create a directory where we will copy our source. We also a directory under, called build/ where we will generate the makefiles.

# Create app directory 
RUN mkdir -p /usr/src/app
RUN mkdir -p /usr/src/app/build
WORKDIR /usr/src/app/build

Copy our source files into the app/ directory.

# Bundle app source
COPY . /usr/src/app

Build our server executable. But first using CMake from the build directory, then calling make.

# build 
RUN cmake -DCMAKE_BUILD_TYPE=Release .. RUN make

We specify we want to generate a Release build with full optimizations.

Next, we create a new image. This will be the real one, and we will only keep the executable from the builder image. Always in the same file:

FROM debian:buster-slim

# Install depencencies, but not dev versions, then clean up after ourselves
RUN apt-get update -y --fix-missing && apt-get install -y \
libcurl4 \
libjsoncpp1 && \
rm -rf /var/lib/apt/lists/*
# Create app directory
RUN mkdir -p /usr/src/app
RUN mkdir -p /usr/src/app/build
WORKDIR /usr/src/app/build

Now, we copy the executable from the builder image into this clean new image.

# Copy the pre-built server from the other image 
COPY --from=builder /usr/src/app/build/MyRoomServer .

Finally, specify the entry point. The executable to launch when a container is created from this image.

# Execute 
ENTRYPOINT ["./MyRoomServer"]

The full Dockerfile should now look like this:

FROM debian:buster-slim as builder

# Install app dependencies
RUN apt-get update -y --fix-missing && apt-get install -y \
cmake \
g++ \ libcurl4-openssl-dev \ libjsoncpp-dev
# Create app directory
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app/build WORKDIR /usr/src/app/build
# Bundle app source
COPY . /usr/src/app
# build
RUN cmake -DCMAKE_BUILD_TYPE=Release .. RUN make
#########################################################################
FROM debian:buster-slim

# Install depencencies, but not dev versions, then clean up after ourselves
RUN apt-get update -y --fix-missing && apt-get install -y \ libcurl4 \ libjsoncpp1 && \ rm -rf /var/lib/apt/lists/*
# Create app directory
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app/build WORKDIR /usr/src/app/build
# Copy the pre-built server from the other image
COPY --from=builder /usr/src/app/build/MyRoomServer .
# Execute
ENTRYPOINT ["./MyRoomServer"]

Now, let's try to compile this. First, you need to go on Docker Hub and create a new repository. Call it MyRoomServer. Taking from this other article, if your image is not publicly available, you need to give docker hub user braincloudhost read-only access to the repo.

Once that's done, make sure your docker daemon is running, and build it this way from the command line:

docker build -t yourdockeraccount/myroomserver:1.0 .

If all well, you can now push it to docker hub.

docker push yourdockeraccount/myroomserver:1.0

Portal Setup

In Cloud Code -> My Servers section, create a new server and put the following properties.

Configure the Docker Repo to point to the image we just created. In the Exposed Ports section, we define the port we will expose from within the container that the players will connect to. For more info on the other fields, I would suggest giving this article a read.

Next, we need to create a Lobby Type for the players to join. This is into the section Multiplayer -> Lobbies.

Configure the first page like this.

Those fields are self-explanatory. Leave Teams untouched, and configure the Rules like so.

This will guarantee the match will start as soon as the first ready player joins in. As a note, make sure the Lobby Service feature is enabled.

Client

To test that the Room Server works, we need to implement the client to find a lobby. We can then inspect the log to see if it launched properly. The Client doesn't have to be in C++. If you already have a game client running inside another engine that tries to launch a Lobby, you can skip this section.

brainCloud client library

In this example, we will not mix client and server code. Let us create a new folder on your PC for the Client code.

Add the brainCloud library as a git submodule from the root folder.

git submodule add https://github.com/getbraincloud/braincloud-cpp.git
git submodule update --init --recursive --progress

The last line will clone submodules recursively. The brainCloud C++ library depends on other submodules itself.

CMake

Once that's done, create a new CMakeLists.txt file. This is the easiest approach to set up brainCloud into a C++ project.

cmake_minimum_required(VERSION 3.10)

# Set up project
project(MyGame)
set(CMAKE_CXX_STANDARD 11)
add_executable(${PROJECT_NAME} main.cpp)
# Add brainCloud library
add_subdirectory(braincloud-cpp)
target_include_directories(${PROJECT_NAME} PUBLIC
braincloud-cpp/include/
braincloud-cpp/lib/jsoncpp-1.0.0/
)
target_link_libraries(${PROJECT_NAME} PUBLIC brainCloud)

Notice we are adding include directory for brainCloud and JsonCpp. The latter will help us parse JSON strings coming from brainCloud.

Source

Following is the entire client code. We won't go step by step for the client code because most of the concepts here are taught in other articles. The steps are:

  1. Authenticate brainCloud

  2. Enable RTT

  3. Find Lobby

  4. connect to Room Server

#include <stdio.h>
#include <chrono>
#include <functional>
#include <memory>
#include <sstream>
#include <thread>
#include <braincloud/BrainCloudWrapper.h>
#include <braincloud/IRTTCallback.h>
#include <braincloud/reason_codes.h>
#include <braincloud/internal/IRelayTCPSocket.h>
using namespace BrainCloud;
using namespace std;
using namespace std::chrono;
using namespace std::chrono_literals;
using CxIds = vector<string>;


// brainCloud Callback hooks
class AuthCallback final : public IServerCallback
{
void serverCallback(ServiceName serviceName,
ServiceOperation serviceOperation,
const string& jsonData) override;
void serverError(ServiceName serviceName,
ServiceOperation serviceOperation,
int statusCode, int reasonCode,
const string& jsonError) override;
};
class FindLobbyCallback final : public IServerCallback
{
void serverCallback(ServiceName serviceName,
ServiceOperation serviceOperation,
const string& jsonData) override {}
void serverError(ServiceName serviceName,
ServiceOperation serviceOperation,
int statusCode, int reasonCode,
const string& jsonError) override;Deprecated
};
class RTTConnectCallback final : public IRTTConnectCallback
{
void rttConnectSuccess() override;
void rttConnectFailure(const string& errorMessage) override;
};
class LobbyCallback final : public IRTTCallback
{
void rttCallback(const std::string& jsonData) override;
};

// Globals
AuthCallback authCallback;
RTTConnectCallback rttConnectCallback;
FindLobbyCallback findLobbyCallback;
LobbyCallback lobbyCallback;
bool isGameRunning = true;
unique_ptr<BrainCloudWrapper> bc;

int main(int argc, char** argv)
{
printf("-- My Game 1.0 --\n");
// Create the brainCloud wrapper
bc.reset(new BrainCloudWrapper("MyGame"));
bc->initialize("https://sharedprod.braincloudservers.com/dispatcherv2",
/* app secret */, /* app id */,
"1.0", "MyCompany", "MyGame");
bc->getBCClient()->enableLogging(true);
// Authenticate
bc->authenticateAnonymous(&authCallback);
// Run brainCloud callbacks in a loop
while (isGameRunning)
{
bc->runCallbacks();
this_thread::sleep_for(17ms); // ~60 fps
}
// Destroy brainCloud wrapper
bc.reset();

return 0;
}


void connectToServer(const Json::Value& connectInfo); // Prototype
void AuthCallback::serverCallback(ServiceName serviceName,
ServiceOperation serviceOperation,
const string& jsonData)
{
// Enable the RTT (real-time tech) service
bc->getRTTService()->enableRTT(&rttConnectCallback, true);
}
void AuthCallback::serverError(ServiceName serviceName,
ServiceOperation serviceOperation,
int statusCode, int reasonCode,
const string& jsonError)
{
printf("Auth error: %s\n", jsonError.c_str());
isGameRunning = false;
}
void RTTConnectCallback::rttConnectSuccess()
{
// Register for Lobby Server real-time events
bc->getRTTService()->registerRTTLobbyCallback(&lobbyCallback);
// Find lobby
string lobbyType = "CppCustomGame";
int rating = 0;
int maxSteps = 1;
string algo = "{\"strategy\":\"ranged-absolute\","
"\"alignment\":\"center\",\"ranges\":[1000]}";
string filter = "{}";
CxIds otherUsers = {};
string settings = "{}";
bool startReady = true;
string extra = "{}";
string teamCode = "all";
bc->getLobbyService()->findOrCreateLobby(lobbyType, rating, maxSteps,
algo, filter, otherUsers,
settings, startReady, extra,
teamCode, &findLobbyCallback);
}
void RTTConnectCallback::rttConnectFailure(const string& errorMessage)
{
printf("RTT connect error: %s\n", errorMessage.c_str());
isGameRunning = false;
}
void FindLobbyCallback::serverError(ServiceName serviceName,
ServiceOperation serviceOperation,
int statusCode, int reasonCode,
const string& jsonError)
{
printf("Find lobby error: %s\n", jsonError.c_str());
isGameRunning = false;
}
void LobbyCallback::rttCallback(const std::string& jsonData)
{
// Convert string to json
Json::Value json;
stringstream ss(jsonData);
ss >> json;
auto operation = json["operation"].asString();
// If the OP is ROOM_READY, this is the signal that we can start
// connecting to our Room Server. Data contains connection info
if (operation == "ROOM_READY")
{
auto serverConnectionInfo = json["data"];
connectToServer(serverConnectionInfo);
}
// DISBANDED means the lobby got disolved. If the reason code for it is
// RTT_ROOM_READY, then all is good. It got disolved because the match
// started.
if (operation == "DISBANDED")
{
if (json["data"]["reason"]["code"].asInt() != RTT_ROOM_READY)
{
// This means the room was disbanded for the wrong reasons
isGameRunning = false;
}
}
}
void connectToServer(const Json::Value& connectInfo)
{
// Create our TCP socket. For simplicity's sake, we are going to reuse
// the brainCloud's internal IRelayTCPSocket.
using TCPSocket = IRelayTCPSocket;
auto address = connectInfo["connectData"]["address"].asString();
auto port = connectInfo["connectData"]["ports"]["7777/tcp"].asInt();
unique_ptr<TCPSocket> tcpSocket(TCPSocket::create(address, port));
// Connect. Calling updateConnection in a loop until connected.
while (!tcpSocket->isConnected())
{
if (!tcpSocket->isValid())
{
printf("TCP Socket connection failed\n");
isGameRunning = false;
return;
}
tcpSocket->updateConnection();
this_thread::sleep_for(10ms);
}
printf("TCP Socket connected\n");
// Send the passcode
auto passcode = connectInfo["passcode"].asString();
tcpSocket->send((const uint8_t*)passcode.data(),
(int)passcode.size() + 1);

// Wait for the response in a loop.
int size;
while (tcpSocket->isValid())
{
auto data = tcpSocket->peek(size);
if (data)
{
// Convert the message. Ignore first 2 bytes,
// they are packet size.
string message = (const char*)(data + 2);
printf("Received: %s\n", message.c_str());
if (message == "CONNECTED")
{
printf("SUCCESS!\n");
isGameRunning = false;
return;
}
}
}
printf("Socket reading failed\n");
isGameRunning = false;
}

Create a build/ folder, add it to a .gitignore file:

build/

CD to the build folder, and generate the solution using CMake:

cmake ..

Open the .sln file. Make sure MyGame is the starting project in Visual Studio by right-clicking it, "Set as Startup Project".

Run it. If all is well, you should see in the log authentication with brainCloud, the lobby's creation, lobby events as it finds the match, TCP connection, then finally "CONNECT" message from the server.

Server Log

Now we want to validate that our docker image was launched on the server. Go into MONITORING / Recent Errors, check the info box, then click Refresh. Room Server logs will be posted every 5mins or so. So be patient. We might have to refresh a few times.

The log entry should look something like:

[instance-id] (ca-central-1) container exited successfully for MyLobbyType_123

Clicking on this log, we should see:

-- My Room Server 1.0 --
SERVER_PORT: 443
SERVER_HOST: sharedprod.braincloudservers.com
APP_ID: *****
SERVER_SECRET: ****************
SERVER_NAME: MyRoomServer
LOBBY_ID: appId:MyRoomServer:id
[S2S SEND appid] {"messages":[{"data":{"appId":"...","serverName":"CppCustomGame","serverSecret":"..."},"operation":"AUTHENTICATE","service":"authenticationV2"}],"packetId":0}
...
...
New connection
Client passcode: 0ae3c8
CONNECTED sent

If your team is using Slack, I recommend setting up the Slack notification for this: http://help.getbraincloud.com/en/articles/4236297-configuring-slack-alerts.

It eases Room Server monitoring.

Did this answer your question?