Ich schreibe gerade einen Server. Clients Verbinden sich mit diesem lokal per Unix Sockets, oder übers Netzwerk per TCP. Es geht mir nun erstmal um die Lokalen Verbindungen, um das Senden. Die Sockets sind non-Blocking, und nutzen SOCK_SEQPACKET (also ein read pro write). Mit jedem write sende ich ein oder mehrere Nachrichten, die haben immer zuerst ein Typ und Längenfeld. Der Empfangspuffer ist 4KiB gross, die Nachrichten müssen also in 4KiB Chunks passen. Ich habe vor, mehrere Nachrichten auf einmal zu senden. Es gibt einen Main Loop mit epoll. Die FDs darin warten entweder auf POLLIN oder POLLOUT. Solange es zu sendende Daten gibt, werden keine neuen empfangen, damit sich da möglichst wenig anstaut. Es gibt eine Funktion, um eine Nachricht zu senden. Nun gibt es ein paar Variationen, wann ich den Speicher reservieren, und wann ich die Daten effektiv Senden könnte. Was schon mal klar ist, wenn es schon zu sendende Daten gibt, muss ich die Liste mit den zu sendenden Daten erweitern. In den anderen Fällen könnte ich folgende Dinge tun: A) Erstmal versuchen zu Senden, falls nötig kopieren Ich könnte erstmal ein write() versuchen. Falls ich EWOULDBLOCK kriege, kann ich die Daten kopieren zur Queue hinzufügen. Dann würde ich sie senden, sobald ich POLLOUT von epoll erhalte. Vorteil dieser Variante: Nachrichten sind oft klein, und können auf dem Stack liegen. Solange das write() klappt, muss ich nichts Kopieren, und brauche kein malloc. B) Daten gleich beim caller mit malloc reservieren, und erstmal versuchen zu senden Ich könnte es so machen, dass die Funktion zum Senden der Nachrichten sich um das freigeben der Daten kümmert, diese also nicht kopiert. Ich könnte immer noch erstmal ein write() versuchen, und falls nötig die Daten zur Queue hinzufügen, aber müsste nichts kopieren. Vorteil dieser Variante: Es muss nichts kopiert werden, aber es brauch immer ein malloc / free. C) Daten kopieren, später Senden Hier würde ich die Daten zuerst mal in die Queue kopieren, die würden dann später gesendet. Vorteil dieser Variante: Eventuell könnte ich mehrere Nachrichten in den selben Buffer packen, dann hätte ich weniger Allokationen. Oft würde aber mehr Speicher Verbraucht, als bei A und B. Ausserdem braucht es nicht für jede einzelne Nachricht einen write() call. D) Daten gleich beim caller mit malloc reservieren, später Senden Hier würde ich die Daten einfach direkt zur Queue hinzufügen. Eine simple linked list sollte da genügen. Vorteil dieser Variante: Die Funktion kann nicht fehlschlagen. Ausserdem braucht es nicht für jede einzelne Nachricht einen write() call. Braucht aber mehr Speicher, als A und B. Welche der Varianten ist am Besten? Bei C und D gibt es noch etwas, das ich tun könnte. Am Ende des main loops, nach dem Empfangen von Daten, könnte ich, falls es zu sendende Daten in der Queue gibt, versuchen die mit writev zu senden. So könnte ich mehrere Nachrichten auf einmal senden, ohne vorher die epoll events anzupassen, und noch einen Zyklus zu warten (das müsste ich nur, falls das Senden fehlschlägt). Wäre das Sinnvoll?
Daniel A. schrieb: > Welche der Varianten ist am Besten? Geschmackssache, ich nehme normalerweise A, dann entsteht der Overhead erst dann, wenn wirklich mal viel am Stück gesendet wird. Denk daran, dass es bei TCP passieren kann, dass ein Teil des übergebenen Pakets gesendet wird und nur der Rest in Deine Queue wandern muss.
So wie ich das verstanden habe, sind die Buffergrößen egal bei TCP. Das wird alles intern erledigt. Ich schicke gerade Bilder, und da habe ich einen write() mit mehreren Megabytes. Das geht oft mit einem Aufruf. Die 4KB sind also Kinderkram. Und wenn der write() nicht alles sendet, in einer Schleife den write() für den Rest gleich hinterher schicken. Funktioniert bei mir prima. 1G Ethernet wird komplett ausgenutzt. Komme auf 120MBytes/sek. Und warum kann man nicht einfach die verschiedenen Möglichkeiten mal ausprobieren? So viel Code ist das gar nicht.
Peter schrieb: > So wie ich das verstanden habe, sind die Buffergrößen egal bei TCP. Das > wird alles intern erledigt. > Ich schicke gerade Bilder, und da habe ich einen write() mit mehreren > Megabytes. Das geht oft mit einem Aufruf. Die 4KB sind also Kinderkram. > Und wenn der write() nicht alles sendet, in einer Schleife den write() > für den Rest gleich hinterher schicken. Ja, bei den TCP Verbindungen von aussen muss ich das so machen. Bei den lokalen Unix Sockets kann ich mir das aber sparen, da kann ich SOCK_SEQPACKET statt SOCK_STREAM nehmen. Dort muss ich mich dann auch nicht um halb gesendete / empfangene Pakete usw. kümmern, und muss nichts zwischenspeichern. Ich behandle da diverse Dinge unterschiedlich, jenachdem, woher die Verbindung kommt. Peter schrieb: > Und warum kann man nicht einfach die verschiedenen Möglichkeiten mal > ausprobieren? So viel Code ist das gar nicht. Ich komme eventuell am Abend zuhause mal ein paar Minuten dazu, daran weiterzumachen. Aber Zeit darüber nachzudenken hab ich viel.
Nimm den einfachsten, robustesten und damit am besten wartbaren Code, der die von dir benötigte Performance bringt. Schön nach Lehrbuch implementiert. Erst mal keine Sonderlocken. Welche Performance? Hast du uns nicht gesagt. Daher nur als Anmerkung: Wenn du sowieso Code schreibst um Messages zu queuen würde ich mir das zusätzliche "probieren wir erst mal so" write() sparen. Ja, das macht es eventuell ein bisschen schneller. Aber malloc()/realloc()/free() sind jetzt nicht die wahnsinnig großen Performance-Katastrophen wenn man es nicht übertreibt. Nur eine Stelle zu haben an der gesendet wird macht die Fehlerbehandlung einfacher und Debuggen sowieso, weil das Ganze etwas deterministischer wird. Wie gesagt, robuster, wartbarer Code vor "premature optimization". Ebenso würde ich gathered writev() nur dann als Optimierung erwägen (und dann messen) wenn die einfache Implementierung zu langsam ist. Dann direkt in die Queue-Verwaltung eingewebt. Also direkt ein iovec[] als die Basis für die Queue nehmen und nicht separat von einer Queue immer ein iovec[] für writev() aufbauen wenn gesendet werden soll. iovec[] entweder statisch oder mit realloc() verwaltet. Ich würde mit einem statischen Array beginnen, 16 Elemente. Das ist die Minimalgröße die eine Implementierung immer unterstützen muss. Wenn das nicht reicht, IOV_MAX ist auf modernen Implementierungen ziemlich groß und daher würde ich die Konstante nicht blind als Array-Größe nehmen. Also was zwischen 16 ≤ iovcnt ⋘ IOV_MAX.
:
Bearbeitet durch User
Peter schrieb: > So wie ich das verstanden habe, sind die Buffergrößen egal bei TCP. Das > wird alles intern erledigt. Naja, was wesentlich ist: Wenn du Daten blockweise sendest, müssen sie nicht in den gleichen Blöcken auf der anderen Seite ankommen. Die Daten von mehreren send()-Aufrufen können zusammengefasst oder ein einzelner send()-Aufruf in mehrere Blöcke aufgeteilt werden (siehe auch Nagle-Algorithmus). Zu einem gewissen Grad kann man das beeinflussen mit der Socket-Option TCP_NODELAY. Man sollte aber trotzdem nicht einfach von der Annahme ausgehen, dass die Daten in genau den Blöcken auf der Empfangsseite ankommen, in denen sie losgeschickt wurden. TCP ist halt Stream-orientiert und nicht Datagram-orientiert. Für letzteres ist UDP eigentlich gemacht, aber da fehlen natürlich ein paar andere Features gegenüber TCP.
Ja, klar, bei TCP ist das so. Aber um es noch einmal zu wiederholen, ich habe, neben TCP Verbindungen, auch lokale per Unix Socket, mit SOCK_SEQPACKET, und da ist das anders: https://www.man7.org/linux/man-pages/man2/socket.2.html > SOCK_SEQPACKET > Provides a sequenced, reliable, two-way connection-based > data transmission path for datagrams of fixed maximum > length; a consumer is required to read an entire packet > with each input system call. Das ist praktisch, weil ich dann keine halben Pakete zwischenspeichern muss. Kann ich aber leider nur lokal verwenden. Es gäbe für übers Netzwerk zwar noch SCTP, aber aber da kann ich nicht davon ausgehen, dass die Netzwerkgeräte dazwischen das unterstützen. Und UDP ist nicht zuverlässig. Darum muss ich dort TCP nehmen, und das etwas anders handhaben, als meine lokalen Verbindungen.
Rolf M. schrieb: > Peter schrieb: >> So wie ich das verstanden habe, sind die Buffergrößen egal bei TCP. Das >> wird alles intern erledigt. > > Naja, was wesentlich ist: Wenn du Daten blockweise sendest, müssen sie > nicht in den gleichen Blöcken auf der anderen Seite ankommen. Die Daten > von mehreren send()-Aufrufen können zusammengefasst oder ein einzelner > send()-Aufruf in mehrere Blöcke aufgeteilt werden (siehe auch > Nagle-Algorithmus). Ja, das ist mit schon klar, dass ich auf der Empfangsseite den Stream wieder in einzelne Datenelement aufspalten muss. Dazu gibt es ein entsprechendes 'Protokoll' in meinen Daten mit Längen und Prüfsummen. Alleine schon wegen unterschiedlicher Ethernet Pakete (wir verwenden bei >1GBit/sec gerne Jumbo Frames), kann die Anzahl der write() und read() Aufrufe schon differieren.
Daniel A. schrieb: > Ja, klar, bei TCP ist das so. Aber um es noch einmal zu wiederholen, ich > habe, neben TCP Verbindungen, auch lokale per Unix Socket, mit > SOCK_SEQPACKET, und da ist das anders: Das hab ich schon verstanden, aber wenn du beides unterstützen willst, musst du deinen Code ja auch so schreiben, dass er mit beidem klar kommt. > Und UDP ist nicht zuverlässig. So würde ich es nicht sagen. Bei TCP wird halt automatisch ein Retransmit ausgelöst, wenn die Daten nicht angekommen sind, und die neue Daten werden ggf. zwischengepuffert und zurückgehalten, bis alles davor angekommen ist. Bei UDP muss man das selbst machen, wenn man es braucht. Je nach Anwendungsfall will man das nicht immer. Peter schrieb: > Ja, das ist mit schon klar, dass ich auf der Empfangsseite den Stream > wieder in einzelne Datenelement aufspalten muss. Dazu gibt es ein > entsprechendes 'Protokoll' in meinen Daten mit Längen und Prüfsummen. Prüfsummen braucht's bei TCP eigentlich nicht, weil das eh schon selber welche bildet.
Rolf M. schrieb: > Daniel A. schrieb: >> Ja, klar, bei TCP ist das so. Aber um es noch einmal zu wiederholen, ich >> habe, neben TCP Verbindungen, auch lokale per Unix Socket, mit >> SOCK_SEQPACKET, und da ist das anders: > > Das hab ich schon verstanden, aber wenn du beides unterstützen willst, > musst du deinen Code ja auch so schreiben, dass er mit beidem klar > kommt. Ja. Ich werde da unterschiedliche Handler nutzen. Ich habe da quasi:
1 | struct epoll_event { |
2 | uint32_t events; |
3 | void* data.ptr; // ---> fd_common_t* |
4 | };
|
5 | |
6 | struct fd_common_t { |
7 | void(*onevent)(uint32_t events, struct fd_common_t* ptr); |
8 | int fd; |
9 | };
|
10 | |
11 | struct client_t { |
12 | struct fd_common_t super; |
13 | struct send_queue send_queue; |
14 | ...
|
15 | };
|
16 | |
17 | struct tcp_client_t { |
18 | struct client_t super; |
19 | void* message_fragment_buffer; |
20 | ...
|
21 | };
|
22 | |
23 | struct unix_client_t { |
24 | client_t super; |
25 | ...
|
26 | };
|
Jeder tcp client kriegt eine tcp_client_t Instanz, jeder unix socket client eine unix_client_t Instanz. Dann hab ich noch fd_common_t Instanzen für die listening Sockets, eine für TCP, eine für den unix socket. Da ist dann also immer eine fd_common_t Instanz, und jeder Typ von fd kriegt seinen eigenen Handler. Einer setzt TCP Nachrichten zusammen, der andere kann sich das sparen, und dann gibt es die, die einfach nur ein accept() aufrufen, und neue Clients registrieren. Sobald ich eine Message dann vollständig empfangen / zusammengesetzt habe, wird die dann aber an die selbe Funktion übergeben. Tatsächlich wird der Aufbau noch etwas komplexer. Bei den Unix Sockets frag ich noch mit ab, welcher User sich verbunden hat (SO_PEERCRED). Der Socket wird möglicherweise mit anderen Anwendungen geteilt. Und dann sende ich noch ein unix socket pair zum Server, wo ich auch nochmal den User abfrage. Nur über den werden tatsächlich Daten ausgetauscht. Die Anwendungen können dann auch mit anderen Usern und / oder in Containern laufen. Bei den TCP Sockets wird das ganze etwas komplexer. Da habe ich vor mit Kerberos für die Authentifizierung zu arbeiten, und die Verbindung noch mit dem zwischen den Clients so ausgetauschten Keys zu verschlüsseln. Aber das kommt später. Ein Paar Mechanismen zum Streamen von Bild, Audio und Videoressourcen muss ich auch noch einbauen. Da werde ich dann vermutlich jeweils eine weitere Verbindung dafür aufmachen. Der Client sagt dem Server dann, welche Ressourcen er hat, und der Server sagt dem Client, welche davon er gerade braucht / priorisiert werden sollen. Das muss ich bei Lokal vs. Remote dann auch wieder anders handhaben. Lokal kann ich einfach die Dateien per FD sharen, oder auch auch einen Puffer zum rein zeichnen. Remote hingegen werde ich Multiplexing der Ressourcen, Ratenbegrenzte Transcodierung von Videos, und Progressives Laden von Bildern umsetzen müssen.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.