Room Servers: brainCloud calls your server, then your server calls back
S2S Basics is the request pattern the spun-up room server uses to talk back to brainCloud. Multiplayer / Relay is what you'd use instead if you don't need a custom server at all.
Everywhere else in this series, brainCloud's relay servers handle the networking directly. Room Servers are for when that's not enough, when a dedicated process needs to run real game logic, not just message fan-out. The pattern is a round trip: a lobby is configured to launch a custom server, brainCloud calls a webhook on that infrastructure to ask for one, the server spins up and answers with connect info, and from that point the server is a first-class brainCloud citizen too, authenticating over S2S the same way any backend would. csharp-examples/RSM (Room Server Manager) is the reference for the launch side. The client side reuses the exact same lobby flow as Relay; it just connects a raw socket to the game's server address instead of brainCloud's.
How it works
- brainCloud requests a server by POSTing to a webhook. The request hits /requestRoomServer with a lobby id in the body. The webhook's job is to validate the request, spin up (or find) a process to host that lobby, and respond with where to reach it.
- The response tells brainCloud, and eventually the client, where to connect. The reply is {lobbyId, connectInfo: {roomId, url, ...}}. This is the same format the client later receives in the lobby's ROOM_READY event; brainCloud simply relays the answer forward.
- The spun-up server authenticates over S2S, using the pattern from the previous article. Once the game server process is running, it's not special-cased. It creates an S2S context with its own app credentials and authenticates like any other backend.
- Some deployments need a readiness handshake before the server accepts players. BrainCloudS2SPrl.IsPreReadyLaunch() gates whether the server needs to run a pre-ready launch sequence before it requests lobby data and starts accepting connections. Check that flag before assuming it's safe to jump straight to GET_LOBBY_DATA.
- The client connects the same way it would for Relay, just to a different address. It watches for the lobby's ROOM_READY event exactly like the Relay article describes, then opens a raw TCP socket, using the same IRelayTCPSocket helper the SDK ships for relay connections, but pointed at the game's server address and port instead of brainCloud's.
Code
static public int Main()
{
string appId = Environment.GetEnvironmentVariable("APP_ID") ?? "20001";
string serverName = Environment.GetEnvironmentVariable("SERVER_NAME") ?? "TestServer";
string serverSecret = Environment.GetEnvironmentVariable("SERVER_SECRET") ?? "...";
string s2sUrl = Environment.GetEnvironmentVariable("S2S_URL") ?? "...";
HttpListener listener = new HttpListener();
listener.Prefixes.Add($"http://*:{port}/");
listener.Start();
HttpListenerContext ctx = listener.GetContext();
HttpListenerRequest req = ctx.Request;
if (req.HttpMethod != "POST" || req.Url.LocalPath != "/requestRoomServer")
{
respond(resp, "Forbidden Request", 403);
return 1;
}
var lobbyJson = JsonReader.Deserialize<Dictionary<string, object>>(reqMessage);
string lobbyId = lobbyJson["id"] as string;
var responseJson = new Dictionary<string, object>()
{
{ "lobbyId", lobbyId },
{ "connectInfo", new Dictionary<string, object>
{
{ "roomId", lobbyId },
{ "url", req.Url.Host },
}}
};
respond(resp, JsonWriter.Serialize(responseJson), 200);
GameServer gameServer = new GameServer(appId, serverName, serverSecret, s2sUrl, lobbyId);
gameServer.Run();
return 0;
}public GameServer(string appId, string serverName, string serverSecret, string s2sUrl, string lobbyId)
{
_lobbyId = lobbyId;
_s2s = new BrainCloudS2S();
_s2s.Init(appId, serverName, serverSecret, false, s2sUrl);
_s2s.Authenticate(OnAuthenticated);
}
void OnAuthenticated(string responseString)
{
var response = JsonReader.Deserialize<Dictionary<string, object>>(responseString);
if ((int)response["status"] != 200)
{
_isRunning = false;
return;
}
if (BrainCloudS2SPrl.IsPreReadyLaunch())
{
_prl = new BrainCloudS2SPrl();
_prl.Start(_s2s, _lobbyId, OnPrlComplete);
}
else
{
RequestLobbyData();
}
}if (operation == "ROOM_READY")
{
serverConnectionInfo = json["data"];
connectToServer();
}
void connectToServer()
{
auto address = serverConnectionInfo["connectData"]["address"].asString();
auto port = serverConnectionInfo["connectData"]["ports"]["7777/tcp"].asInt();
unique_ptr<IRelayTCPSocket> tcpSocket(IRelayTCPSocket::create(address, port));
while (!tcpSocket->isConnected())
{
if (!tcpSocket->isValid())
{
printf("Socket connection failed\n");
isGameRunning = false;
}
tcpSocket->updateConnection();
this_thread::sleep_for(10ms);
}
}Gotchas worth knowing up front
- This is Lobby plus a raw socket handoff, not a separate matchmaking system. Everything from Article 22 about findOrCreateLobby and portal-configured lobby types still applies. Room Servers change what happens after the lobby is ready, not how players find each other.
- The webhook has to respond fast, or the client waits. RSM.cs here is deliberately minimal: listen, validate, spin up, respond. A real deployment needs to handle concurrent requests and probably a process pool, not a listener that blocks on one connection at a time the way this reference does.
- Check IsPreReadyLaunch() before assuming lobby data can be requested immediately. Skipping the PRL handshake when a deployment requires it means the server tries to read lobby state it isn't cleared to see yet.
- The connectData port key is literal, not a placeholder. ports["7777/tcp"] in the client code reads a specific port the server declared it's listening on. A room server listening on a different port needs that key to match, or the client ends up asking for a port that was never offered.
- The webhook secret and the S2S server secret are two different credentials with two different jobs. The webhook validates that the incoming launch request actually came from brainCloud. The S2S secret authenticates the spun-up game server back to brainCloud afterward. Securing one doesn't cover the other.