The following user tutorial was created by Kahshoo-heem, for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of SDL. This tutorial, though not purposely a part of the SDLTutorials.com or created for the series, may be a branch or addition to the series. Please read notes by the author for any additional code and/or framework used by the author. If you wish to submit your own tutorial to this site, please visit the "User Tutorials" page.
In this tutorial, I will show you a simple c++ library to add network resources to your game.
Firstly, we have to add the SDL_net headers and libraries to your desktop environment. If you are using a linux distro like Ubuntu, it is enough to download the libsdl-netx.x-dev package from the repositories. If not, the process is analogous of including sdl mixer, image, etc, better explained by Tim's Tutorials.
Don't forget, as well, to set the linker options in your IDE, adding the SDL_net library.
So, let's create our two files, CNet.h and CNet.cpp, that will build our framework that we'll help us at making network games. It's necessary to say that this library, for the time being, only works with TCP/IP protocol.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cstring>
#include "SDL_net.h"
typedef char charbuf [256];
class CNet{
public:
static bool Init(); //Initialize SDL_net
static void Quit(); //Exit SDL_net
};
Here is our header file, and, after the includes and the charbuf declaration, our first class. Its aim is initialize and end the network environment, with two static functions. However, they are unnecessary, if we're using SDL_Init(SDL_INIT_EVERYTHING).
class CNetMessage {
protected:
charbuf buffer; //message stored in buffer, limited to 256 bytes
enum bufstates { //message states
EMPTY,
READING,
WRITING,
FULL };
bufstates state; //the message state
void reset(); // reset message: fulfill it with zeroes and change its state to EMPTY
public:
CNetMessage(); //constructor
//Virtual member function that indicates how many bytes may have to be loaded into the instance. By default, it works with strings that have a maximum
//of 255 characters.
virtual int NumToLoad();
//Virtual member function that indicates how many bytes may have to be downloaded from the instance.
virtual int NumToUnLoad();
void LoadBytes(charbuf& inputbuffer, int n); //Load a char set into the message buffer
void UnLoadBytes(charbuf& destbuffer); //Unload a char set from the buffer
void finish(); //set the state object to full. No more data to be loaded
};
The class above provides the needed interface for the application to deal with the packets sent through the network. It's limited to a maximum of 256 bytes.
Regardless this class uses a buffer with a fixed length, the packets sent through the network can be much more smaller, according to the program's needs. In our TIC TAC TOE game, only one byte is sent. To work with this customization, we put some message states, to help the framework user to build a derived class.
The LoadBytes and UnloadBytes member functions load and unload in a buffer the wanted chars. They were determined by the virtual functions NumToLoad and NumToUnload, each of which indicates how many bytes have to be loaded or unloaded. By default, this class is able to work with strings that have a maximum of 255 characters. If you want to work with other data structures, as we'll do at the game making, you may override this member function.
Last, the finish() member function, which sets the class ready to be used. So, we can send through the socket, in the same packet, a message with two different data structures, avoiding unnecessary network pollution. Below, the implementation code of this and the latter classes:
#include "CNet.h"
bool CNet::Init() {
if (SDLNet_Init() < 0) {
std::cerr << "SDLNet_Init: " << SDLNet_GetError() << std::endl;
return false;
}
else
return true;
}
void CNet::Quit() {
SDLNet_Quit();
}
CNetMessage::CNetMessage() {
reset();
}
void CNetMessage::reset() {
for (int i =0; i < 256; i++)
buffer [i] = 0;
state = EMPTY;
}
void CNetMessage::finish() {
if (state == READING)
state = FULL;
}
int CNetMessage::NumToLoad() {
if (state == EMPTY)
return 255;
else
return 0;
}
int CNetMessage::NumToUnLoad() {
if (state == FULL)
return strlen(buffer) + 1;
else
return 0;
}
void CNetMessage::LoadBytes(charbuf& inputbuffer, int n) {
for (int i = 0; i < n; i++)
buffer[i] = inputbuffer[i];
state = READING;
}
void CNetMessage::UnLoadBytes(charbuf& destbuffer) {
for (int i=0; i < this->NumToUnLoad(); i++)
destbuffer[i] = buffer[i];
reset();
}
Now, back to the header file. Let's take a look at the CIpAddress class:
class CIpAddress {
private:
IPaddress m_Ip; //the IPaddress structure
public:
CIpAddress(); //constructor
CIpAddress (Uint16 port); //create and associate a port to the instance
CIpAddress (std::string host, Uint16 port); //create and associate a port and a host to the instance
void SetIp (IPaddress sdl_ip); //set a CIpAddress object from an existing SDL IPaddress
bool Ok() const; //True if the object have a port and a host associated to it
IPaddress GetIpAddress() const; //return a SDL_net IPaddress structure
Uint32 GetHost() const; //return the host
Uint16 GetPort() const; //return the port
};
Basically, that is a C++ interface for the SDL NET structure IPaddress. It has informations about the host IP address, and the port which the program is associated to. Here is the definition of the class members, at CNet.cpp:
CIpAddress::CIpAddress (Uint16 port) {
if (SDLNet_ResolveHost(&m_Ip, NULL, port) < 0){
std::cerr << "SDLNet_ResolveHost: " << SDLNet_GetError() << std::endl;
m_Ip.host = 0;
m_Ip.port = 0;
}
}
CIpAddress::CIpAddress (std::string host, Uint16 port) {
if (SDLNet_ResolveHost(&m_Ip, host.c_str(), port) < 0) {
std::cerr << "SDLNet_ResolveHost: " << SDLNet_GetError() << std::endl;
m_Ip.host = 0;
m_Ip.port = 0;
}
}
CIpAddress::CIpAddress() {
m_Ip.host = 0;
m_Ip.port = 0;
}
bool CIpAddress::Ok() const {
return !(m_Ip.port == 0);
}
void CIpAddress::SetIp (IPaddress sdl_ip) { //sets a CTcpSocket object from a existing SDL socket
m_Ip = sdl_ip;
}
IPaddress CIpAddress::GetIpAddress() const {
return m_Ip;
}
Uint32 CIpAddress::GetHost() const {
return m_Ip.host;
}
Uint16 CIpAddress::GetPort() const {
return m_Ip.port;
}
Again, back to header, let's analyze CTcpSocket, a framework to the SDL NET TCP socket structure. In a few words, a socket is a connection, a "pipe" between two computers, that delivers messages to them, in and out. In a TCP socket, there are necessarily two fixed computers in each pipe's end, and messages always are got in the right order that they have sent.
class CTcpSocket {
protected:
TCPsocket m_Socket; //the TCP socket structure
SDLNet_SocketSet set; //a set of sockets. Used here only to check existing packets
public:
CTcpSocket();
virtual ~CTcpSocket();
virtual void SetSocket (TCPsocket the_sdl_socket); //set a CTcpSocket object from a existing SDL socket
bool Ok() const; //indicate if theres is a TCPsocket associated to the instance
bool Ready() const; //True if there are bytes ready to be read
virtual void OnReady(); //pure virtual
};
In the code above, the member function Ready() plays an important role: it checks whether there is or not activity in the socket, freeing the program's flow to do what they have to do, without waiting messages from the other side of the pipe.
Below, the code in CNet.cpp:
CTcpSocket::CTcpSocket() {
m_Socket = NULL;
set = SDLNet_AllocSocketSet(1);
}
CTcpSocket::~CTcpSocket() {
if (!(m_Socket == NULL)) {
SDLNet_TCP_DelSocket(set,m_Socket);
SDLNet_FreeSocketSet(set);
SDLNet_TCP_Close (m_Socket);
}
}
void CTcpSocket::SetSocket (TCPsocket the_sdl_socket) {
if (!(m_Socket == NULL)) {
SDLNet_TCP_DelSocket(set,m_Socket);
SDLNet_TCP_Close (m_Socket);
}
m_Socket = the_sdl_socket;
if(SDLNet_TCP_AddSocket(set,m_Socket)==-1) {
std::cerr << "SDLNet_TCP_AddSocket: " << SDLNet_GetError() << std::endl;
m_Socket = NULL;
}
}
bool CTcpSocket::Ok() const {
return !(m_Socket == NULL);
}
bool CTcpSocket::Ready() const {
bool rd = false;
int numready = SDLNet_CheckSockets(set, 0);
if (numready == -1)
std::cerr << "SDLNet_CheckSockets: " << SDLNet_GetError() << std:: endl;
else
if (numready)
rd = SDLNet_SocketReady (m_Socket);
return rd;
}
void CTcpSocket::OnReady() {
}
From this class, we will derive CHostSocket, according to the piece of code of CNet.h:
class CClientSocket;
class CHostSocket : public CTcpSocket {
public:
CHostSocket (CIpAddress& the_ip_address); //create and open a new socket, with an existing CIpAddress object
CHostSocket (Uint16 port); //create and open a new socket with the desired port
bool Accept (CClientSocket&); //set a client CTcpSocket object after listening to the port
virtual void OnReady(); //pure virtual
};
A host socket is a socket whose utility is listen to a certain port, to check whether there is someone wanting to connect to the service provided by the program, in the machine it runs. If found, we have to use the member Accept to associate another socket to the client. Here is the implementation:
CHostSocket::CHostSocket (CIpAddress& the_ip_address) {
CTcpSocket::CTcpSocket();
IPaddress iph = the_ip_address.GetIpAddress();
if (!(m_Socket = SDLNet_TCP_Open(&iph))) {
SDLNet_FreeSocketSet(set);
std::cerr << "SDLNet_TCP_Open: " << SDLNet_GetError() << std::endl;
}
}
CHostSocket::CHostSocket (Uint16 port) {
CIpAddress iplistener (port);
if (!iplistener.Ok()) {
m_Socket = NULL;
}
else {
CTcpSocket::CTcpSocket();
IPaddress iph = iplistener.GetIpAddress();
if (!(m_Socket = SDLNet_TCP_Open(&iph))) {
SDLNet_FreeSocketSet(set);
std::cerr << "SDLNet_TCP_Open: " << SDLNet_GetError() << std::endl;
}
}
}
bool CHostSocket::Accept (CClientSocket& the_client_socket) {
TCPsocket cs;
if ((cs = SDLNet_TCP_Accept(m_Socket))) {
the_client_socket.SetSocket(cs);
return true;
}
else
return false;
}
void CHostSocket::OnReady() {
}
So, a tcp socket client is a socket whose aim is to communicate with a remote host, who, in turn, is using a tcp client to send and receive messages too. Below, the header file for this class:
class CClientSocket : public CTcpSocket {
private:
CIpAddress m_RemoteIp; //the CIpAddress object corresponding to the remote host
public:
CClientSocket(); //constructor
CClientSocket (std::string host, Uint16 port); //Create the object and connect to a host, in a given port
bool Connect (CIpAddress& remoteip); //make a connection to communicate with a remote host
bool Connect (CHostSocket& the_listener_socket); //make a connection to communicate with a client
void SetSocket (TCPsocket the_sdl_socket); //set a CTcpSocket object from an existing SDL_net socket
CIpAddress getIpAddress () const; //return a CIpAddress object associated to the remote host
virtual void OnReady(); //pure virtual
bool Receive(CNetMessage& rData); //receive data and load it into a CNetMessage object
bool Send (CNetMessage& sData); //send data from a CNetMessage object
};
The same way CHostSocket is, this class is derived from CTcpSocket, and uses the constructor or Connect() to make a connection to the host. If so, the object uses the Send() and Receive() member functions to throw and get messages through the network. Below, the implementation:
CClientSocket::CClientSocket() {
CTcpSocket::CTcpSocket();
}
CClientSocket::CClientSocket (std::string host, Uint16 port) {
CIpAddress remoteip (host.c_str(), port);
if (!remoteip.Ok()) {
m_Socket = NULL;
}
else {
CTcpSocket::CTcpSocket();
Connect(remoteip);
}
}
bool CClientSocket::Connect (CHostSocket& the_listener_socket) {
return false;
}
bool CClientSocket::Connect (CIpAddress& remoteip) {
TCPsocket cs;
IPaddress ip = remoteip.GetIpAddress();
if ((cs = SDLNet_TCP_Open(&ip)))
{
SetSocket(cs);
return true;
}
else {
std::cerr << "SDLNet_TCP_Open: " << SDLNet_GetError() << std::endl;
return false;
}
}
void CClientSocket::SetSocket (TCPsocket the_sdl_socket) {
CTcpSocket::SetSocket (the_sdl_socket);
IPaddress* ips;
if ((ips = SDLNet_TCP_GetPeerAddress(m_Socket))) {
/* Print the address, converting it onto the host format */
m_RemoteIp.SetIp(*ips);
Uint32 hbo = m_RemoteIp.GetHost();
Uint16 pbo = m_RemoteIp.GetPort();
std::cout << "Client connected: " << SDLNet_Read32(&hbo) << ' '
<< SDLNet_Read16 (&pbo) << std::endl;
}
else
std::cerr << "SDLNet_TCP_GetPeerAddress: " << SDLNet_GetError() << std::endl;
}
CIpAddress CClientSocket::getIpAddress () const {
return m_RemoteIp;
}
bool CClientSocket::Receive(CNetMessage& rData) {
//Firstly, check if there is a socket
if (m_Socket == NULL)
return false;
charbuf buf;
//Check if the instance can receive bytes, if it can, load the number of bytes specified by NumToLoad() virtual function
while (rData.NumToLoad() > 0) {
if (SDLNet_TCP_Recv(m_Socket, buf, rData.NumToLoad()) > 0) {
rData.LoadBytes (buf, rData.NumToLoad());
}
else {
return false;
}
}
rData.finish();
return true;
}
bool CClientSocket::Send (CNetMessage& sData) {
//check if there is a socket
if (m_Socket == NULL)
return false;
charbuf buf;
int len;
//Check if the instance can send bytes, if it can, unload the number of bytes specified by NumToLoad() virtual function
while ((len = sData.NumToUnLoad()) > 0) {
sData.UnLoadBytes (buf);
if (SDLNet_TCP_Send(m_Socket, (void *)buf, len) < len) {
std::cerr << "SDLNet_TCP_Send: " << SDLNet_GetError() << std::endl;
return false;
}
}
return true;
}
void CClientSocket::OnReady() {
}
So that, we have finished the first part of our tutorial. Next, we'll change the TIC TAC TOE source code to add playability over a network.
Email (required, not published):
Website:
It's a nice tutorial, but I have found one detail I just cant seem to figure out.
CHostSocket has the function Accept(CClientSocket&). Above the CHostSocket declaration, you have declared "class CClientSocket". Then below CHostSocket in the header file, you again declare CCLientSocket.
Now, I just cant figure out how this all works together. If I remove the CClientSocket declaration above CHostSocket, the compiler tells me Accept(CCLientSocket&) is a syntax error (even though it's just a referance to a class, that is declared below CChostSocket). Accept() is also using the CClientSocket& in the function, but as far as I can understand, this is just a reference to a completely empty class, so how does this even work?
Are there anybody able to enlighten me as to how this even works, and why?
Email (required, not published):
Website:
Email (required, not published):
Website:
Email (required, not published):
Website:
For those of you who downloaded the files, I need to apologize. An untested change in the final hours when making this tutorial led to a bug at CNet.cpp. Since host sockets don't have a host to communicate to, class CIpAddress, used by CHostSocket, doesn't need, necessarily, a host. So we have to alter the CIpAddress::Ok() function member code from
return !(m_Ip.host == 0 || m_Ip.port == 0)
to
return !(m_Ip.port == 0).
I will resend the source code files to Tim, and ask him to update the tutorial at the line mentioned above.
Email (required, not published):
Website:
Email (required, not published):
Website: