ClanLib Tutorial – Part 3: Network Basics

In this part of the ClanLib Tutorial, we will learn about ClanNetwork. Some Internet protocol basics will be covered, but the main focus will be on a ClanLib API called NetGame, which is an event based networking engine. A simple server and client application will be explained, and we’ll go into details on sending/receiving and dispatching events.

About ClanNetwork

The ClanNetwork library provides various network APIs. At the simplest, it provides some classes for basic TCP and UDP communication. There also exists classes for doing HTTP request/respond, Soap webservices and more, but we’ll not touch on those here.

Then there is the NetGame API, which provides higher level classes for easy server/client connection and communication. This tutorial will for the most part focus on this API.

Internet Protocol Basics

Although you can find much better information elsewhere on how the Internet works and how IP packet routing operates, we will give a small summary here of its operation from the point of an application.

Typically applications communicate over the Internet or on a Local Area Network (LAN) using either the Transmission Control Protocol (TCP) or the User Datagram Protocol (UDP). The TCP protocol is used for connection based communication between two parties (like a phone call), while the UDP protocol is used to send individual messages without an actual connection (like sending a SMS). With both protocols you connect or send messages to a specific IP address. But because there may be many different programs running on a machine, the destination is also identified by a port. The combination of both is called a socket name and is represented by the class CL_SocketName. For example, the Google search engine web server runs on the server http://www.google.com, port 80 (the http protocol port).

The CL_SocketName class is designed so it can either hold a machine DNS name or an IP address of the server. This is because finding the DNS name for an IP address (or vise versa) may take time and therefore is only done when absolutely necessary. However, it is easy enough to convert from one type to the other:

CL_SocketName dnsname("www.google.com", "80");
CL_String ipaddress = dnsname.lookup_ipv4();

CL_SocketName ipname(ipaddress, "80");
CL_String dnsname = ipname.lookup_hostname();

Just remember that these two functions may block for a while when contacting the DNS server, so use them wisely.

About NetGame API

The NetGame API is an event based networking engine. In all its simplicity the engine assist in easily creating server and clients, and routing simple messages from a client to a server and back.

The API largely consists of the following classes:

We will explain all these classes in the following sections.

Creating a server and a client

To create a server, the application must construct an instance of CL_NetGameServer. Then for each port that the server should listen on, it must call CL_NetGameServer::start(port).

CL_NetGameServer server;
server.start("4556");

The class also features three important signals:

  • Client connected
  • Client disconnected
  • Event received

CL_NetGameServer is a CL_KeepAlive based class, which means that during calls to CL_KeepAlive::process() in your main loop, the class will emit those three signals for each client that has connected/disconnected since last call, and for each network event that has arrived. Each client connected is represented through a CL_NetGameConnection object.

It is also possible to call process_events() on the CL_NetGameServer or CL_NetGameClient objects directly to cause the signals to be emitted. Generally, in client applications the signals are processed automatically because the main render loop already calls the CL_KeepAlive::process() function, but for servers that to not use ClanDisplay the process_events() function can be called directly.

The CL_NetGameClient class is similar in design, except that there is only one connection (to the server), and therefore the connect/disconnect signals do not include any CL_NetGameConnection objects.

CL_NetGameClient client;
client.connect("codegrind.net", "4556");

Sending Events

The class CL_NetGameEvent represents a game event message that is either sent or received. Each event consists of two things: an event name and list of event parameters. The name is what identifies what the event is about, while each of the parameters is one of the supported type – int, string, bool, float or custom type.

The constructor for CL_NetGameEvent lets you automatically pass anything from 0 to 5 parameters in a single line. The types of the parameters are automatically deducted from the C++ type you pass for each parameter, if you are using the supported types.

int my_int = 7;
CL_String my_string1 = "test1";
CL_String my_string2 = "test2";
float my_float = 3.14f;

CL_NetGameEvent event1("eventName1", my_int, my_float);
CL_NetGameEvent event2("eventName2", my_float, my_string1);
CL_NetGameEvent event3("eventName3", my_string1, my_string2);
CL_NetGameEvent event4("eventName4");

If you need to pass more than 5 parameters, you will need to use the add_argument function:

CL_NetGameEvent create_event("ObjectCreate");
create_event.add_argument(game_object->get_id());
create_event.add_argument(game_object->get_name());
create_event.add_argument(game_object->get_position().x);
create_event.add_argument(game_object->get_position().y);
create_event.add_argument(game_object->get_visual_identifier());
create_event.add_argument(game_object->get_collision_identifier());

