Nous fabriquons un messager * qui fonctionne même dans l'ascenseur

* en fait, nous n'écrirons que le prototype du protocole.

Peut-être avez-vous rencontré une situation similaire - assis dans votre messager préféré, discutant avec des amis, allez dans l'ascenseur / le tunnel / la calèche, et Internet semble toujours intercepter, mais rien ne peut être envoyé? Ou parfois, votre fournisseur de communication configure incorrectement le réseau et 50% des paquets disparaissent, et rien ne fonctionne non plus. Peut-être que vous pensiez à ce moment - eh bien, vous pouvez probablement faire quelque chose pour que, avec une mauvaise connexion, vous puissiez toujours envoyer le petit morceau de texte que vous voulez? Tu n'es pas seul.

Source d'image

Dans cet article, je vais parler de mon idée d'implémenter un protocole basé sur UDP, qui peut aider dans cette situation.

Problèmes TCP / IP


Quand nous avons une mauvaise connexion (mobile), alors un grand pourcentage de paquets commencent à être perdus (ou disparaissent avec un très long délai), et le protocole TCP / IP peut percevoir cela comme un signal que le réseau est encombré, et tout commence à fonctionner sioooooooooooooooobly si cela fonctionne en général. Cela n'ajoute pas de joie que l'établissement d'une connexion (en particulier TLS) nécessite l'envoi et la réception de plusieurs paquets, et même de petites pertes affectent très mal son fonctionnement. Il nécessite également souvent d'accéder au DNS avant d'établir une connexion - quelques paquets supplémentaires supplémentaires.

Dans l'ensemble, les problèmes d'une API REST basée sur TCP / IP typique avec une mauvaise connexion:

  • Mauvaise réponse à la perte de paquets (réduction rapide de la vitesse, grands délais d'attente)
  • L'établissement d'une connexion nécessite l'échange de paquets (+3 paquets)
  • Souvent, vous avez besoin d'une requête DNS "supplémentaire" pour trouver le serveur IP (+2 paquets)
  • Nécessite souvent TLS (+2 paquets minimum)

Au total, cela signifie que pour se connecter au serveur, nous devons envoyer 3-7 paquets, et avec un pourcentage élevé de pertes, la connexion peut prendre beaucoup de temps, et nous n'avons même pas encore envoyé quoi que ce soit.

Idée de mise en œuvre


L'idée est la suivante: nous n'avons besoin d'envoyer qu'un seul paquet UDP à l'adresse IP précâblée du serveur avec les données d'autorisation et le texte de message nécessaires, et obtenir une réponse. Toutes les données peuvent être cryptées en plus (ce n'est pas dans le prototype). Si la réponse n'est pas arrivée dans une seconde, nous pensons que la demande est perdue et essayons de la renvoyer. Le serveur doit être en mesure de supprimer les messages en double, de sorte que le renvoi ne devrait pas créer de problèmes.

Pièges possibles pour une mise en œuvre prête à la production


Voici (en aucun cas toutes) les choses auxquelles vous devez réfléchir avant d'utiliser quelque chose comme ça dans des conditions de «combat»:

  1. UDP peut être "coupé" par le fournisseur - vous devez pouvoir travailler sur TCP / IP
  2. UDP n'est pas convivial avec NAT - généralement il y a peu (~ 30 sec) de temps pour répondre à une demande du client
  3. Le serveur doit être résistant pour gagner des attaques - vous devez vous assurer que le paquet de réponse n'est pas plus que le paquet de demande
  4. Le chiffrement est difficile, et si vous n'êtes pas un expert en sécurité, vous avez peu de chances de l'implémenter correctement
  5. Si vous définissez l'intervalle de retransmission de manière incorrecte (par exemple, au lieu de réessayer toutes les secondes, de réessayer sans vous arrêter), vous pouvez faire bien pire que TCP / IP
  6. Plus de trafic peut commencer à arriver sur votre serveur en raison du manque de rétroaction dans UDP et des tentatives sans fin
  7. Le serveur peut avoir plusieurs adresses IP et elles peuvent changer au fil du temps, vous devez donc pouvoir mettre à jour le cache (Telegram fonctionne bien :))

Implémentation


Nous allons écrire un serveur qui enverra une réponse via UDP et enverra dans la réponse le numéro de la requête qui lui est parvenue (la requête ressemble à «texte du message request-ts»), ainsi que l'horodatage de la réception de la réponse:

//  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) } 

Maintenant, la partie délicate est le client. Nous enverrons les messages un par un et attendrons que le serveur réponde avant d'envoyer le suivant. Nous enverrons l'horodatage actuel et un morceau de texte - l'horodatage servira d'identifiant de demande.

 //   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) } 

Code de fonctionnalité:

 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 } } 

J'ai également implémenté la même chose sur la base de (plus ou moins) REST standard: en utilisant HTTP POST, nous envoyons les mêmes requestTs et texte de message et attendons une réponse, puis passons à la suivante. L'appel a été fait par nom de domaine, la mise en cache DNS n'était pas interdite dans le système. HTTPS n'a pas été utilisé pour rendre la comparaison plus honnête (il n'y a pas de cryptage dans le prototype). Le délai a été fixé à 15 secondes: TCP / IP a déjà la transmission des paquets perdus et l'utilisateur n'attendra probablement pas plus de 15 secondes.

Tests, résultats


Lors du test du prototype, les éléments suivants ont été mesurés (tous en millisecondes ):

  1. Premier temps de réponse (premier)
  2. Temps de réponse moyen (moyenne)
  3. Temps de réponse maximum (max)
  4. H / U - ratio «HTTP time» / «UDP time» - combien de fois moins de retard lors de l'utilisation d'UDP

100 séries de 10 demandes ont été faites - nous simulons une situation où vous n'avez besoin d'envoyer que quelques messages et après que l'Internet normal (par exemple, Wi-Fi dans le métro ou 3G / LTE dans la rue) devient disponible.

Types de communication testés:


  1. Profil «Very Bad Network» (perte de 10%, latence de 500 ms, 1 Mbps) dans Network Link Conditioner - «Very Bad»
  2. EDGE, téléphone au réfrigérateur («ascenseur») - réfrigérateur
  3. BORD
  4. 3G
  5. LTE
  6. Wifi

Résultats (temps en millisecondes):




( idem au format CSV )

Conclusions


Voici les conclusions qui peuvent être tirées des résultats:

  1. Hormis l'anomalie LTE, la différence d'envoi du premier message est d'autant plus grande que la connexion est mauvaise (en moyenne 2-3 fois plus rapide)
  2. L'envoi ultérieur de messages dans HTTP n'est pas beaucoup plus lent - en moyenne 1,3 fois plus lent, mais sur une connexion Wi-Fi stable, il n'y a aucune différence du tout
  3. Le temps de réponse basé sur UDP est beaucoup plus stable, ce qui est indirectement vu par la latence maximale - il est également 1,4 à 1,8 fois moins

En d'autres termes, dans des conditions appropriées («mauvaises»), notre protocole fonctionnera beaucoup mieux, en particulier lors de l'envoi du premier message (souvent c'est tout ce qui doit être envoyé).

Implémentation du prototype


Le prototype est posté sur github . Ne l'utilisez pas en production!

La commande pour démarrer le client sur le téléphone ou l'ordinateur:
 instant-im -client -num 10 
. Le serveur fonctionne toujours :). Il faut tout d'abord regarder au moment de la première réponse, ainsi qu'au retard maximum. Toutes ces données sont imprimées à la fin.

Exemple de lancement d'ascenseur


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


All Articles