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 }; #endif
UDP.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.