Sending the event is done through the CL_NetGameClient object on the client side, or through the CL_NetGameServer object on the server side if you want to send it to all connected clients, or through a single CL_NetGameConnection object if it should be sent to only one client.

Lets say that the client wants to send a log on message to the server. We want to pass a username and a password as the arguments, so we do like this:

CL_NetGameEvent event("logon", "Mr. Jones", "the secret of all secrets");
client.send_event(event);

Receiving Events

Any incoming events are dispatched through the sig_event_received signal with the event available as a CL_NetGameEvent parameter. To read the event content, you call get_parameter with an zero-based index. The type is deducted automatically as long as you use the supported types. You could do bound checking for parameter count by calling get_argument_count() as well.

void Client::on_event_received(const CL_NetGameEvent &e)
{
	if(e.get_name() == "loadmap")
	{
		CL_String map_name = e.get_argument(0);
		int max_players = e.get_argument(1);
		load_map(map_name, max_players);
	}
}

We’ll look into a more elegant way to handle event routing later, when we talk about event dispatching using the CL_NetGameEventDispatcher class.

Creating a Server

Let us create a working example of a server application from the knowledge we’ve learned so far. It won’t actually do much, except listen to some events and write out some console text.

Note this is implemented as a Console application, we do not use the ClanApplication module here. If you are using Visual Studio, you will need to modify the Project Configuration to reflect this. Under Configuration Properties->Linker->System set the SubSystem to Console (/SUBSYSTEM:CONSOLE).

Create a file called main.cpp with the following content:

#include "server.h"

int main(int argc, char**argv)
{
	try
	{
		CL_SetupCore setupCore;
		CL_SetupNetwork setupNetwork;

		CL_ConsoleLogger logger;

		Server server;
		server.exec();

		return 0;
	}
	catch (CL_Exception &e)
	{
		CL_Console::write_line("Unhandled Exception: %1", e.get_message_and_stack_trace());

		return 1;
	}
}

Create a file called server.h with the following content:

#include <ClanLib/core.h>
#include <ClanLib/network.h>

class Server
{
public:
	Server();

	void exec();

private:
	void on_client_connected(CL_NetGameConnection *connection);
	void on_client_disconnected(CL_NetGameConnection *connection);
	void on_event_received(CL_NetGameConnection *connection, const CL_NetGameEvent &e);

private:
	CL_NetGameServer network_server;
	CL_SlotContainer slots;
};

And finally create a file called server.cpp:

#include "server.h"

Server::Server()
{
	slots.connect(network_server.sig_client_connected(), this, &Server::on_client_connected);
	slots.connect(network_server.sig_client_disconnected(), this, &Server::on_client_disconnected);
	slots.connect(network_server.sig_event_received(), this, &Server::on_event_received);
}

void Server::exec()
{
	network_server.start("4556");

	cl_log_event("system", "SERVER started");

	while (true)
	{
		// Wait till we receive any network events
		CL_Event::wait(network_server.get_event_arrived());

		network_server.process_events();
	}

	network_server.stop();
}

void Server::on_client_connected(CL_NetGameConnection *connection)
{
	cl_log_event("network", "Client connected");
}

void Server::on_client_disconnected(CL_NetGameConnection *connection)
{
	cl_log_event("network", "Client disconnected");
}

void Server::on_event_received(CL_NetGameConnection *connection, const CL_NetGameEvent &e)
{
	cl_log_event("events", "Client sent event: %1", e.to_string());
}

Notice the CL_Event::wait(network_server.get_event_arrived()) line. This will effectively block our main loop until we receive any events from the network. CL_Event (not to be confused with net events) is a handy mechanism for programs to sleep waiting for something external to happen. Our server application will not hog the CPU, it will only do work when a client connects, disconnects or sends an event. We will get back to CL_Event later when we’ll listen to more than one event.

When connecting the server signals we use CL_SlotContainer, which is a convenience class when using signals and slots. Instead of creating a separate CL_Slot for the three events, we just put them all in the container. Notice the syntax is slightly different than when using CL_Slots directly.

We’re using a helper function called cl_log_events to track what is happening. By instantiating the CL_ConsoleLogger object, the cl_log_event calls are routed to the console output. It is handy to use, since it can be routed to file logging as well (or any custom logger).

