<< Voltar Índice Próxima >>

SERVIDOR

O servidor que será mostrado no tutorial é bem simples. Ele é executado em linha de comando, sendo extremamente leve. E basicamente irá distribuir os pacotes de dados de um jogador para os outros conectados a ele. O código completo juntamente com o projeto pode ser baixado aqui

#include "IRAserver.h"

int main(void)
{
	IRAserver *IRAs;

	IRAs = new IRAserver();

	return 0;
}

 

O arquivo "main.cpp" contém apenas as instruções para iniciar a execução do servidor, criando um objeto do servidor.

#include "MessageIdentifiers.h"
#include "RakNetworkFactory.h"
#include "RakPeerInterface.h"
#include "RakNetStatistics.h"
#include "RakNetTypes.h"
#include "BitStream.h"
#include <assert.h>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#include <conio.h>
#include <windows.h> // Sleep
#include "StringCompressor.h"

 

Trabalhando agora com o arquivo "IRAserver.h", estes são os includes da RakNet necessários para o servidor.

#include "stdafx.h"
#include "Thread.h"

class IRAserver : public CThread
{

 

Apesar da RakNet utilizar threads internamente, utilizaremos uma classe externa que implementa threads em C/C++. Principalmente para mostrar a idéia do recebimento dos pacotes. Por isso esses includes são adicionados e a classe "IRAserver" herda dados da classe "CThread" que implementa uma thread.

    server = RakNetworkFactory::GetRakPeerInterface();
    server->InitializeSecurity(0,0,0,0);
    int i = server->GetNumberOfAddresses();
    server->SetIncomingPassword("Irado", (int)strlen("Irado"));

    clientID = UNASSIGNED_SYSTEM_ADDRESS;

 

Indo para o arquivo "IRAserver.cpp" no método construtor da clase. Criamos um server na primeira linha do código acima com a função "GetRakPeerInterface()". Pode-se utilizar chaves públicas para proteger a conexão com o servidor utilizando a função "InitializeSecurity()", mas isso não será abordado neste tutorial.

Obtém-se o endereço IP da máquina com "GetNumberOfAddresses()" e depois usando "SetIncomingPassword()" define-se uma senha que o cliente deve conhecer para se conectar.

    SocketDescriptor socketDescriptor(atoi("1000"),0);
    bool b = server->Startup(32, 30, &socketDescriptor, 1 );
    server->SetMaximumIncomingConnections(5);

 

Cria-se um descritor de socket passando como parâmetro para a função "socketDescriptor()" o número da porta que o servidor ficará esperando conexões. O segundo parâmetro é o endereço que ele terá, como será na própria máquina ele terá o localhost(127.0.0.1) informado colocando o número 0(zero).

Para iniciar a execução o método "Startup()" é chamado, e o resultado é passado para uma variávelo do tipo booleana para uma posterior verificação. O primeiro parâmetro é o número máximo de conexões suportadas pelo servidor, o segundo é o tempo em milisegundos que a thread que cuida das conexões ficará dormindo(sleep). O terceiro é a descrição do socket, que no tutorial contém o número da porta do servidor e o quarto é o número de descritores de socket usados.

A função "SetMaximumIncomingConnections()" especifica a quantidade máxima de conexões que o servidor pode receber ao mesmo tempo.

