Monen yrityksen ja erehdyksen jälkeen sain valmiiksi UDP-protocollaa käyttävän nettisysteemin. Olen pyrkinyt tekemään siitä mahdollisimman yksinkertaisen ja helppokäyttöisen, mutta silti monipuolisen. Systeemi huolehtii itse hävinneiden pakettien uudelleen lähetyksen ja käsittelee paketit siinä järjestyksessä, kuin ne lähetetäänkin. Luokka voi toimia isäntänä, käyttäjänä tai vaikka molempina yhtäaikaa.
Käyttö on yksinkertaista. Ensiksi on alustettava SDL_net ja tämän jälkeen kutsutaan luokan alustavaa Initialize-funktiota. Jos suurimman sallitun käyttäjämäärän jättää nollaksi, systeemi toimii pelkkänä käyttäjänä.
Alustamisen jälkeen voidaan yhdistää isäntään Connect-funktiolla ja yhteyden katkaisu tapahtuu Disconnect-funktiolla.
Viestien lähetys ja vastaanottaminen tapahtuvat SendData- ja RecvData-funktioilla. RecvData-funktio hoitaa lisäksi viestien uudelleenlähetyksen sekä serveripuolen käyttäjien yhteyspyyntöjen käsittelyn.
Luokan muisti vapautuu Release-funktiolla, joka suoritetaan myös, kun luokka tuhotaan.
Lisäksi luokassa on sisäänrakennettu mahdollisuus aikakatkaisulle.
Käyttäjien potkiminen käy Kick-funktiolla ja Reserved-funktio kertoo, onko kyseinen id varattu.
Jos löydätte luokasta virheitä, pyydän ilmoittamaan niistä alla kommenteissa tai sitten sähköpostilla.
Muokkaushistoria
28.6.2007 — Korjattu virhe pakettien uudelleenlähetyksessä.
29.12.2007 — Korjattu virhe luokan alustuksessa(kts. kommentit).
4.1.2008 — Testasin järjestelmän todellisen paineen alla ja löysin sieltä särön(pakettien uudelleen lähetyksestä), jonka korjasin. Lisäksi parantelin virheenkäsittelyä ja poistin vastuuta käyttäjän harteilta lisäilemällä jokusen tarkistuksen. Nyt järjestelmä toimii moitteettomasti myös äärimmäisen raskaassa käytössä. Päivitin myös esimerkit!
Lisäksi mainittakoon, että nykyään saman ip:n takaa voi liittyä monta käyttäjää, kunhan ne majailevat eri porttien takana(vrt. monta konetta saman ip:n takana).
9.9.2008 — Pientä viilailua tehty viimeisimmän projektin ohessa. Korjattu myös puhdas virhe serverin kick-komennon käsittelyssä käyttäjän päässä.
UDP.h
#ifndef UDP_H
#define UDP_H
#include <SDL/SDL_net.h>
const int HOST = 0; // Isännän indexi
const int NOTIMEOUT = 0; // Ei aikakatkaisua
class UDP {
private:
UDPsocket socket; // Socketti
UDPpacket *packet; // Käsittelypaketti
UDPpacket ***sbuffer; // Lähtevien pakettien puskuri
UDPpacket ***rbuffer; // Saapuvien pakettien puskuri
int *sbindex; // Lähtevän puskurin osoitin
int *rbindex; // Saapuvan puskurin osoitin
IPaddress *user; // Käyttäjien osoitteet
Uint32 *time; // Viimeisimpien viestien saapumisajankohdat
int maxusers; // Suurin mahdollinen käyttäjämäärä
int maxpacketsize; // Suurin mahdollinen paketin koko
int buffersize; // Pakettipuskurien koko
Uint32 timeout; // Aikakatkaisu
public:
UDP(); // Constructor
~UDP(); // Destructor
int Initialize(int musers, int mpsize, int bsize, Uint16 port); // Alustaa luokan käyttökuntoon
void Release(); // Vapauttaa luokan
int Connect(const char *ip, Uint16 port); // Yhdistää isäntään
int KeepGoing(); // Kertoo serverille olemassaolostaan
void Disconnect(); // Katkaisee yhteyden isäntään
int SendData(void *pdata, int lenght, int id); // Lähettää paketin
int RecvData(void *pdata, int *id); // Vastaanottaa paketin
void Kick(int id); // Katkaisee yhteyden käyttäjään
void SetTimeout(int second); // Asettaa aikakatkaisun
bool Reserved(int id); // Onko id varattu
};
#endifUDP.cpp
#include <string.h>
#include "net.h"
// Viestityypit
const int UDP_MESSAGE = 0; // Normaali viesti(ensimmäistä kertaa)
const int UDP_REMESSAGE = 1; // Uudelleen lähetetty viesti
const int UDP_RESEND = 2; // "Pyydän lähettämään paketin # uudelleen"
const int UDP_CT = 3; // Yhteyskokeilu
const int UDP_CTA = 4; // Yhteyskokeilun vastaus
const int UDP_DISCONNECT = 5; // Yhteyden katkaiseminen
const int UDP_KG = 6; // "Keep going, I am here!" - Olen linjoilla
UDP::UDP() {
// Aikakatkaisu oletuksena pois päältä
timeout = NOTIMEOUT;
// Merkitään luokka alustamattomaksi
user = NULL;
}
UDP::~UDP() {
// Vapautetaan varattu muisti
if(user)
Release();
}
/* ------------------------------------------------------------ *\
Alustaa luokan toimintakelposeksi
parametrit:
musers: suurin sallittu käyttäjämäärä
mpsize: paketin maksimikoko
bsize: puskurin koko
port: käytettävä portti
palautusarvot:
0: Alustaminen onnistui
-1: Alustaminen epäonnistui
\* ------------------------------------------------------------ */
int UDP::Initialize(int musers, int mpsize, int bsize, Uint16 port) {
// Jos luokka on alustettu
if(user)
Release();
// Asetetaan asetukset
maxusers = musers+1;
maxpacketsize = mpsize;
buffersize = bsize;
// Koska paketin indexille on varattu vain kaksi tavua
if(buffersize > 65536)
buffersize = 65536;
// Varataan tilaa käsittelypaketille
packet = SDLNet_AllocPacket(maxpacketsize);
// Varataan tilaa puskuritaulukolle
sbindex = new int[maxusers];
rbindex = new int[maxusers];
sbuffer = new UDPpacket**[maxusers];
rbuffer = new UDPpacket**[maxusers];
for(int i = 0; i < maxusers; i++) {
sbuffer[i] = NULL;
rbuffer[i] = NULL;
}
// Alustetaan ajastimet
time = new Uint32[maxusers];
// Alustetaan osotteet
user = new IPaddress[maxusers];
for(int i = 0; i < maxusers; i++)
user[i].host = 0;
// Avataan socketti tietojen välittämistä varten
socket = SDLNet_UDP_Open(port);
if(!socket)
return -1;
return 0;
}
/* ------------------------------------------------------------ *\
Vapauttaa luokan varaaman muistin
\* ------------------------------------------------------------ */
void UDP::Release() {
// Jos luokkaa ei ole alustettu
if(!user)
return;
// Vapautetaan käsittelypaketti
SDLNet_FreePacket(packet);
packet = NULL;
// Vapautetaan puskuritaulukot
for(int i = 0; i < maxusers; i++) {
if(sbuffer[i]) {
for(int j = 0; j < buffersize; j++) {
SDLNet_FreePacket(sbuffer[i][j]);
SDLNet_FreePacket(rbuffer[i][j]);
}
}
delete [] sbuffer[i];
delete [] rbuffer[i];
}
delete [] sbuffer;
delete [] rbuffer;
sbuffer = NULL;
rbuffer = NULL;
// Vapautetaan puskuritaulukoiden "osoittimet"
delete [] sbindex;
delete [] rbindex;
sbindex = NULL;
rbindex = NULL;
// Vapautetaan käyttäjätaulu
delete [] user;
user = NULL;
// Suljetaan socketti
SDLNet_UDP_Close(socket);
socket = NULL;
}
/* ------------------------------------------------------------ *\
Yhdistää isäntään
parametrit:
ip: ip-osoite
port: portti
palautusarvot:
0: Yhdistäminen onnistui
-1: Isäntä ei vastaa
-2: Virheellinen osoite tai alustamaton luokka
\* ------------------------------------------------------------ */
int UDP::Connect(const char *ip, Uint16 port) {
Uint32 timer;
int result;
// Jos luokkaa ei ole alustettu
if(!user)
return -2;
// Suljetaan mahdollinen voimassa oleva yhteys
if(sbuffer[HOST])
Disconnect();
// Määritetään isännän osoite
if(SDLNet_ResolveHost(&user[HOST], ip, port))
return -2;
// Varataan muistia puskureille
sbuffer[HOST] = new UDPpacket*[buffersize];
rbuffer[HOST] = new UDPpacket*[buffersize];
for(int i = 0; i < buffersize; i++) {
sbuffer[HOST][i] = SDLNet_AllocPacket(maxpacketsize);
rbuffer[HOST][i] = NULL;
}
// Nollataan puskurien osoittimet
sbindex[HOST] = 0;
rbindex[HOST] = 0;
// Luodaan yhteyskokeilusanoma
packet->data[0] = UDP_CT;
packet->len = 1;
// Nollataan socketin suuntaus
SDLNet_UDP_Unbind(socket, 0);
// Suunnataan socketti isäntään
if(SDLNet_UDP_Bind(socket, 0, &user[HOST]))
return -2;
// Lähetetään sanoma
if(!SDLNet_UDP_Send(socket, 0, packet))
return -2;
// Odotetaan vastausta(3s)
timer = SDL_GetTicks();
while(timer+3000 > SDL_GetTicks()) {
// Vastaanotetaan paketti
result = SDLNet_UDP_Recv(socket, packet);
// Jos saatiin paketti
if(result > 0)
// Varmistetaan paketin osoite
if(packet->address.host == user[HOST].host)
// Varmistetaan, sisältö
if(packet->data[0] == UDP_CTA)
return 0;
// Jos tapahtui virhe
if(result < 0)
return -2;
}
// Tuhotaan isännän osote
user[HOST].host = 0;
// Vapautetaan varattu muisti
delete [] sbuffer[HOST];
delete [] rbuffer[HOST];
sbuffer[HOST] = NULL;
rbuffer[HOST] = NULL;
return -1;
}
/* ------------------------------------------------------------ *\
Lähettää tiedon serverille olemassa olostaan
palautusarvot:
0: Lähetys onnistui
-1: Viestin lähettäminen epäonnistui
-2: Virheellinen osoite tai alustamaton luokka
\* ------------------------------------------------------------ */
int UDP::KeepGoing() {
Uint16& packetid = (Uint16&)packet->data[1]; // Viittaus paketin indexiin
// Jos luokkaa ei ole alustettu
if(!user)
return -2;
// Kootaan paketti
packet->data[0] = UDP_KG;
packetid = sbindex[HOST];
packet->len = 3;
// Nollataan socketin suuntaus
SDLNet_UDP_Unbind(socket, 0);
// Suunnataan socketti isäntään
if(SDLNet_UDP_Bind(socket, 0, &user[HOST]))
return -2;
// Lähetetään sanoma
if(!SDLNet_UDP_Send(socket, 0, packet))
return -1;
return 0;
}
/* ------------------------------------------------------------ *\
Katkaisee yhteyden isäntään
\* ------------------------------------------------------------ */
void UDP::Disconnect() {
// Jos luokkaa ei ole alustettu
if(!user)
return;
// Jos isäntään ei ole yhteyttä
if(!user[HOST].host)
return;
// Lähetetään ilmoitus yhteydenkatkaisusta
packet->data[0] = UDP_DISCONNECT;
packet->len = 1;
SDLNet_UDP_Unbind(socket, 0);
SDLNet_UDP_Bind(socket, 0, &user[HOST]);
SDLNet_UDP_Send(socket, 0, packet);
// Tuhotaan isännän osoite
user[HOST].host = 0;
// Vapautetaan varattu muisti
for(int i = 0; i < buffersize; i++) {
SDLNet_FreePacket(sbuffer[HOST][i]);
if(rbuffer[HOST][i])
SDLNet_FreePacket(rbuffer[HOST][i]);
}
delete [] sbuffer[HOST];
delete [] rbuffer[HOST];
sbuffer[HOST] = NULL;
rbuffer[HOST] = NULL;
}
/* ------------------------------------------------------------ *\
Lähettää paketin vastaanottajalle
parametrit:
pdata: lähetettävä viesti
lenght: viestin pituus
id: käyttäjän id
palautusarvot:
0: Paketti lähetys onnistui
-1: Paketin lähetys epäonnistui
-2: Luokkaa ei ole alustettu
\* ------------------------------------------------------------ */
int UDP::SendData(void *pdata, int lenght, int id) {
Uint8 *data = (Uint8*)pdata;
Uint16& packetid = (Uint16&)packet->data[1]; // Viittaus paketin indexiin
// Jos luokkaa ei ole alustettu
if(!user)
return -2;
// Jos id on laiton
if(id < 0 || id >= maxusers)
return -1;
// Jos id:llä ei ole käyttäjää, poistutaan
if(!user[id].host)
return -1;
// Jos viestin pituus on suurempi, kuin sen suurin sallittu pituus
if(lenght > maxpacketsize)
return -1;
// Luodaan lähetettävä paketti
packet->data[0] = UDP_MESSAGE;
packetid = sbindex[id];
memcpy(&packet->data[3], data, lenght);
packet->len = lenght+3;
// Lisätään viesti puskuriin
memcpy(sbuffer[id][sbindex[id]]->data, data, lenght);
sbuffer[id][sbindex[id]]->len = lenght;
sbindex[id]++;
if(sbindex[id] >= buffersize)
sbindex[id] = 0;
// Nollataan socketin suuntaus
SDLNet_UDP_Unbind(socket, 0);
// Suunnataan socketti
if(SDLNet_UDP_Bind(socket, 0, &user[id]))
return -2;
// Lähetetään paketti
if(!SDLNet_UDP_Send(socket, 0, packet))
return -2;
return 0;
}
/* ------------------------------------------------------------ *\
Käsittelee vastaanotetut paketit: vastaanottaa viestit,
käsittelee yhteyspyynnöt, hoitaa viestien uudelleenlähetykset ja
yhteyksie aikakatkaisut sekä yhteyksien sulkemiset
parametrit:
pdata: osoite viestimuuttujaan
id: osoite käyttäjän id-muuttujaan
palautusarvot:
Palauttaa paketin pituuden
0: Ei pakettia
-1: Yhteys katkaistu
-2: Virheellinen osoite tai alustamaton luokka
\* ------------------------------------------------------------ */
int UDP::RecvData(void *pdata, int *id) {
int index = -1;
int lenght;
int userid = -1;
static int lasthand = 0;
Uint8 *data = (Uint8*)pdata;
Uint16& packetid = (Uint16&)packet->data[1]; // Viittaus paketin indexiin
// Jos luokkaa ei ole alustettu
if(!user)
return -2;
// Kun jokainen käyttäjä on käyty läpi
if(lasthand >= maxusers)
lasthand = 0;
// Luetaan puskurit
for(int i = lasthand; i < maxusers; i++) {
lasthand++;
// Jos käyttäjän puskurille on varattu muistia
if(rbuffer[i]) {
// Jos pakettipaikassa on jotain
if(rbuffer[i][rbindex[i]]) {
// Palautetaan tieto, pituus ja id
memcpy(data, rbuffer[i][rbindex[i]]->data,
rbuffer[i][rbindex[i]]->len);
lenght = rbuffer[i][rbindex[i]]->len;
if(id != NULL)
*id = i;
// Vapautetaan puskurin paketti
SDLNet_FreePacket(rbuffer[i][rbindex[i]]);
rbuffer[i][rbindex[i]] = NULL;
// Siirretään puskuriosoitin seuraavaan paikkaan
rbindex[i]++;
if(rbindex[i] >= buffersize)
rbindex[i] = 0;
return lenght;
}
// Jos aikakatkaisu on käytössä
if(timeout) {
// Jos käyttäjästä ei ole kuulunut riittävän pitkään
if(time[i]+timeout < SDL_GetTicks()) {
// Jos ollaan serveri
if(maxusers > 0) {
// palautetaan id
if(id != NULL)
*id = i;
// Poistetaan käyttäjälistalta
Kick(i);
} else {
// Katkaistaan yhteys
Disconnect();
}
return -1;
}
}
}
}
// Vastaanotetaan paketti
if(SDLNet_UDP_Recv(socket, packet) > 0) {
// Jos viesti on virheellinen
if(packet->data[0] != 3 && packetid > buffersize)
return 0;
// Selvitetään käyttäjän id
for(int i = 0; i < maxusers; i++) {
if(packet->address.host == user[i].host &&
packet->address.port == user[i].port) {
userid = i;
break;
}
}
// Jos käyttäjä ei ole listalla, eikä viesti ole yhteyspyyntö
if(userid == -1 && packet->data[0] != 3)
return 0;
// Päivitetään viimeisimmän viestin saapumisajankohta
time[userid] = SDL_GetTicks();
// Käsitellään paketti sen tyypin mukaan
switch(packet->data[0]) {
case UDP_MESSAGE:
// Jos paketti voidaan palauttaa saman tien
if(packetid == rbindex[userid]) {
// Palautetaan tieto ja id
memcpy(data, &packet->data[3], packet->len-3);
if(id != NULL)
*id = userid;
// Siirretään puskurin osoitinta eteenpäin
rbindex[userid]++;
if(rbindex[userid] >= buffersize)
rbindex[userid] = 0;
// Palautetaan viestin pituus
return packet->len-3;
}
// Jos puskuri pääsee täyttymään
if(packetid == rbindex[userid]-1 ||
(packetid >= buffersize-1 && rbindex[userid] == 0)) {
// Jos ollaan serveri
if(maxusers > 0) {
// Pudotetaan käyttäjä linjalta
Kick(userid);
} else {
// Katkaistaan yhteys
Disconnect();
return -1;
}
break;
}
// Lisätään paketti puskuriin odottamaan käsittelyä
rbuffer[userid][packetid] = SDLNet_AllocPacket(packet->len-3);
memcpy(rbuffer[userid][packetid]->data,
&packet->data[3], packet->len-3);
rbuffer[userid][packetid]->len = packet->len-3;
// Kootaan pyyntö lähettää aikasempi paketti uudelleen
packet->data[0] = UDP_RESEND;
packetid = rbindex[userid];
packet->len = 3;
// Nollataan socketin suuntaus
SDLNet_UDP_Unbind(socket, 0);
// Suunnataan socketti
if(SDLNet_UDP_Bind(socket, 0, &packet->address))
return -2;
// Lähetetään paketti
if(!SDLNet_UDP_Send(socket, 0, packet))
return -2;
break;
case UDP_REMESSAGE:
// Jos paketti voidaan palauttaa saman tien
if(packetid == rbindex[userid]) {
// Palautetaan tieto ja id
memcpy(data, &packet->data[3], packet->len-3);
if(id != NULL)
*id = userid;
// Siirretään puskurin osoitin eteenpäin
rbindex[userid]++;
if(rbindex[userid] >= buffersize)
rbindex[userid] = 0;
return packet->len-3;
}
break;
case UDP_RESEND:
// Lähetetään pyydetty paketti uudelleen
if(sbuffer[userid][packetid]) {
// Kootaan paketti
packet->data[0] = UDP_REMESSAGE;
packet->len = sbuffer[userid][packetid]->len+3;
memcpy(&packet->data[3], sbuffer[userid][packetid]->data,
sbuffer[userid][packetid]->len);
// Nollataan socketin suuntaus
SDLNet_UDP_Unbind(socket, 0);
// Suunnataan socketti
if(SDLNet_UDP_Bind(socket, 0, &packet->address))
return -2;
// Lähetetään paketti
if(!SDLNet_UDP_Send(socket, 0, packet))
return -2;
}
break;
case UDP_CT:
// Lisätään käyttäjä listaan
for(int i = 1; i < maxusers; i++) {
if(user[i].host == 0) {
user[i] = packet->address;
index = i;
break;
}
}
// Jos käyttäjäpaikat on täynnä
if(index == -1)
break;
// Varataan käyttäjän puskureille muistia
sbuffer[index] = new UDPpacket*[buffersize];
rbuffer[index] = new UDPpacket*[buffersize];
for(int i = 0; i < buffersize; i++) {
sbuffer[index][i] = SDLNet_AllocPacket(maxpacketsize);
rbuffer[index][i] = NULL;
}
// Nollataan käyttäjän puskurien osoittimet
sbindex[index] = 0;
rbindex[index] = 0;
// Päivitetään viimeisimmän viestin aika
time[index] = SDL_GetTicks();
// Kootaan paketti
packet->data[0] = UDP_CTA;
packet->len = 1;
// Nollataan socketin suuntaus
SDLNet_UDP_Unbind(socket, 0);
// Suunnataan socketti
if(SDLNet_UDP_Bind(socket, 0, &packet->address))
return -2;
// Lähetetään paketti
if(!SDLNet_UDP_Send(socket, 0, packet))
return -2;
break;
case UDP_DISCONNECT:
// Jos käyttäjää ei ole olmassakaan
if(!user[userid].host)
break;
// Suljetaan yhteys
user[userid].host = 0;
// Vapautetaan käyttäjän puskurit
for(int i = 0; i < buffersize; i++) {
SDLNet_FreePacket(sbuffer[userid][i]);
if(rbuffer[userid][i]) {
SDLNet_FreePacket(rbuffer[userid][i]);
rbuffer[userid][i] = NULL;
}
}
delete [] sbuffer[userid];
delete [] rbuffer[userid];
sbuffer[userid] = NULL;
rbuffer[userid] = NULL;
break;
case UDP_KG:
// Jos paketti voitaisiin lukea suoraan
if(packetid == rbindex[userid])
break;
// Kootaan pyyntö lähettää aikasempi paketti uudelleen
packet->data[0] = UDP_RESEND;
packetid = rbindex[userid];
packet->len = 3;
// Nollataan suuntaus
SDLNet_UDP_Unbind(socket, 0);
// Suunnataan socketti
if(SDLNet_UDP_Bind(socket, 0, &packet->address))
return -2;
// Lähetetään paketti
if(!SDLNet_UDP_Send(socket, 0, packet))
return -2;
break;
default:
break;
}
}
return 0;
}
/* ------------------------------------------------------------ *\
Katkaisee yhteyden käyttäjään
parametrit:
id: käyttäjän id
\* ------------------------------------------------------------ */
void UDP::Kick(int id) {
// Jos luokkaa ei ole alustettu
if(!user)
return;
// Jos ei olla serveri
if(maxusers == 0)
return;
// Jos id on laiton
if(id < 0 || id >= maxusers)
return;
// Jos id ei ole käytössä
if(!user[id].host)
return;
// Lähetetään ilmoitus yhteydenkatkaisusta
packet->data[0] = UDP_DISCONNECT;
SDLNet_UDP_Unbind(socket, 0);
SDLNet_UDP_Bind(socket, 0, &user[id]);
SDLNet_UDP_Send(socket, 0, packet);
// Poistetaan käyttäjä listalta
user[id].host = 0;
// Vapautetaan käyttäjän puskurit
for(int i = 0; i < buffersize; i++) {
SDLNet_FreePacket(sbuffer[id][i]);
if(rbuffer[id][i])
SDLNet_FreePacket(rbuffer[id][i]);
}
delete [] sbuffer[id];
delete [] rbuffer[id];
sbuffer[id] = NULL;
rbuffer[id] = NULL;
}
/* ------------------------------------------------------------ *\
Asettaa aikakatkaisun. Jos käyttäjältä ei saada viestejä annetun
ajan kuluessa, suljetaan tämän yhteys.
parametrit:
second: sekunnit
\* ------------------------------------------------------------ */
void UDP::SetTimeout(int second) {
timeout = second*1000;
}
/* ------------------------------------------------------------ *\
Tarkastaa, onko id varattu
parametrit:
id: käyttäjän id
\* ------------------------------------------------------------ */
bool UDP::Reserved(int id) {
// Jos luokka ei ole alustettu
if(!user)
return false;
// Jos ei olla serveri
if(maxusers == 0)
return false;
// Jos id on laiton
if(id < 0 || id >= maxusers)
return false;
// Jos id:lle on merkitty ip
if(user[id].host)
return true;
// Id on vapaa
return false;
}// Esimerkki clientistä. Ohjelma luo clientin, yhdistää serveriin, pyytää
// käyttäjältä syötteitä ja lähettää ne serverille.
#include <SDL/SDL.h>
#include <SDL/SDL_net.h>
#include <iostream>
#include <string>
#include "udp.h"
using std::cout;
using std::cin;
using std::endl;
using std::string;
// Pakettien maksimikoot ja puskurien koot tulee
// alustaa samoiksi serverillä ja clientillä
const int PACKET_MAX_SIZE = 1024;
const int PACKET_BUFFER_SIZE = 512;
int main(int count, char* param[]) {
UDP client; // client -objekti
char packet[PACKET_MAX_SIZE] = {0}; // käsiteltävän paketin puskuri
string input; // käyttäjän syöte
int result = 0; // tulos
bool exit = false; // onko aika lopettaa
// Alustetaan SDL ja SDL_net
SDL_Init(0);
SDLNet_Init();
// Luetaan ja kirjoitetaan konsoliin
freopen("CON", "w", stdout);
freopen("CON", "r", stdin);
// Alustetaan clientti, jossa siirrettävän paketin dataosuuden maksimi
// koko on PACKET_MAX_SIZE tavua, puskuriin mahtuu 512 pakettia ja
// vastaanottaa paketteja ensimmäiseen porttiin, joka on vapaana
// alustus hetkellä
if(client.Initialize(0, PACKET_MAX_SIZE, PACKET_BUFFER_SIZE, 0) < 0) {
cout << "Clientin alustaminen epäonnistui" << endl;
return EXIT_FAILURE;
}
// Yhdistetään serveriin
if(client.Connect("127.0.0.1", 5000) < 0) {
cout << "Yhdistäminen serveriin epäonnistui" << endl;
client.Release(); // Ei pakollinen
return EXIT_FAILURE;
}
// Yhdistäminen onnistui
cout << "Yhdistettiin serveriin" << endl;
// Pyöritetään clienttiä
while(!exit) {
// Luetaan syöte aina rivinvaihtoon asti
getline(cin, input);
// Jos halutaan poistua
if(input == "/quit") {
// Lopetetaan
client.Disconnect();
exit = true;
} else {
// Lähetetään viesti serverille
result = client.SendData((Uint8*)input.c_str(), input.length(), HOST);
if(result == -1)
cout << "Viestin lähettäminen epäonnistui" << endl;
if(result == -2)
cout << "Socket error!" << endl;
// Serverillä lähettäminen menisi samaan tapaan
// server.SendData((Uint8*)input.c_str(), input.lenght(), userid);
}
// Reagoidaan serverin pyyntöihin. pdata voi olla NULL, koska
// esimerkkiserveri lähettää vain järjestelmän sisäisiä paketteja
result = client.RecvData(NULL, NULL);
// Jos serveri katkaisi yhteyden
if(result == -1) {
cout << "Serveri katkaisi yhteyden" << endl;
exit = true;
}
if(result == -2) {
cout << "Socket error!" << endl;
exit = true;
}
// Koska asetimme serverillä timeoutin 60sek, olisi hyvä kertoa
// serverille välillä olemassaolostamme. Se kävisi yksinkertaisesti
// niin, että vaikka 20sek välein kutsuttaisiin: client.KeepGoing();.
// Tällöin clientti lähettää serverille tiedon olemassa olostaan.
}
// Luokkaa ei ole pakko vapauttaa, se osaa sen itsekin
client.Release();
return EXIT_SUCCESS;
}// Esimerkki serveristä. Ohjelma luo serverin, vastaanottaa paketteja ja
// tulostaa niiden lähettäjät ja pakettien sisällöt.
#include <SDL/SDL.h>
#include <SDL/SDL_net.h>
#include <iostream>
#include "udp.h"
using std::cout;
using std::cin;
using std::endl;
// Pakettien maksimikoot ja puskurien koot tulee
// alustaa samoiksi serverillä ja clientillä
const int PACKET_MAX_SIZE = 1024;
const int PACKET_BUFFER_SIZE = 512;
const int MAX_USERS = 128;
int main(int count, char* param[]) {
UDP server; // server -objekti
char packet[PACKET_MAX_SIZE] = {0}; // käsiteltävän paketin puskuri
int packetlen = 0; // vastaanotettavan paketin pituus
int userid = 0; // käyttäjän id
bool exit = false; // onko aika lopettaa
// Alustetaan SDL ja SDL_net
SDL_Init(0);
SDLNet_Init();
// Ohjetaan tulostus konsoliin
freopen("CON", "w", stdout);
// Alustetaan serveri, joka ottaa vastaan maksimissaan 128 käyttäjää,
// siirrettävän paketin dataosuuden maksimikoko on PACKET_MAX_SIZE tavua,
// puskuriin mahtuu 512 pakettia ja kuunneltava portti on 5000
if(server.Initialize(MAX_USERS, PACKET_MAX_SIZE, PACKET_BUFFER_SIZE, 5000) < 0) {
cout << "Serverin alustaminen epäonnistui" << endl;
return EXIT_FAILURE;
}
// Asetetaan aikakatkaisu(nyt 30 sekuntia, eli jos käyttäjä ei lähetä
// paketteja tietyn ajan kuluessa, niin se tiputetaan
server.SetTimeout(30);
// Alustaminen onnistui
cout << "Serveri luotiin onnistuneesti, odotellaan käyttäjiä" << endl;
// Pyöritetään serveriä
while(!exit) {
// Katsotaan, onko uusia paketteja
packetlen = server.RecvData(packet, &userid);
// Clientillä vastaanottaminen tapahtuisi samaan tapaan
// client.RecvData(packet, NULL);
// Jos yhteys katkeaa
if(packetlen == -1) {
cout << "Käyttäjä(" << userid << ") pudotettu pitkän viiven takia" << endl;
exit = true;
}
// Jos socketti hajos
if(packetlen == -2) {
cout << "Socket-error! " << endl;
exit = true;
}
// Jos saatiin paketti
if(packetlen > 0) {
// Lisätään nollatavu viestin perään
packet[packetlen] = 0;
// Tulostetaan lähettäjä ja paketin sisältö
cout << "user " << userid << ": " << packet << endl;
}
// Jos haluaisimme tietää, onko jokin tietty id varattu, kutsuisimme
// server.Reserved(id);
}
// Luokkaa ei ole pakko vapauttaa, se osaa sen itsekin
server.Release();
return EXIT_SUCCESS;
}Tämä olisi hieno systeemi jos tässä käytettäisiin ihan systeemin soketteja eikä SDL:n net rajapintaa vaikka ei siinäkään mitään vikaa ole...
Voisiko joku laittaa toimivia esimerkkejä?
Onhan se hieno pätkä, ja ehdottomasti mieluummin sdl_netillä kuin systeemin socketeilla... Mutta eikö mieluummin kannattaisi käyttää ihan TCP:tä jos pakettien täytyy liikkua järjestyksessä ja hukkumatta?
Turatzuro kirjoitti:
Onhan se hieno pätkä, ja ehdottomasti mieluummin sdl_netillä kuin systeemin socketeilla... Mutta eikö mieluummin kannattaisi käyttää ihan TCP:tä jos pakettien täytyy liikkua järjestyksessä ja hukkumatta?
Tarkoitukseni oli tehdä systeemistä mahdollisimman kevyt ja nopea. Kehitin tämän jo reilun vuoden verran kehitteillä olleeseen mmorpg:iin, koska sen aikaisempi verkkosysteemi(TCP) oli kovin hidas ja raskas.
Aivan mahtava koodi, vaikken tuosta Release-funktiosta tykkääkään. Miksei sen tilalla oo esim. Javan String-olio??
Eipä nyt ihan äkkiä tule mieleen, että miksi C++-luokan Release-funktion tilalla pitäisi olla esim. Javan String-olio.
Noh, mutta kun C++:ssa nyt kummiskin on konstruktorit ja destruktorit, niin onko jokin erityinen syy tehdä Initialize- ja Release-funktiot? Tuollaisten kanssa sattuu helposti virheitä. Esmes, mitä tapahtuukaan, jos Initializea kutsutaan useampaan kertaan?
Luokassa on paljon resurssien hallintaa. Missä on kopioiva konstruktori ja sijoitusoperaattori? Nyt taitaa mennä debuggaushommiksi, jos UDP-olion kopioi toiseksi?
Tarviiko erilaisia määriä ja rajoja edes asetella etukäteen? Eikös koodi olisi dynaamisempaa ja robustimpaa, jos siinä käytettäisiin C++:n container-luokkia ja muita C++-maisuuksia kuten exceptioneita? Koska tämä nyt meinaa kumminkin olla C++:aa, eikös siinä pitäisi ainakin varautua exceptioneihin?
Jos lähetettävien pakettien pitää olla määrämuotoisia (tunnus-id-sisältö), eikö se olisi hyvä pakottaa kutsurajapinnassa?
Systeemin raskauteen ja hitauteen vaikuttaa ihan varmasti ekaksi se, että siinä ylipäätänsä tehdään i/o:ta. UDP on kevyt vain sen takia, että jotakin jätetään tekemättä. Jos haluaa vikasietoisuutta - niin kuin esmes nuo uudelleenlähetykset - ja niin että se toimii erilaisissa verkoissa käsiin räjähtämättä, pitää olla aika elite fakiiri. Ei se TCP välttämättä ole ihan huono vaihtoehto.
Ihan rivi riviltä en tuota pätkää jaksanut kahlata, mutta kyllä se sinänsä hyvältä idealta vaikuttaa. Ja vielä parempi siitä varmaan tulisi toisen suunnittelukierroksen jälkeen.
Jos nyt oikein katsoin niin vastapään pitää osata pyytää puuttuvaa pakettia, eikö? Miten vastapää tietää, että paketti puuttuu? Oletetaan esimerkiksi että lähetän yhden paketin tunnissa. Vasta tunnin päästä lähettäessäni toista pakettia tiedestään että hupsista, edellinen puuttuu, pyydetäänpä sekin sieltä. Eli tässä on ihan erilainen uudelleenlähetys kuin TCP:ssä.
Käytät paria constia, mutta miksi paketin tyypit jne eivät ole vaikka enumeja vaan numerovakioita? Samoin paluuarvot. Nyt -1 voi tarkoittaa "ei saatu yhteyttä", "pakettia ei voitu lähettää" jne. Rumaa.
Ja samaa mieltä noista vakiokokoisten taulukoiden käytöstä. Vectorit käyttöön!
jones7: Mut eihän toi oo javaa!
Vähän jälkikäteen tulee tämä vastaus, mutta tulee kumminkin :)
Tässä on tullut paljon hyvää rakentavaa kommenttia ja siitä suuri kiitos teille! En ole mikää c++-guru, eli kielen kaikki hienoudet eivät ole vielä tuttuja(container-luokka? exceptioni?).
Vaikka järjestelmä sinällään(virheenkorjauskykyineenkin) kelpaa peliini, ryhdyin suunnittelemaan vastaavanlaista järjestelmää TCP:lle. Eiköhän siitä tule entistä parempi :)
Saako käyttää omassa projektissa? Itse epäosaan kaiken internetin kanssa ohjelmoinnissa paitsi jotkut Winsockit :(
Saa käyttää omissa projekteissa. Sitähän varten tuon tänne lähetinkin :).
Voisiko saada esimerkin palvelimesta ja asiakkaasta?
Itsekin varmasti saisin aikaiseksi, mutta turha siihen on aikaa tuhlata jos sinulla on esimerkit valmiina.
Edit:
// Jos luokka on alustettu if(user) Release();
= SegFault
// Jos luokka on alustettu //if(user) Release();
= Toimii ongelmitta :)
Kaatumisen aiheuttaa Release-funktion kutsu kesken Initializen. If-lause on aina tosi. Korjaa.
Edit2:
Yritin käyttää. Packetbuffer täyttyy ylimääräisellä datalla joka ei lähde lopusta. TUlin siihen tulokseen että yrität vain huijata ihmisiä. Siirryn SDL_netin suoraan käyttöön.
ville-v kirjoitti:
// Jos luokka on alustettu if(user) Release();= SegFault
// Jos luokka on alustettu //if(user) Release();= Toimii ongelmitta :)
Kaatumisen aiheuttaa Release-funktion kutsu kesken Initializen. If-lause on aina tosi. Korjaa.
Jep, olin huomannut virheen aiemmin omissa projekteissani, mutta en ollut muistanut korjata sitä tänne.
ville-v kirjoitti:
Yritin käyttää. Packetbuffer täyttyy ylimääräisellä datalla joka ei lähde lopusta. TUlin siihen tulokseen että yrität vain huijata ihmisiä. Siirryn SDL_netin suoraan käyttöön.
Nyt en ymmärtänyt kunnolla. Mikä puskuri tarkalleen ottaen täyttyy ylimääräisestä datasta? Ei lähde mistä lopusta?
Sen verran voin sanoa luokan toimivuudesta, että olen itse käyttänyt sitä kahdessa projektissani(toinen valmis ja toinen keskeneräinen), enkä ole havainnut toimivuusongelmia.
Olisi mukava, jos jaksaisit tarkentaa havaintojasi :)
Gaxx kirjoitti:
ville-v kirjoitti:
Yritin käyttää. Packetbuffer täyttyy ylimääräisellä datalla joka ei lähde lopusta. TUlin siihen tulokseen että yrität vain huijata ihmisiä. Siirryn SDL_netin suoraan käyttöön.
Nyt en ymmärtänyt kunnolla. Mikä puskuri tarkalleen ottaen täyttyy ylimääräisestä datasta? Ei lähde mistä lopusta?
Sen verran voin sanoa luokan toimivuudesta, että olen itse käyttänyt sitä kahdessa projektissani(toinen valmis ja toinen keskeneräinen), enkä ole havainnut toimivuusongelmia.
Olisi mukava, jos jaksaisit tarkentaa havaintojasi :)
Tämäntapaiset ohjelmat tein. Jos tein jotain väärin, se johtuu siitä ettet vaivautunut tekemään esimerkkejä helppokäyttöiseen luokkaasi.
#include "./socket/UDP.h"
#include <iostream>
// g++ -o udpservertest.bin $(sdl-config --cflags) udpservertest.c ./socket/UDP.c -lSDL_net
int main(void){
int quit = 0;
SDL_Init(0);
SDLNet_Init();
UDP serversock;
char packetbuffer[1000];
int user_id = 0;
serversock.Initialize(100,1000,1000,5000);
std::cout << "Aloitetaan" << std::endl;
while(!quit){
if(serversock.RecvData(&packetbuffer, &user_id)){
std::cout << packetbuffer << std::endl;
}
}
serversock.Release();
return 0;
}#include "./socket/UDP.h"
#include <iostream>
#include <string>
// g++ -o udpclienttest.bin $(sdl-config --cflags) udpclienttest.c ./socket/UDP.c -lSDL_net
int main(void){
int quit = 0;
SDL_Init(0);
SDLNet_Init();
UDP clientsock;
char packetbuffer[1000];
int user_id = 0;
std::string buffer;
clientsock.Initialize(0,1000,1000,0);
clientsock.Connect("localhost", 5000);
while(!quit){
getline(std::cin, buffer);
clientsock.SendData((char*)buffer.c_str(), buffer.length(), user_id);
}
clientsock.Release();
return 0;
}Tulostus:
ville-v@a84-230-83-57:~/Projektit/Senia/mmorpg$ g++ -o udpservertest.bin $(sdl-config --cflags) udpservertest.c ./socket/UDP.c -lSDL_net ville-v@a84-230-83-57:~/Projektit/Senia/mmorpg$ ./udpservertest.bin Aloitetaan moikkaٷ,�ٷ� miten menee?� se bugi näkyykin näköjään äkkiä vai mitä?�kyykin näköjään äkkiä kumma jos tätä ei ole muilla äkkiä tämäjos tätä ei ole muilla äkkiä kaimäjos tätä ei ole muilla äkkiä riittää tätä ei ole muilla äkkiä esimerkiksiätä ei ole muilla äkkiä
ville-v@a84-230-83-57:~/Projektit/Senia/mmorpg$ g++ -o udpclienttest.bin $(sdl-config --cflags) udpclienttest.c ./socket/UDP.c -lSDL_net ville-v@a84-230-83-57:~/Projektit/Senia/mmorpg$ ./udpclienttest.bin moikka miten menee? se bugi näkyykin näköjään äkkiä vai mitä? kumma jos tätä ei ole muilla tämä kai riittää esimerkiksi
Olet tehnyt klassisen virheen: nollatavua ei lähetetä(eikä sitä myöskään lisätä vastaanoton yhteydessä).
Luokka ei tietenkään itse lisäile mitään ylimääräisiä nollatavuja, koska sen täytyy osata välittää, myös raakaa dataan, ei vain merkkijonoja, joiden lopetusmerkkinä on tuo mainittu nollatavu. Luokan ei myöskään ole mielekästä tyhjentää paketbufferia(käyttäjä voi tehdä siitä vaikka gigan kokoisen), vaan vastaanotetun datan pituuden saa RecvData:n palautusarvona(lue funktion kuvaus).
Kieltämättä hieman ihmetyttää, etten ole väsännyt niitä esimerkkejä. Lupaan tehdä ne tämän viikon aikana!
Edit: Ei, se on sen verran pieni homma, että voin luvata esimerkit jo huomiseen iltaan mennessä.
Edit: Nono, tuli lunastettua lupaus ja vielä hyvissä ajoin :)
Aihe on jo aika vanha, joten et voi enää vastata siihen.