Download source code and project files for this part

Creating a Client

We follow the same pattern as the server application and create a simple console based client. We start a new project, and create a main.cpp file which contains the same code as the server application, except we create a Client object instead of a Server object.

Create a file called client.h with the following content:

#include <ClanLib/core.h>
#include <ClanLib/network.h>

class Client
{
public:
	Client();

	void exec();

private:
	void connect_to_server();

	void on_connected();
	void on_disconnected();
	void on_event_received(const CL_NetGameEvent &e);

private:
	CL_NetGameClient network_client;
	CL_SlotContainer slots;

	bool quit;
};

And create a file called client.cpp with the following content:

#include "client.h"

Client::Client()
{
	slots.connect(network_client.sig_event_received(), this, &Client::on_event_received);
	slots.connect(network_client.sig_connected(), this, &Client::on_connected);
	slots.connect(network_client.sig_disconnected(), this, &Client::on_disconnected);

	quit = false;
}

void Client::exec()
{
	cl_log_event("system", "CLIENT started");

	connect_to_server();

	while (!quit)
	{
		CL_Event::wait(network_client.get_event_arrived());

		network_client.process_events();
	}
}

void Client::connect_to_server()
{
	try
	{
		network_client.connect("localhost", "4556");
	}
	catch(const CL_Exception &e)
	{
		cl_log_event("error", e.message);
	}
}

void Client::on_connected()
{
	cl_log_event("network", "Connected to server");
}

void Client::on_disconnected()
{
	cl_log_event("network", "Disconnected from server");
	quit = true;
}

void Client::on_event_received(const CL_NetGameEvent &e)
{
	cl_log_event("events", "Server sent event: %1", e.to_string());
}

Not much new to learn here, it is just the same as the server except the actual connecting to the server. If we start up a server and two clients, we should see something like this in console windows:

Download source code and project files for this part

Dispatching events

Next step is to send some events between client and server, and react to them. We will create an over-simplified flow of network events that lets client connect, login and request to start a game. The server will respond with login status, and send map load and game start events.

The following diagram tries to explain the flow of events between server and clients. (It should possibly be a UML sequence diagram or similar, but unfortunately my diagram skills are very limited.)

On the server side we get the sig_event_received signal when we receive an event from a client. One straight forward way would be to write the function like:

void Server::on_event_received(CL_NetGameConnection *connection, const CL_NetGameEvent &e)
{
    if(e.get_name() == "Login")
        on_event_login();
    else if(e.get_name() == "Game-RequestStart")
        on_event_game_requeststart();
    else
        ...
}

But this gets unwieldy fast when the amount of events gets larger. To remedy this, the NetGame API has a class called CL_NetGameEventDispatcher, which helps you route incoming events to separate functions.

The CL_NetGameEventDispatcher is actually a collection of 4 classes – depending on how many extra parameters the responding event function should have. Note the extra parameters does not have anything to do with the actual content of the event itself, it is just some extra objects we want to pass along to the event function. How these extra parameters are passed along are explained in the last section, where we will code the actual event functions. The dispatcher mechanism might seem a little confusing in the start, but through the following examples it should be made clearer. Remember that the usage of the dispatcher is completely optional, it is just a helper class.

In server.h, lets add some dispatchers:

CL_NetGameEventDispatcher_v1<ServerUser*> login_events;
CL_NetGameEventDispatcher_v1<ServerUser*> game_events;

We use the _v1 version of the CL_NetGameEventDispatcher, which lets us have one extra parameter to the event function, in our case a ServerUser pointer. ServerUser is a class we’ll create in a moment.

To make the syntax more clear, here are some examples of how the other versions of CL_NetGameEventDispatcher work:

CL_NetGameEventDispatcher_v0 server_events;
CL_NetGameEventDispatcher_v2<MyClass *, int andYetAnotherParameter> some_other_events;

In our Server constructor, lets configure the dispatcher to route incoming events to functions. Notice the syntax looks very familiar to how to connect the ClanLib signals and slots together!

login_events.func_event("Login").set(this, &Server::on_event_login);
game_events.func_event("Game-RequestStart").set(this, &Server::on_event_game_requeststart);

If you had more events defined, you could do similarly:

game_events.func_event("SomeRequest1").set(this, &Server::on_event_request1);
game_events.func_event("SomeRequest2").set(this, &Server::on_event_request2);
game_events.func_event("SomeRequest3").set(this, &Server::on_event_request3);

