Wir machen einen Boten *, der auch im Aufzug funktioniert

* In der Tat werden wir nur den Prototyp des Protokolls schreiben.

Vielleicht sind Sie auf eine ähnliche Situation gestoßen - Sie sitzen in Ihrem Lieblingsboten, unterhalten sich mit Freunden, gehen in den Aufzug / Tunnel / Wagen, und das Internet scheint immer noch zu fangen, aber es kann nichts gesendet werden? Oder manchmal konfiguriert Ihr Kommunikationsanbieter das Netzwerk falsch und 50% der Pakete verschwinden, und es funktioniert auch nichts. Vielleicht haben Sie in diesem Moment nachgedacht - nun, Sie können wahrscheinlich irgendwie etwas tun, damit Sie bei einer schlechten Verbindung immer noch das kleine Stück Text senden können, das Sie möchten? Du bist nicht allein.

Bildquelle

In diesem Artikel werde ich über meine Idee zur Implementierung eines auf UDP basierenden Protokolls sprechen, das in dieser Situation hilfreich sein kann.

TCP / IP-Probleme


Wenn wir eine schlechte (mobile) Verbindung haben, geht ein großer Prozentsatz der Pakete verloren (oder es kommt zu einer sehr langen Verzögerung), und das TCP / IP-Protokoll kann dies als Signal dafür wahrnehmen, dass das Netzwerk überlastet ist und alles soooooooooooooob funktioniert, wenn es funktioniert im Allgemeinen. Es macht keine Freude, dass der Aufbau einer Verbindung (insbesondere TLS) das Senden und Empfangen mehrerer Pakete erfordert und selbst kleine Verluste den Betrieb sehr stark beeinträchtigen. Außerdem muss häufig auf das DNS zugegriffen werden, bevor eine Verbindung hergestellt werden kann - ein paar zusätzliche Pakete.

Alles in allem die Probleme einer typischen TCP / IP-basierten REST-API mit einer schlechten Verbindung:

  • Schlechte Reaktion auf Paketverlust (starke Geschwindigkeitsreduzierung, große Zeitüberschreitungen)
  • Das Herstellen einer Verbindung erfordert einen Paketaustausch (+3 Pakete)
  • Oft benötigen Sie eine "zusätzliche" DNS-Abfrage, um den IP-Server herauszufinden (+2 Pakete)
  • Benötigen oft TLS (mindestens +2 Pakete)

Insgesamt bedeutet dies, dass wir nur für die Verbindung zum Server 3-7 Pakete senden müssen. Bei einem hohen Prozentsatz an Verlusten kann die Verbindung viel Zeit in Anspruch nehmen, und wir haben noch nicht einmal etwas gesendet.

Implementierungsidee


Die Idee ist folgende: Wir müssen nur ein UDP-Paket mit den erforderlichen Autorisierungsdaten und dem Nachrichtentext an die vorverdrahtete IP-Adresse des Servers senden und eine Antwort darauf erhalten. Alle Daten können zusätzlich verschlüsselt werden (dies ist nicht im Prototyp enthalten). Wenn die Antwort nicht innerhalb einer Sekunde eingetroffen ist, glauben wir, dass die Anfrage verloren gegangen ist, und versuchen, sie erneut zu senden. Der Server sollte in der Lage sein, doppelte Nachrichten zu entfernen, sodass das erneute Senden keine Probleme verursachen sollte.

Mögliche Fallstricke für eine produktionsbereite Implementierung


Das Folgende sind (keineswegs alle) die Dinge, die Sie durchdenken müssen, bevor Sie so etwas unter "Kampf" -Bedingungen verwenden:

  1. UDP kann vom Anbieter "abgeschnitten" werden - Sie müssen in der Lage sein, über TCP / IP zu arbeiten
  2. UDP ist nicht NAT-freundlich - normalerweise bleibt nur wenig (~ 30 Sekunden) Zeit, um auf eine Client-Anfrage zu antworten
  3. Der Server muss resistent gegen Angriffe sein - Sie müssen sicherstellen, dass das Antwortpaket nicht mehr als das Anforderungspaket ist
  4. Die Verschlüsselung ist schwierig, und wenn Sie kein Sicherheitsexperte sind, haben Sie kaum eine Chance, sie korrekt zu implementieren
  5. Wenn Sie das Neuübertragungsintervall falsch eingestellt haben (z. B. anstatt es jede Sekunde erneut zu versuchen, ohne es erneut anzuhalten), können Sie viel schlechter abschneiden als TCP / IP
  6. Aufgrund mangelnden Feedbacks in UDP und endloser Wiederholungsversuche kann mehr Datenverkehr auf Ihren Server gelangen
  7. Der Server kann mehrere IP-Adressen haben und diese können sich im Laufe der Zeit ändern, daher müssen Sie in der Lage sein, den Cache zu aktualisieren (Telegramm funktioniert gut :))

Implementierung


Wir werden einen Server schreiben, der eine Antwort über UDP sendet und in der Antwort die Nummer der an sie gesendeten Anfrage (die Anfrage sieht aus wie "Anfrage-ts Nachrichtentext") sowie den Zeitstempel für den Empfang der Antwort senden:

