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