Next step is to actually route the incoming events to the functions. Basically, this is achieved by calling dispatch() on the dispatcher object:

void Server::on_event_received(CL_NetGameConnection *connection, const CL_NetGameEvent &e)
{
        bool handled_event = false;

        handled_event |= login_events.dispatch(e);
        handled_event |= game_events.dispatch(e);

        if (!handled_event)
            cl_log_event("events", "Unhandled event: %1", e.to_string());
}

The dispatch function return true if the event was actually dispatched, that is, it found a matching function for the event name. By handling this return value, we can write out a warning message to the log if an event was not processed. This can be a sign that we forgot to hook up an event or that a client is trying to do something it should not do.

Note, the code above won’t compile, since our dispatcher actually expects one extra parameter – a ServerUser object. So lets make the server a bit more interesting by actually having some server side users with a state and some event flow:

#pragma once

#include <ClanLib/network.h>

class ServerUser
{
public:
	ServerUser(CL_NetGameConnection *connection) : connection(connection), id(0)
	{
	}

	void send_event(const CL_NetGameEvent &gameEvent)
	{
		connection->send_event(gameEvent);
	}

public:
	int id;
	CL_String user_name;

private:
	CL_NetGameConnection *connection;
};

This class holds the state of a connected user, basically his user name and login id for now. The server will keep around a list of every connected client using a C++ map:

std::map<CL_NetGameConnection *, ServerUser *> users;

When a client connects, we will create an instance of this class and put it into the map:

void Server::on_client_connected(CL_NetGameConnection *connection)
{
    cl_log_event("network", "Client connected");

    ServerUser *user = new ServerUser(connection);
    users[connection] = user;
}

And when the client disconnects, we delete the object and remove it from the map:

void Server::on_client_disconnected(CL_NetGameConnection *connection)
{
	cl_log_event("network", "Client disconnected");

	// Delete user
	std::map<CL_NetGameConnection *, ServerUser *>::iterator it = users.find(connection);
	if(it != users.end())
	{
		ServerUser *user = it->second;
		delete user;
		users.erase(it);
	}
}

And create a helper function to retrieve a user given a connection object:

ServerUser *Server::find_user(CL_NetGameConnection *connection)
{
	std::map<CL_NetGameConnection *, ServerUser *>::iterator it = users.find(connection);
	if(it != users.end())
		return it->second;
	return 0;
}

Lets rewrite the event received function to use the dispatchers and the server user objects. Lets also code the actual event functions:

void Server::on_event_received(CL_NetGameConnection *connection, const CL_NetGameEvent &e)
{
	cl_log_event("events", "Client sent event: %1", e.to_string());

	ServerUser *user = find_user(connection);
	if(user)
	{
		bool handled_event = false;

		if (user->id == 0)	// User has not logged in, so route events to login dispatcher
			handled_event = login_events.dispatch(e, user);
		else
			handled_event = game_events.dispatch(e, user);

		if (!handled_event)
			cl_log_event("events", "Unhandled event: %1", e.to_string());
	}
}

void Server::on_event_login(const CL_NetGameEvent &e, ServerUser *user)
{
	cl_log_event("events", "Client requested login");

	CL_String user_name = e.get_argument(0);

	if(user_name.length() == 0)
	{
		// Send login failed event to specific user
		user->send_event(CL_NetGameEvent("Login-Fail", "Missing user name"));
	}
	else
	{
		// Assign name and id to User object (created when user connected earlier)
		user->user_name = user_name;
		user->id = next_user_id++;

		// Send login success event to specific user
		user->send_event(CL_NetGameEvent("Login-Success"));
	}
}

void Server::on_event_game_requeststart(const CL_NetGameEvent &e, ServerUser *user)
{
	cl_log_event("events", "Client requested game start");

	if(game_running == false)
	{
		game_running = true;

		// Send some dummy data
		CL_String map_name = "Map1";
		int max_players = 6;
		float position_x = 143.3f;
		float position_y = 22.2f;

		CL_NetGameEvent load_map_event("Game-LoadMap");
		load_map_event.add_argument(map_name);
		load_map_event.add_argument(max_players);
		load_map_event.add_argument(position_x);
		load_map_event.add_argument(position_y);

		// Send events to all connected clients
		network_server.send_event(load_map_event);

		network_server.send_event(CL_NetGameEvent("Game-Start"));
	}
}