    if (b)
    {
    	puts("IRAserver iniciado, esperando conexoes \n");
    	this->Start();
    }
    else
    {
    	puts("Erro ao iniciar.");
    	exit(1);
    }

 

Acima é a verificação do início do servidor. Caso não tenha problemas mostra a mensagem e inicia a thread que espera novas conexões(da classe CThread). Se algum erro ocorrer e o servidor não for inicializado sai do programa.

while(true)
{
    gets(comando);

    if (strcmp(comando, "quit")==0)
    {
        puts("Finalizando.");
    	break;
    }

    if (strcmp(comando, "stat")==0)
    {
        rss=server->GetStatistics(server->GetSystemAddressFromIndex(0));
        StatisticsToString(rss, message, 2);
        printf("%s", message);
        printf("Ping %i\n", server->GetAveragePing(server->GetSystemAddressFromIndex(0)));

        continue;
    }

    if (strcmp(comando, "ping")==0)
    {
        server->Ping(clientID);

        continue;
    }

    if (strcmp(comando, "kick")==0)
    {
        server->CloseConnection(clientID, true, 0);

        continue;
    }

    if (strcmp(comando, "ban")==0)
    {
        printf("Enter IP to ban.  You can use * as a wildcard\n");
        gets(message);
        server->AddToBanList(message);
        printf("IP %s added to ban list.\n", message);

        continue;
    }

 

O loop acima recebe os comandos do console, executa-os e dependendo do comando, coloca o resultado no array de char(String) de nome "message", para posteriormente ser enviado ao(s) cliente(s). O trecho acima mostra algumas das ações que podem ser tomadas pelo servido em relação a um jogador, como por exemplo, testar o ping ou tirá-lo do servidor(kick).

    char message2[420];

    message2[0]=0;
    strcpy(message2, "Server: ");
    strcat(message2, message);

    server->Send(message2, (const int) strlen(message2)+1, HIGH_PRIORITY,
        RELIABLE_ORDERED, 0, UNASSIGNED_SYSTEM_ADDRESS, true);
}

 

O trecho acima encontra-se no final do loop while. Criamos uma váriavel "message2" para conter todo o conteúdo da String a ser enviada. No inicio ela terá "Server: " e depois o texto criado a partir de um dos comandos. Isso é feito utilizando a função "strcpy()" para copiar uma string para a variável e "strcat()" para concatenar a string do segundo parâmetro a do primeiro.

O pacote contendo a string é enviado utilizando a função "Send()". O primeiro parâmetro são os dados a serem enviados, o segundo o seu comprimento, o terceiro é a prioridade que podem ser:

SYSTEM_PRIORITY - Muito alta prioridade;
HIGH_PRIORITY - Alta;
MEDIUM_PRIORITY - Média;
LOW_PRIORITY - Baixa;

O quarto parâmetro é a forma como os pacotes serão entregues, as principais são:

UNRELIABLE - igual ao protocolo UDP, sem ordenação e garantia de entrega.
UNRELIABLE_SEQUENCED - semelhante ao UDP, mas mensagens fora de ordem serão descartadas.
RELIABLE - mensagens com garantia de entrega, mas sem ordenação
RELIABLE_ORDERED - garantia de entrega e em ordem. Haverá uma parada no recebimento enquanto a próxima mensagem da ordem não chegar.
RELIABLE_SEQUENCED - garantia de entrega e recebimento na sequência em que foram enviadas

O quinto é o o canal de ordenação. O sexto é o endereço IP que receberá o pacote e em caso de broadcast(envio do pacote para todos conectados no servidor) esse parâmetro especificará qual endereço não receberá a mensagem. Caso nenhum endereço IP deva ser especificado nesse parâmetro deve-se colocar UNASSIGNED_SYSTEM_ADDRESS. O sétimo parâmetro define se o envio será por broadcast ou não.

DWORD IRAserver::Run( LPVOID )
{
	while (true)
	{
		//Sleep necessario para evitar q o programa utilize 100% da cpu continuamente
		#ifdef _WIN32
			Sleep(30);
		#else
			usleep(30 * 1000);
		#endif

		p = server->Receive();
	    if (p!=0) { identificaPacote(); }
	}

	return true;
}

 

O método acima é executando enquanto a thread estiver ativa, ele é chamado quando a thread começou a ser executada(this->Start()). Basicamente ele fica em um loop infinito com uma pequena parada de 30 milisegundo, para que ela não ocupe todo o processamento da CPU e não permita que outro processo(programa) execute.

O pacote é recebido com a função "Receive()" e passado para a váriavel "p" do tipo Packet. Então é feita a verificação se o pacote contém algum dado, se possuir será chamado o método "identificaPacotes()".

void IRAserver::identificaPacote()
{
    packetIdentifier = GetPacketIdentifier(p);

    // Check if this is a network message packet
    switch (packetIdentifier)
    {
    	case ID_DISCONNECTION_NOTIFICATION:
    		  // Connection lost normally
    		printf("ID_DISCONNECTION_NOTIFICATION \n");
    		break;

    	case ID_NEW_INCOMING_CONNECTION:
    		 // Somebody connected.  We have their IP now
    		printf("ID_NEW_INCOMING_CONNECTION\n");
    		clientID=p->systemAddress; // Record the player ID of the client
    		break;

    	case ID_MODIFIED_PACKET:
    		// Cheater!
    		printf("ID_MODIFIED_PACKET\n");
    		break;

    	case ID_CONNECTION_LOST:
    		printf("Conexao Perdida \n");
    		break;

 

O método "identificaPacotes()" executará alguma ação dependendo do tipo do pacote recebido. E o método "GetPacketIdentifier()" irá obter o valor que identifica o pacote. A RakNet possui alguns tipos padrões, por exemplo ID_DISCONNECTION_NOTIFICATION chamado quando um jogador desconecta do server.

default:

        codigo = atoi(strtok((char*)p->data,"&"));

        switch(codigo)
        {
        case 1:
            strcpy(message,"2&Voce digitou isso? ");
            strcat(message,strtok(NULL,"&"));
            printf(message);
            server->Send(message, (const int) strlen(message)+1, HIGH_PRIORITY,
                RELIABLE_ORDERED, 0, UNASSIGNED_SYSTEM_ADDRESS, true);
            break;

        }
}

 

Caso o pacote recebido não se encaixe em nenhuma das opções acima no método então ele irá para a opção "default", que esta no final do método "identificaPacotes()", onde será feita a verificação criada para o jogo especificamente.

Os pacotes de dados usados no exemplo são strings com o formato:

código_do_pacote & dados_enviados & dados_enviados ...

No inicio da string terá um número que identificará o tipo dos dados transmitidos e depois terá várias informações separadas por "&"(i comercial). Eles estão organizados dessa forma para o uso da função "strtok()". Ela trabalha uma string separando-as por tokens, delimitados por um caractere, no caso "&".

Um exemplo de dados transmitidos é:

"5&Irado&100,0,150"

A string acima poderia ser o envio da posição do jogador(100,0,150) de nome Irado, onde o pacote da localização poderia ser identificado com o número 5.

Usando a função "strtok()" o primeiro parâmetro é a string em si e o segundo é o delimitador quer irá separar os tokens. Depois de pegar o primeiro caractere que identifica o tipo de dado enviado o switch verifica a ação a ser feita de acordo com o código.

No exemplo obteremos uma string enviada pelo jogador e enviaremos de volta em forma de pergunta, "Você digitou isso?" + "conteudo_enviado_pelo_jogador". Para isso observe que a primeira vez que a função "strtok()" é utilizada ela retorna o primeiro token, e na próxima vez que for executada

(strtok(NULL,"&"));)

não é necessário passar novamente a string(coloca-se NULL), apenas o caractere delimitador. Sendo que este pode ser mudado a cada token. Depois disso a função "Send()" é chamada para o envio dos dados.

Italo Mendes
italo.ribeiro@gmail.com

<< Voltar Índice Próxima >>