Registriert: Di Okt 13, 2009 17:25 Beiträge: 365
Programmiersprache: C++
Hallo Leute, seit einigen Wochen beschäftige ich mich wieder mit Netzwerkprogrammierung und möchte nun endlich mal einen UDP-Server hinbekommen, der vernünftig mehrere Clients verwalten kann. Mein momentaner Ansatz sieht so aus:
Der Server besteht im Wesentlichen aus zwei Threads. Ein Send- und ein Receive-Thread. Beide laufen quasi in einer Endlosschleife (allerdings mit Sleep() zwischendurch). Bevor die Threads gestartet werden, wird natürlich ein bind() aufgerufen. Wenn der Receive-Thread ein Paket mit recvfrom empfängt, wird es analysiert und ein Antwortpaket an eine Sendeliste angehängt. Diese Liste wird vom Send-Thread abgearbeitet (mit sendto). Wenn kein Paket in der Liste ist, wird dennoch ein leeres Paket gesendet, damit der Client ein Timeout erkennen kann.
Die Clientseite sieht eigentlich genau gleich aus, jedoch wird vorher kein bind() aufgerufen. Das Senden funktioniert dennoch prima, leider kommt aber die Antwort vom Server nicht beim Client an (sie wird aber definitiv gesendet). Das recvfrom auf Clientseite blockiert ewig. Dies ist offenbar kein Fehler:
MSDN hat geschrieben:
The local address of the socket must be known. For server applications, this is usually done explicitly through bind. Explicit binding is discouraged for client applications. For client applications using this function, the socket can become bound implicitly to a local address through sendto, WSASendTo, or WSAJoinLeaf.
Okay, dachte ich, noch einen Port binden will ich sowieso nicht, also sorge ich dafür, dass das erste sendto vor dem ersten recvfrom aufgerufen wird. Gedacht, getan, Ergebnis: keine Änderung. Eine weitere Sache, die ich ausprobiert habe, ist nicht recvfrom, sondern recv zu verwenden und vorher ein connect aufzurufen. Doch auch das hat nicht geholfen.
*Argh* UDP macht mich fertig. Was mache ich falsch, hat da jemand eine Idee?
Vielen Dank schonmal im Voraus!
Zuletzt geändert von mrtrain am Mi Aug 31, 2011 21:32, insgesamt 1-mal geändert.
Registriert: Mi Dez 03, 2008 12:01 Beiträge: 167 Wohnort: /country/germany
Programmiersprache: C++ / FreeBASIC
Hm, so direkt fällt mir jetzt nichts ein, meine bisherigen Experimente mit UDP sind schon ne Weile her und waren auch nicht besonders umfangreich Aber vielleicht findest du hier einen Hinweis, da wird eigentlich alles ganz gut erklärt: http://www.c-worker.ch/tuts/udp.php
Mehr fällt mir dazu leider nicht ein
_________________ Traue keinem Computer, den du nicht aus dem Fenster werfen kannst -- Steve Wozniak
Registriert: Do Sep 02, 2004 19:42 Beiträge: 4158
Programmiersprache: FreePascal, C++
Gibts nen speziellen Grund warum du das so „roh“ machst und nicht z.B. Indy oder Synapse verwendest?
greetings
_________________ If you find any deadlinks, please send me a notification – Wenn du tote Links findest, sende mir eine Benachrichtigung. current projects: ManiacLab; aioxmpp zombofant network • my photostream „Writing code is like writing poetry“ - source unknown
„Give a man a fish, and you feed him for a day. Teach a man to fish and you feed him for a lifetime. “ ~ A Chinese Proverb
Registriert: Di Mai 18, 2004 16:45 Beiträge: 2623 Wohnort: Berlin
Programmiersprache: Go, C/C++
udp hat eigentlich nur sehr selten Vorteile, meistens ist es einfacher wenn man tcp nutzt und socketoptions benutzt. So gibt es z.B. nodelay um dafür zu sorgen, dass das windowing nicht benutzt wird und somit die latenzen von udp schafft. Cork kann dir helfen, wenn du viele Pakete senden willst um den overhead von tcp entgegen zu wirken und nahe udp durchsatz zu kommen. Allerdings ist udp session less und garantiert das ankommen von paketen nicht. Diese muss man also erstmal selber implementieren und das ist oft wesentlich ineffizienter. Daher nutzen auch Spiele immer TCP und zusätzlich oft UDP für unkritische pakete wie updates.
Bei UDP gibt es das Problem mit "package lost" und dann kommt noch dazu, dass es mehrere network interfaces gibt also musst du sicher sein auf dem richtigen sein. Wenn du auf loopback adapter sendest und auf deiner Netzwerk adapter hörst wird es nie gehen.
Zu deinem Konzept. Es macht in der Praxis kein Sinn ein Sendethread zu haben, dein Receive Thread bekommt die msg, verarbeitet sie und generiert dein paket und das gibst du dem send thread. Du hast also den aufwändigen Teil immer noch im Receive Thread, denn das Senden des Paketes ist vergleichsweise günstig und du baust ein bottleneck beim mutex der task liste im send thread auf. Macht der Receive Thread das senden, dann kannst du problemlos hoch skalieren, hast du ein 4kerner oder 8 macht hier entsprechend mehr mögliche clients aber mit dem einen send Thread nicht, der hat sein limit auf der lock/unlock time. Die Liste so um zu bauen, das es atomic operation nutzt ist recht schwer und reduziert nur die Zeit bis das bottleneck wieder hast. Eine schmerzliche Erfahrung die ich machen musste ist, dass verteilen von der Arbeit über mehrere Threads, versuch wirklich alles was mit dem Client zu tun hat in den entsprechendem Thread zu packen, so dass dieser nie auf Daten von ein anderen Thread gucken muss. Mutex sind ziemlich teuer und skalieren ultra schlecht und machen den unterschied zwischen 100 und 1000 Usern.
_________________ "Wer die Freiheit aufgibt um Sicherheit zu gewinnen, der wird am Ende beides verlieren" Benjamin Franklin
Registriert: Di Okt 13, 2009 17:25 Beiträge: 365
Programmiersprache: C++
Danke für eure Antworten.
@c-worker.ch: Das Tutorial kenne ich schon, es war eine meiner Lernquellen.
Lord Horazont hat geschrieben:
Gibts nen speziellen Grund warum du das so „roh“ machst und nicht z.B. Indy oder Synapse verwendest?
Wenn ich das richtig sehe, ist die eine Bibliothek für C#/.NET und die andere für Delphi, oder? Nun, ich programmiere aber mit C++... Hast du dafür auch 'ne gute Library auf Lager? Allerdings bin ich noch nicht so erfahren damit, fremde Libraries zu verwenden. Bei manchen erschließt sich mir auch nicht der Sinn. Es gibt ja z.B. auch noch SDLnet, aber wenn ich mich nicht irre, ist es eigentlich genau wie Winsock, nur dass die Funktionen minimal anders heißen.
@TAK2004: Danke für deine umfangreiche Antwort, ich geh nachher darauf ein, aber jetzt muss ich erst wieder zur Schule.
Registriert: Di Mai 18, 2004 16:45 Beiträge: 2623 Wohnort: Berlin
Programmiersprache: Go, C/C++
Raknet ist die beste udp based lib, die baut in prinzip tcp in udp nach und bietet darüber hinaus noch tonnenweise weitere Features. Für Projekte mit mehreren hundert Usern sehr gut geeignet, auf richtiger Server Hardware schaffte man sogar bis zu 5k Verbindungen.
_________________ "Wer die Freiheit aufgibt um Sicherheit zu gewinnen, der wird am Ende beides verlieren" Benjamin Franklin
Registriert: Di Okt 13, 2009 17:25 Beiträge: 365
Programmiersprache: C++
Nun zu dir, TAK. Dass UDP nicht zuverlässig ist, ist mit bekannt. Was mir nicht bekannt ist, ist "Cork". Was ist das?
TAK2004 hat geschrieben:
Dann kommt noch dazu, dass es mehrere network interfaces gibt also musst du sicher sein auf dem richtigen sein. Wenn du auf loopback adapter sendest und auf deiner Netzwerk adapter hörst wird es nie gehen.
Ich denke hier liegt mein Problem. Wie lege ich denn mein Network Interface fest?
TAK2004 hat geschrieben:
Zu deinem Konzept. Es macht in der Praxis kein Sinn ein Sendethread zu haben, dein Receive Thread bekommt die msg, verarbeitet sie und generiert dein paket und das gibst du dem send thread.
Die Idee ist folgende: Man gehe der Einfachheit halber zunächst von nur einem Client aus. Client und Server tauschen alle paar Millisekunden Datenpakete aus, notfalls auch leere. Warum leere Pakete? 1. Um ein Timout zu erkennen 2. Es ist nicht vorhersehbar, welcher PC als nächstes Daten zu senden hat. Wenn der Client eine Nachricht senden will, aber gerade von recv/recvfrom blockiert wird, weil der Server nichts sendet, kommt die Nachricht nie beim Server an.
Nun ist es bei UDP ja möglich, dass ein Paket auf der Strecke verloren geht. Deshalb funktioniert ein reines PingPong-Prinzip hier nicht mehr. (Der Server wartet dann auf das Paket des Clienten, das verloren gegangen ist. Der Client wiederum wartet währenddessen auf das Antwortpaket.) So kam mir die Idee, das Senden vom Empfangen zu trennen, sprich in zwei verschiedenen Threads zu erledigen. Nun brauche ich auch grundsätzlich nur noch zwei Kommunikationsthreads, weil der Sende-Thread gleich alle Clients mit Paketen versorgen kann. Der Empfangsthread kann auch von INADDR_ANY empfangen (tut recvfrom glaube ich sowieso), die Pakete den jeweiligen Clients zusortieren und gleich wieder in Empfangsbereitschaft gehen.
Wenn ein Client eine Nachricht empfangen hat, sendet er ein Bestätigungspaket, sodass der Eintrag in der Sendeliste des Servers erst dann gelöscht wird und sein Sendethread auch erst dann aufhört, das Paket zu senden. So sollte imho gewährleistet sein, dass jedes Paket beim Clienten ankommt. Diese Überprüfung kann ich mir natürlich sparen, wenn es sich um Pakete mit Positionsdaten o.ä. handelt. Aber so weit bin ich noch nicht.
Dass ich einen Send- und einen Receive-Thread habe, hat also nichts mit der Performance zu tun, sondern ist imho notwendig, damit nicht beide Seiten irgendwann beim recv stehenbleiben. Dass das alles nicht funktioniert, wenn ich mit der localhost-Adresse arbeite, ist mir klar. Darum habe ich auch mit einem zweiten Computer getestet (eine VM hätte es natürlich auch getan).
100 User oder gar noch mehr plane ich übrigens nicht. Ich weiß nicht, ob Raknet etwas überdimensioniert ist. Was mich skeptisch macht, ist jedenfalls, dass auf deren Website einerseits steht, es sei Open-Source, andererseits kostet es aber für Unternehmen Geld und als Hobbyist muss man sich registrieren.
Registriert: Di Mai 18, 2004 16:45 Beiträge: 2623 Wohnort: Berlin
Programmiersprache: Go, C/C++
Über bind legst du fest, auf welcher nic er horschen soll, oft INADDR_ANY ist aber nicht zu empfehlen. Da du dann Pakete bekommen kannst, die du garnicht willst, weil du auf dem Loopback Adapter z.B. von einem Tool die Pakete mit abfängst, weil der zufällig den gleichen Port nutzt.
Um das auf einem Thread zu realisieren, wird normalerweise ein Timeout verwendet. Der Client sollte den Heartbeat(so nennt sich das bei tcp) machen. Du legst ein TimeToLive fest, bei TCP z.B. 2Minuten. Nun muss dein Client mindestens jede Minute ein Paket senden(Nyquist-Shannon-Abtasttheorem). Also sollte dein Client recv spätestens nach 1Minute timeouten. Der Server macht den Gegenpart, er wartet im recv für 1min, prüft ob die 2min rum sind, wenn recv mit timeout err zurück kommt und wird er die verbindung als tot betrachten, wenn es der fall ist. Kommt ein Paket rein, dann addierst du einfach now mit 2min und weißt sie der variable für timeout check zu. Damit hast du eine primitive session, zwar nicht mit der von tcp vergleichbar aber immerhin. Nun fällt dir allerdings auf, dass im blödesten Fall du am client und server nur jede minute mal senden kannst, deswegen wird die recv timeout oft kleiner gesetzt. Es gibt mehrere techniken, z.B. interrupts, feste timesleep intervalle, time prediction und natürlich paralleles senden/recv.
Wenn du z.B. ein Gameserver hast, dann wäre dein recv thread ein frage/antwort system und parallel dazu läuft auf einem anderem thread Spielelogik z.B. ein send für updates(pushverfahren). Sockets sind bis zu einem bestimmten punkt thread safe, gleichzeitiges recv und send ist thread safe aber recv/recv und send/send nicht. Hier kannst du nun Mutex verwenden, um sicher zu gehen, dass das senden geht kostet aber oder du nimmst ein weiteren socket für push notification. Daher wäre mein Vorschlag nimm 2 sockets und invertiere die Seiten, also 1. socket ist auf dem server der server und auf dem 2. der client und auf dem client entsprechend umgedreht. Dann hast du auf beiden seiten, jeweils eine session für recv/send und eine für send. Dann brauchst du zwar 2 ports aber hast keine threading probleme, die threads hängen nahezu immer im sleep und das ist gut. Ach und die anzahl der genutzten ports verdoppelt sich nicht, da du ja n+1 ports nutzt, wobei n die clientanzahl ist und +1 der push port auf den alle clients hängen.
Edit: Das sind vorschläge, teilweise schon umgesetzt gesehen, teilweise einfach so, wie ich es machen würde. Also kein Patentrezept und ich arbeite lieber mit TCP, das es viele Graue Haare erspart und nicht langsamer als UDP ist, wenn man es korrekt nutzt.
Registriert: Di Okt 13, 2009 17:25 Beiträge: 365
Programmiersprache: C++
Danke für deinen ausführlichen Rat zu dem Thema. Nach einiger weiterer Recherche habe ich ihn befolgt und verwende nun doch wieder TCP. Das spart erstmal eine Menge Ärger.
Dieses Konzept mit n+1 Ports habe ich jetzt auch in anderen Foren gefunden. Mal wird gesagt, es sei die einzige mögliche Lösung, an anderen Stellen wird wiederum behauptet, es sei genau so gut möglich, das Konzept, mit dem ich gescheitert bin, zu verwenden.
Mir ist noch immer schleierhaft, warum mein UDP-Ansatz nicht funktioniert hat. Wenn ich tatsächlich auf jeder Seite ein bind() bäuchte, dann könnten alle Tutorials wie das auf c-worker.ch auch nicht funktionieren. Aber wie dem auch sei, mit TCP läuft es wunderbar!
Registriert: Di Mai 18, 2004 16:45 Beiträge: 2623 Wohnort: Berlin
Programmiersprache: Go, C/C++
Freut mich zu hören, dass es läuft
Bei Netzwerk-programmierung unter Windows gibt es ein recht ärgerliches Problem, den Netzwerkbuffer. Die Netzwerk Pakete werden ja in einem Recv/Send Buffer gelegt und dann tut die Netzwerkkarte über dma diese Pakete versenden/empfangen. Bei dem Loopback Adapter entfällt der letzte Schritt und die Lasten(CPU/Speicher) sind wesentlich höher, wenn man sein Server lokal testet. Bei Windows begegnet man dann ein Phänomen, dass Pakete verloren gehen, wenn zu viel last auf dem Loopback Adapter liegt, der Grund ist ein fixer Paketbuffer und das Pakete gedropped werden, wenn der Buffer zu klein für die Maße an kommunizierten Paketen ist. Daher nicht wundern, wenn du ohne Wartezeiten, Pakete zwischen Server und Client versendest, dass zwischendurch Pakete verloren gehen, obwohl du TCP hast und das Paket nie bis zur Netzwerkkarte kommt. Solltest du gegen so etwas resistent sein wollen, dann müsstest du mit den Socket options für Send und Recv Buf den Buffer entsprechend vergrößern(abhängig von Hardware). Bei Linux hat man das Phänomen nicht, allerdings weiß ich nicht auf welcher Ebene und wie vorgegangen wird.
_________________ "Wer die Freiheit aufgibt um Sicherheit zu gewinnen, der wird am Ende beides verlieren" Benjamin Franklin
Mitglieder in diesem Forum: 0 Mitglieder und 2 Gäste
Du darfst keine neuen Themen in diesem Forum erstellen. Du darfst keine Antworten zu Themen in diesem Forum erstellen. Du darfst deine Beiträge in diesem Forum nicht ändern. Du darfst deine Beiträge in diesem Forum nicht löschen. Du darfst keine Dateianhänge in diesem Forum erstellen.