Criamos um messenger * que funciona mesmo no elevador

* de fato, escreveremos apenas o protótipo do protocolo.

Talvez você tenha encontrado uma situação semelhante - sentado em seu mensageiro favorito, conversando com amigos, entrando no elevador / túnel / carruagem e a Internet ainda pareça travar, mas nada pode ser enviado? Ou, às vezes, seu provedor de comunicação configura incorretamente a rede e 50% dos pacotes desaparecem e nada funciona. Talvez você estivesse pensando neste momento - bem, você provavelmente pode, de alguma maneira, fazer algo para que, com uma conexão ruim, ainda possa enviar o pequeno pedaço de texto que deseja? Você não está sozinho.

Fonte da imagem

Neste artigo, falarei sobre minha ideia de implementar um protocolo baseado em UDP, que pode ajudar nessa situação.

Problemas de TCP / IP


Quando temos uma conexão ruim (móvel), uma grande porcentagem de pacotes começa a ser perdida (ou passa com um atraso muito longo), e o protocolo TCP / IP pode perceber isso como um sinal de que a rede está congestionada e tudo começa a funcionar muuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuhupp em geral Não é uma alegria que o estabelecimento de uma conexão (especialmente TLS) exija o envio e recebimento de vários pacotes, e até pequenas perdas afetam muito sua operação. Também requer acesso ao DNS antes de estabelecer uma conexão - mais alguns pacotes extras.

Em suma, os problemas de uma API REST típica baseada em TCP / IP com uma conexão ruim:

  • Má resposta à perda de pacotes (redução acentuada de velocidade, grandes tempos limite)
  • Estabelecer uma conexão requer troca de pacotes (+3 pacotes)
  • Muitas vezes, você precisa de uma consulta DNS "extra" para descobrir o servidor IP (+2 pacotes)
  • Freqüentemente precisa de TLS (+2 pacotes no mínimo)

No total, isso significa que apenas para conectar ao servidor precisamos enviar de 3 a 7 pacotes e, com uma alta porcentagem de perdas, a conexão pode levar uma quantidade significativa de tempo e ainda nem enviamos nada.

Ideia de implementação


A idéia é a seguinte: precisamos enviar apenas um pacote UDP para o endereço IP pré-conectado do servidor com os dados de autorização e o texto da mensagem necessários e obter uma resposta. Todos os dados podem ser criptografados adicionalmente (isso não está no protótipo). Se a resposta não chegar dentro de um segundo, acreditamos que a solicitação foi perdida e tentamos enviá-la novamente. O servidor deve poder remover mensagens duplicadas, portanto, reenviar não deve criar problemas.

Possíveis armadilhas para implementação pronta para produção


A seguir, são (de forma alguma todas) as coisas que você precisa pensar antes de usar algo parecido com isto em condições de "combate":

  1. O UDP pode ser "cortado" pelo provedor - você precisa trabalhar com TCP / IP
  2. O UDP não é amigável com o NAT - geralmente há pouco (~ 30 segundos) tempo para responder a uma solicitação do cliente
  3. O servidor deve ser resistente a ataques - você precisa garantir que o pacote de resposta não seja mais do que o pacote de solicitação
  4. A criptografia é difícil e, se você não é especialista em segurança, tem poucas chances de implementá-la corretamente
  5. Se você definir o intervalo de retransmissão incorretamente (por exemplo, em vez de tentar novamente a cada segundo, tentar novamente sem parar), poderá fazer muito pior que o TCP / IP
  6. Mais tráfego pode começar a chegar ao servidor devido à falta de feedback no UDP e a inúmeras tentativas
  7. O servidor pode ter vários endereços IP e eles podem mudar com o tempo, portanto, você precisa atualizar o cache (o Telegram funciona bem :))

Implementação


Escreveremos um servidor que enviará uma resposta via UDP e enviaremos na resposta o número da solicitação que chegou a ela (a solicitação se parece com "request-ts message text"), bem como o carimbo de data e hora para receber a resposta:

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

Agora a parte complicada é o cliente. Enviaremos mensagens uma de cada vez e aguardaremos a resposta do servidor antes de enviar a próxima. Enviaremos o carimbo de data / hora atual e um texto - o carimbo de data / hora servirá como identificador da solicitação.

 //   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 do Recurso:

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

Também implementei a mesma coisa com base no (mais ou menos) padrão REST: usando HTTP POST, enviamos os mesmos requestTs e mensagem de texto e aguardamos uma resposta, depois passamos para a próxima. A apelação foi feita pelo nome de domínio, o cache do DNS não foi proibido no sistema. HTTPS não foi usado para tornar a comparação mais honesta (não há criptografia no protótipo). O tempo limite foi definido em 15 segundos: o TCP / IP já tem o encaminhamento de pacotes perdidos e o usuário provavelmente não esperará muito mais do que 15 segundos.

Teste, Resultados


Ao testar o protótipo, foram medidos os seguintes itens (todos em milissegundos ):

  1. Tempo de primeira resposta (primeiro)
  2. Tempo médio de resposta (média)
  3. Tempo máximo de resposta (máximo)
  4. H / U - proporção "tempo HTTP" / "tempo UDP" - quantas vezes menos atraso ao usar UDP

Foram feitas 100 séries de 10 solicitações - simulamos uma situação em que você precisa enviar apenas algumas mensagens e depois que a Internet normal (por exemplo, Wi-Fi no metrô ou 3G / LTE na rua) fica disponível.

Tipos de comunicação testados:


  1. Perfil “Rede muito ruim” (perda de 10%, latência de 500 ms, 1 Mbps) no Condicionador de link de rede - “Muito ruim”
  2. EDGE, telefone na geladeira (“elevador”) - geladeira
  3. EDGE
  4. 3G
  5. LTE
  6. Wifi

Resultados (tempo em milissegundos):




(o mesmo no formato CSV )

Conclusões


Aqui estão as conclusões que podem ser extraídas dos resultados:

  1. Além da anomalia do LTE, a diferença no envio da primeira mensagem é quanto maior, pior a conexão (em média, 2-3 vezes mais rápido)
  2. O envio subsequente de mensagens em HTTP não é muito mais lento - em média 1,3 vezes mais lento, mas em Wi-Fi estável, não há diferença alguma
  3. O tempo de resposta baseado em UDP é muito mais estável, o que é indiretamente visto pela latência máxima - também é 1,4-1,8 vezes menos

Em outras palavras, sob condições apropriadas ("ruins"), nosso protocolo funcionará muito melhor, especialmente ao enviar a primeira mensagem (geralmente isso é tudo o que precisa ser enviado).

Implementação de protótipo


O protótipo é publicado no github . Não use em produção!

O comando para iniciar o cliente no telefone ou computador:
 instant-im -client -num 10 
. O servidor ainda está em execução :). É necessário olhar antes de tudo no momento da primeira resposta, bem como no atraso máximo. Todos esses dados são impressos no final.

Exemplo de Lançamento do Elevador


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


All Articles