//  Go. //      buf := make([]byte, maxUDPPacketSize) //   UDP addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("0.0.0.0:%d", serverPort)) conn, _ := net.ListenUDP("udp", addr) for { //   UDP,      n, uaddr, _ := conn.ReadFromUDP(buf) req := string(buf[0:n]) parts := strings.SplitN(req, " ", 2) //          curTs := time.Now().UnixNano() clientTs, _ := strconv.Atoi(parts[0]) //       -      //   conn.WriteToUDP([]byte(fmt.Sprintf("%d %d", curTs, clientTs)), uaddr) } 

Jetzt ist der schwierige Teil der Kunde. Wir senden nacheinander Nachrichten und warten, bis der Server antwortet, bevor wir die nächsten senden. Wir senden den aktuellen Zeitstempel und einen Text - der Zeitstempel dient als Anforderungskennung.

 //   addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", serverIP, serverPort)) conn, _ := net.DialUDP("udp", nil, addr) //  UDP      ,     . resCh := make(chan udpResult, 10) go readResponse(conn, resCh) for i := 0; i < numMessages; i++ { requestID := time.Now().UnixNano() send(conn, requestID, resCh) } 

Funktionscode:

 func send(conn *net.UDPConn, requestID int64, resCh chan udpResult) { for { //     ,       . conn.Write([]byte(fmt.Sprintf("%d %s", requestID, testMessageText))) if waitReply(requestID, time.After(time.Second), resCh) { return } } } //   ,  . //      ,   ,   // ,        , //   . func waitReply(requestID int64, timeout <-chan time.Time, resCh chan udpResult) (ok bool) { for { select { case res := <-resCh: if res.requestTs == requestID { return true } case <-timeout: return false } } } //    type udpResult struct { serverTs int64 requestTs int64 } //           . func readResp(conn *net.UDPConn, resCh chan udpResult) { buf := make([]byte, maxUDPPacketSize) for { n, _, _ := conn.ReadFromUDP(buf) respStr := string(buf[0:n]) parts := strings.SplitN(respStr, " ", 2) var res udpResult res.serverTs, _ = strconv.ParseInt(parts[0], 10, 64) res.requestTs, _ = strconv.ParseInt(parts[1], 10, 64) resCh <- res } } 

Dasselbe habe ich auch auf der Basis von (mehr oder weniger) Standard-REST implementiert: Mit HTTP POST senden wir dieselben RequestTs und Nachrichtentexte und warten auf eine Antwort. Dann fahren wir mit der nächsten fort. Der Einspruch wurde nach Domainnamen eingelegt, DNS-Caching war im System nicht verboten. HTTPS wurde nicht verwendet, um den Vergleich ehrlicher zu gestalten (der Prototyp enthält keine Verschlüsselung). Das Zeitlimit wurde auf 15 Sekunden festgelegt: TCP / IP leitet bereits verlorene Pakete weiter, und der Benutzer wird höchstwahrscheinlich nicht länger als 15 Sekunden warten.

Testen, Ergebnisse


Beim Testen des Prototyps wurden die folgenden Dinge gemessen (alle in Millisekunden ):

  1. Erste Antwortzeit (zuerst)
  2. Durchschnittliche Reaktionszeit (Durchschnitt)
  3. Maximale Reaktionszeit (max)
  4. H / U - Verhältnis „HTTP-Zeit“ / „UDP-Zeit“ - wie viel weniger Verzögerung bei Verwendung von UDP

Es wurden 100 Serien von 10 Anfragen gestellt - wir simulieren eine Situation, in der Sie nur wenige Nachrichten senden müssen und danach normales Internet (z. B. Wi-Fi in der U-Bahn oder 3G / LTE auf der Straße) verfügbar wird.

Getestete Kommunikationsarten:


  1. Profil "Sehr schlechtes Netzwerk" (10% Verlust, 500 ms Latenz, 1 Mbit / s) im Network Link Conditioner - "Sehr schlecht"
  2. EDGE, Telefon im Kühlschrank („Aufzug“) - Kühlschrank
  3. EDGE
  4. 3G
  5. LTE
  6. Wifi

Ergebnisse (Zeit in Millisekunden):




( Gleiches im CSV-Format )

Schlussfolgerungen


Hier sind die Schlussfolgerungen, die aus den Ergebnissen gezogen werden können:

  1. Abgesehen von der LTE-Anomalie ist der Unterschied beim Senden der ersten Nachricht umso größer, je schlechter die Verbindung ist (im Durchschnitt 2-3 mal schneller).
  2. Das anschließende Senden von Nachrichten in HTTP ist nicht viel langsamer - im Durchschnitt 1,3-mal langsamer, aber bei stabilem WLAN gibt es überhaupt keinen Unterschied
  3. Die UDP-basierte Antwortzeit ist viel stabiler, was indirekt an der maximalen Latenz zu erkennen ist - sie ist auch 1,4-1,8-mal kürzer

Mit anderen Worten, unter geeigneten („schlechten“) Bedingungen funktioniert unser Protokoll viel besser, insbesondere beim Senden der ersten Nachricht (oft ist dies alles, was gesendet werden muss).

Prototyp-Implementierung


Der Prototyp ist auf Github veröffentlicht . Verwenden Sie es nicht in der Produktion!

Der Befehl zum Starten des Clients auf dem Telefon oder Computer:
 instant-im -client -num 10 
. Der Server läuft noch :). Es ist notwendig, zuerst den Zeitpunkt der ersten Antwort sowie die maximale Verzögerung zu betrachten. Alle diese Daten werden am Ende gedruckt.

Beispiel für den Start eines Aufzugs


Source: https://habr.com/ru/post/de435026/


All Articles