Hacemos un messenger * que funciona incluso en el elevador

* de hecho, solo escribiremos el prototipo del protocolo.

Tal vez haya encontrado una situación similar: siéntese en su mensajero favorito, chatee con amigos, vaya al elevador / túnel / carro, e Internet aún parece atraparse, pero no funciona enviar. O a veces su proveedor de comunicación configura incorrectamente la red y el 50% de los paquetes desaparecen, y nada funciona tampoco. Tal vez estaba pensando en ese momento, bueno, ¿probablemente puede hacer algo de alguna manera para que con una mala conexión aún pueda enviar ese pequeño texto que desea? No estas solo

Fuente de imagen

En este artículo hablaré sobre mi idea para implementar un protocolo basado en UDP, que puede ayudar en esta situación.

Problemas de TCP / IP


Cuando tenemos una conexión pobre (móvil), un gran porcentaje de paquetes comienzan a perderse (o se demoran mucho), y el protocolo TCP / IP puede percibir esto como una señal de que la red está congestionada, y todo comienza a funcionar muuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu si, si funciona. en general No agrega alegría que el establecimiento de una conexión (especialmente TLS) requiera el envío y la recepción de varios paquetes, e incluso pequeñas pérdidas afectan su funcionamiento muy gravemente. También a menudo requiere acceder al DNS antes de establecer una conexión, un par más de paquetes adicionales.

En general, los problemas de una API REST basada en TCP / IP típica con una conexión deficiente:

  • Mala respuesta a la pérdida de paquetes (reducción brusca de la velocidad, grandes tiempos de espera)
  • Establecer una conexión requiere el intercambio de paquetes (+3 paquetes)
  • A menudo necesita una consulta DNS "extra" para averiguar el servidor IP (+2 paquetes)
  • A menudo necesita TLS (+2 paquetes mínimo)

En total, esto significa que solo para conectarnos al servidor necesitamos enviar de 3 a 7 paquetes, y con un alto porcentaje de pérdidas, la conexión puede tomar una cantidad de tiempo considerable, y aún no hemos enviado nada.

Idea de implementación


La idea es esta: solo necesitamos enviar un paquete UDP a la dirección IP precableada del servidor con los datos de autorización y el texto del mensaje necesarios, y obtener una respuesta. Todos los datos se pueden cifrar adicionalmente (esto no está en el prototipo). Si la respuesta no ha llegado en un segundo, creemos que la solicitud se ha perdido y tratamos de enviarla nuevamente. El servidor debería poder eliminar mensajes duplicados, por lo que reenviar no debería crear problemas.

Posibles dificultades para la implementación lista para producción


Las siguientes son (de ninguna manera todas) las cosas que necesita pensar antes de usar algo como esto en condiciones de "combate":

  1. UDP puede ser "cortado" por el proveedor: debe poder trabajar a través de TCP / IP
  2. UDP no es compatible con NAT; por lo general, hay poco tiempo (~ 30 segundos) para responder a una solicitud del cliente
  3. El servidor debe ser resistente para obtener ataques : debe asegurarse de que el paquete de respuesta no sea más que el paquete de solicitud
  4. El cifrado es difícil, y si no eres un experto en seguridad, tienes pocas posibilidades de implementarlo correctamente
  5. Si configura el intervalo de retransmisión incorrectamente (por ejemplo, en lugar de intentarlo de nuevo cada segundo, intente nuevamente sin detenerse), entonces puede hacerlo mucho peor que TCP / IP
  6. Es posible que comience a llegar más tráfico a su servidor debido a la falta de retroalimentación en UDP y reintentos interminables
  7. El servidor puede tener varias direcciones IP y pueden cambiar con el tiempo, por lo que debe poder actualizar el caché (Telegram funciona bien :))

Implementación


Escribiremos un servidor que enviará una respuesta a través de UDP y enviaremos en la respuesta el número de la solicitud que recibió (la solicitud se ve como "texto de mensaje de solicitud-ts"), así como la marca de tiempo para recibir la respuesta:

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

Ahora la parte difícil es el cliente. Enviaremos mensajes de uno en uno y esperaremos a que el servidor responda antes de enviar el siguiente. Le enviaremos la marca de tiempo actual y un fragmento de texto; la marca de tiempo servirá como identificador de la solicitud.

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

Código de característica:

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

También implementé lo mismo sobre la base de (más o menos) REST estándar: usando HTTP POST enviamos las mismas solicitudes y texto de mensaje y esperamos una respuesta, luego pasamos a la siguiente. La apelación se realizó por nombre de dominio, el almacenamiento en caché de DNS no estaba prohibido en el sistema. HTTPS no se usó para hacer la comparación más honesta (no hay cifrado en el prototipo). El tiempo de espera se estableció en 15 segundos: TCP / IP ya tiene reenvío de paquetes perdidos, y el usuario probablemente no esperará mucho más de 15 segundos.

Pruebas, resultados


Al probar el prototipo, se midieron las siguientes cosas (todas en milisegundos ):

  1. Primer tiempo de respuesta (primero)
  2. Tiempo de respuesta promedio (promedio)
  3. Tiempo máximo de respuesta (max)
  4. H / U - relación "tiempo HTTP" / "tiempo UDP" - cuántas veces menos retraso al usar UDP

Se realizaron 100 series de 10 solicitudes: simulamos una situación en la que necesita enviar solo unos pocos mensajes y después de eso Internet normal (por ejemplo, Wi-Fi en el metro o 3G / LTE en la calle) está disponible.

Tipos de comunicación probados:


  1. Perfil "Red muy mala" (pérdida del 10%, latencia de 500 ms, 1 Mbps) en el acondicionador de enlace de red - "Muy mala"
  2. EDGE, teléfono en la nevera ("ascensor") - nevera
  3. EDGE
  4. 3G
  5. LTE
  6. Wifi

Resultados (tiempo en milisegundos):




(lo mismo en formato CSV )

Conclusiones


Estas son las conclusiones que se pueden extraer de los resultados:

  1. Además de la anomalía LTE, la diferencia en el envío del primer mensaje es mayor, peor es la conexión (en promedio, 2-3 veces más rápido)
  2. El envío posterior de mensajes en HTTP no es mucho más lento, en promedio 1.3 veces más lento, pero en Wi-Fi estable no hay ninguna diferencia
  3. El tiempo de respuesta basado en UDP es mucho más estable, lo que se ve indirectamente por la latencia máxima: también es 1.4-1.8 veces menos

En otras palabras, bajo condiciones apropiadas ("malas"), nuestro protocolo funcionará mucho mejor, especialmente cuando se envía el primer mensaje (a menudo, esto es todo lo que debe enviarse).

Implementación de prototipo


El prototipo se publica en github . ¡No lo uses en producción!

El comando para iniciar el cliente en el teléfono o la computadora:
 instant-im -client -num 10 
. El servidor aún se está ejecutando :). Es necesario mirar primero a la hora de la primera respuesta, así como al retraso máximo. Todos estos datos se imprimen al final.

Ejemplo de lanzamiento de elevador


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


All Articles