When receiving an event, we first lookup the ServerUser object for a given connection. Users with no id value assigned are not logged in, so we route the events to the login dispatcher for those. If the user is logged in, we route the events to the game dispatcher. Notice we pass along the ServerUser object to the dispatch function – that is our extra parameter.

All event function have the same basic parameter requirement – the first parameter should be a “const CL_NetGameEvent &”. If we are passing along any extra parameters, they come as subsequent parameters:

void Server::on_event_login(const CL_NetGameEvent &e, ServerUser *user)

Notice the first line in our on_event_received function. CL_NetGameEvent has a to_string() function that is handy for debugging. It returns a human readable version of the event name and all parameters. Example for the Game-LoadMap event received from a client will output “Game-LoadMap(“Map1″,6,143.300003,22.200001)”

cl_log_event("events", "Client sent event: %1", e.to_string());

Next step is to update the client to send, receive and dispatch the events as well:

#include "client.h"

Client::Client()
{
	// Connect essential signals - connecting, disconnecting and receiving events
	slots.connect(network_client.sig_event_received(), this, &Client::on_event_received);
	slots.connect(network_client.sig_connected(), this, &Client::on_connected);
	slots.connect(network_client.sig_disconnected(), this, &Client::on_disconnected);

	// Set up event dispatchers to route incoming events to functions
	login_events.func_event("Login-Success").set(this, &Client::on_event_login_success);
	login_events.func_event("Login-Fail").set(this, &Client::on_event_login_fail);
	game_events.func_event("Game-LoadMap").set(this, &Client::on_event_game_loadmap);
	game_events.func_event("Game-Start").set(this, &Client::on_event_game_startgame);

	quit = false;
	logged_in = false;
}

void Client::on_connected()
{
	cl_log_event("network", "Connected to server");

	// For demonstration purposes, lets fail a login
	// We will receive an error event for this, as we don't send a proper user name
	network_client.send_event(CL_NetGameEvent("Login", ""));

	// Properly login
	network_client.send_event(CL_NetGameEvent("Login", "my user name"));
}

void Client::on_event_received(const CL_NetGameEvent &e)
{
	cl_log_event("events", "Server sent event: %1", e.to_string());

	bool handled_event = false;

	if(!logged_in)
	{
		// We haven't logged in, so route events to login dispatcher
		handled_event = login_events.dispatch(e);
	}
	else
	{
		// We have logged in, so route events to game dispatcher
		handled_event = game_events.dispatch(e);
	}

	// We received an event which we didn't hook up
	if(!handled_event)
	{
		cl_log_event("events", "Unhandled event: %1", e.to_string());
	}
}

void Client::on_event_login_success(const CL_NetGameEvent &e)
{
	cl_log_event("events", "Login success");

	logged_in = true;

	network_client.send_event(CL_NetGameEvent("Game-RequestStart"));
}

void Client::on_event_login_fail(const CL_NetGameEvent &e)
{
	CL_String fail_reason = e.get_argument(0);

	cl_log_event("events", "Login failed: %1", fail_reason);
}

void Client::on_event_game_loadmap(const CL_NetGameEvent &e)
{
	CL_String map_name = e.get_argument(0);
	int max_players = e.get_argument(1);
	float position_x = e.get_argument(2);
	float position_y = e.get_argument(3);

	cl_log_event("events", "Loading map: %1 with %2 players, Position %3,%4", map_name, max_players, position_x, position_y);
}

void Client::on_event_game_startgame(const CL_NetGameEvent &e)
{
	cl_log_event("events", "Starting game!");
}

Starting up a server and a client should give the following output:

Download source code and project files for this part

This concludes the basic network part. Hopefully you’ve understood most of it. If not, don’t hesitate to leave a comment!

In the next part, we will convert the server application to be a threaded service. Read Part 4!

About these ads
  1. #1 by Marocs on January 11, 2011 - 15:31

    spectacular *-*

  2. #2 by Marocs on January 11, 2011 - 15:40

    The ClanNetwork support a MMORPG? Some 1000 players

  3. #3 by halley on June 27, 2011 - 12:07

    excellent lesson!

  1. ClanLib教程翻译:ClanLib教程 – 第2部分:图形,ClanLib Tutorial – Part 2: Visuals | Slecktris,颈椎方块
  2. 转载:BreakingChanges | Slecktris,颈椎方块

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: