Bate-papo no iOS: usando soquetes


Imagem criada por rawpixel.com

Nesta publicação, iremos até a camada TCP, aprenderemos sobre os soquetes e ferramentas do Core Foundation usando o exemplo do desenvolvimento de um aplicativo de bate-papo.

Tempo estimado de leitura: 25 minutos.

Por que soquetes?


Você pode estar se perguntando: "Por que devo ir um nível abaixo da URLSession ?" Se você é inteligente o suficiente e não faz essa pergunta, vá diretamente para a próxima seção.

A resposta para não tão inteligente
Ótima pergunta! O fato é que o uso do URLSession é baseado no protocolo HTTP , ou seja, a comunicação ocorre no estilo de solicitação-resposta , aproximadamente da seguinte maneira:

  • solicitar ao servidor alguns dados no formato JSON
  • obtenha esses dados, processo, exibição etc.

Mas e se precisarmos de um servidor por sua própria iniciativa para transferir dados para seu aplicativo? Aqui o HTTP está sem trabalho.

Obviamente, podemos puxar continuamente o servidor e ver se há dados para nós (também conhecido como polling ). Ou podemos ser mais sofisticados e usar pesquisas longas . Mas todas essas muletas são levemente inapropriadas neste caso.

Afinal, por que limitar-se ao paradigma solicitação-resposta, se ele se encaixa em nossa tarefa um pouco menos que nada?

Neste guia, você aprenderá como mergulhar em um nível mais baixo de abstração e usar diretamente os SOQUETES no aplicativo de bate-papo.

Em vez de verificar se há novas mensagens no servidor, nosso aplicativo usará fluxos que permanecem abertos durante a sessão de bate-papo.

Introdução


Faça o download dos materiais de origem . Há um aplicativo cliente falso e um servidor simples escrito em Go .

Você não precisa escrever no Go, mas precisará executar o aplicativo de servidor para que os aplicativos clientes possam se conectar a ele.

Inicie o aplicativo do servidor


Os materiais de origem têm um aplicativo compilado e uma fonte. Se você tem uma paranóia saudável e não confia no código compilado de outra pessoa, você mesmo pode compilar o código-fonte.

Se você for corajoso, abra o Terminal , vá para o diretório com os materiais baixados e execute o comando:

sudo ./server

Quando solicitado, digite sua senha. Depois disso, você deverá ver uma mensagem

Escutando 127.0.0.1:80.

Nota: o aplicativo do servidor inicia no modo privilegiado (o comando “sudo”) porque escuta na porta 80. Todas as portas com números inferiores a 1024 requerem acesso especial.

Seu servidor de bate-papo está pronto! Você pode ir para a próxima seção.

Se você deseja compilar o código-fonte do servidor,
Nesse caso, você precisa instalar o Go usando o Homebrew .

Se você não possui o Homebrew, é necessário instalá-lo primeiro. Abra o Terminal e cole a seguinte linha:

/usr/bin/ruby -e \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"


Em seguida, use este comando para instalar o Go:

brew install go

No final, vá para o diretório com os materiais de origem baixados e compile o código-fonte do aplicativo do servidor:

go build server.go

Por fim, você pode iniciar o servidor com o comando no início desta seção .

Analisamos o que temos no cliente


Agora abra o projeto DogeChat , compile-o e veja o que está lá.



Como você pode ver, o DogeChat agora permite que você digite um nome de usuário e vá para a própria seção de bate-papo.

Parece que o desenvolvedor deste projeto não tinha idéia de como fazer um bate-papo. Então, tudo o que temos é uma interface do usuário e navegação básicas. Vamos escrever uma camada de rede. Viva!

Crie uma sala de bate-papo


Para ir diretamente para o desenvolvimento, acesse ChatRoomViewController.swift . Este é um controlador de exibição que pode receber texto digitado pelo usuário e exibir mensagens recebidas em uma tableview.

Como temos um ChatRoomViewController , faz sentido desenvolver uma classe ChatRoom que fará todo o trabalho duro.

Vamos pensar sobre o que a nova classe fornecerá:

  • abrir uma conexão com o aplicativo do servidor;
  • conectar um usuário com o nome especificado por ele ao chat;
  • enviando e recebendo mensagens;
  • fechando a conexão no final.

Agora que sabemos o que queremos dessa classe, pressione Command-N , selecione Swift File e chame-o de ChatRoom .

Criando fluxos de E / S


Substitua o conteúdo do ChatRoom.swift por:

 import UIKit class ChatRoom: NSObject { //1 var inputStream: InputStream! var outputStream: OutputStream! //2 var username = "" //3 let maxReadLength = 4096 } 

Aqui, definimos a classe ChatRoom e declaramos as propriedades que precisamos.

  1. Primeiro, definimos os fluxos de entrada / saída. Usá-los como um par nos permitirá criar uma conexão de soquete entre o aplicativo e o servidor de bate-papo. Obviamente, enviaremos mensagens usando o fluxo de saída e receberemos o fluxo de entrada.
  2. Em seguida, definimos o nome de usuário.
  3. E, finalmente, definimos a variável maxReadLength, que limita o tamanho máximo de uma única mensagem.

Agora vá para o arquivo ChatRoomViewController.swift e adicione esta linha à lista de suas propriedades:

 let chatRoom = ChatRoom() 

Agora que criamos a estrutura básica da classe, é hora de executar a primeira das tarefas planejadas: abrir a conexão entre o aplicativo e o servidor.

Conexão aberta


Voltamos ao ChatRoom.swift e adicionamos este método para definições de propriedades:

 func setupNetworkCommunication() { // 1 var readStream: Unmanaged<CFReadStream>? var writeStream: Unmanaged<CFWriteStream>? // 2 CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, "localhost" as CFString, 80, &readStream, &writeStream) } 

Aqui está o que fazemos aqui:

  1. primeiro, definimos duas variáveis ​​para fluxos de soquete sem usar o gerenciamento automático de memória
  2. então, usando essas mesmas variáveis, criamos fluxos diretamente vinculados ao host e ao número da porta.

A função possui quatro argumentos. O primeiro é o tipo de alocador de memória que usaremos ao inicializar os threads. Você deve usar o kCFAllocatorDefault , embora existam outras opções possíveis, caso deseje alterar o comportamento dos encadeamentos.

Nota do tradutor
A documentação da função CFStreamCreatePairWithSocketToHost diz: use NULL ou kCFAllocatorDefault . E a descrição do kCFAllocatorDefault diz que é um sinônimo para NULL . O círculo está fechado!

Em seguida, definimos o nome do host. No nosso caso, estamos nos conectando ao servidor local. Se o seu servidor estiver localizado em outro local, você poderá definir seu endereço IP.

Em seguida, o número da porta que o servidor está ouvindo.

Finalmente, passamos ponteiros para nossos fluxos de E / S para que a função possa inicializá-los e conectá-los aos fluxos que cria.

Agora que temos os fluxos inicializados, podemos salvar links para eles adicionando estas linhas no final do método setupNetworkCommunication () :

 inputStream = readStream!.takeRetainedValue() outputStream = writeStream!.takeRetainedValue() 

O uso de takeRetainedValue () aplicado a um objeto não gerenciado nos permite manter uma referência a ele e, ao mesmo tempo, evitar futuros vazamentos de memória. Agora podemos usar nossos threads onde quisermos.

Agora precisamos adicionar esses threads ao loop de execução para que nosso aplicativo processe corretamente os eventos de rede. Para fazer isso, adicione essas duas linhas no final de setupNetworkCommunication () :

 inputStream.schedule(in: .current, forMode: .common) outputStream.schedule(in: .current, forMode: .common) 

Finalmente é hora de navegar! Para começar, adicione isso no final do método setupNetworkCommunication () :

 inputStream.open() outputStream.open() 

Agora temos uma conexão aberta entre nosso aplicativo cliente e servidor.

Podemos compilar e executar nosso aplicativo, mas você não verá nenhuma alteração ainda, porque enquanto não estamos fazendo nada com nossa conexão cliente-servidor.

Conectar ao chat


Agora que temos uma conexão estabelecida com o servidor, é hora de começar a fazer algo a respeito! No caso de bate-papo, você precisa se apresentar primeiro e depois pode enviar mensagens aos interlocutores.

Isso nos leva a uma conclusão importante: como temos dois tipos de mensagens, precisamos distinguir de alguma forma.

Protocolo de bate-papo


Uma das vantagens de usar a camada TCP é que podemos definir nosso próprio “protocolo” para comunicação.

Se usássemos HTTP, precisaríamos usar essas palavras diferentes GET , PUT , PATCH . Nós precisaríamos formar URLs e usar os cabeçalhos certos e tudo mais.

Temos apenas dois tipos de mensagens. Nós enviaremos

iam:Luke

para entrar no chat e se apresentar.

E nós enviaremos

msg:Hey, how goes it, man?

para enviar uma mensagem de bate-papo a todos os entrevistados.

É muito simples, mas absolutamente sem princípios, portanto, não use esse método em projetos críticos.

Agora sabemos o que nosso servidor espera e podemos escrever um método na classe ChatRoom que permitirá que o usuário se conecte ao chat. O único argumento é o apelido do usuário.

Adicione este método dentro do ChatRoom.swift :

 func joinChat(username: String) { //1 let data = "iam:\(username)".data(using: .utf8)! //2 self.username = username //3 _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } //4 outputStream.write(pointer, maxLength: data.count) } } 

  1. Primeiro, formamos nossa mensagem usando nosso próprio “protocolo”
  2. Salve o nome para referência futura.
  3. withUnsafeBytes (_ :) fornece uma maneira conveniente de trabalhar com um ponteiro inseguro dentro de um fechamento.
  4. Finalmente, enviamos nossa mensagem para o fluxo de saída. Isso pode parecer mais complicado do que você imagina, no entanto, write (_: maxLength :) usa o ponteiro não seguro criado na etapa anterior.

Agora nosso método está pronto, abra o ChatRoomViewController.swift e adicione uma chamada a esse método no final do viewWillAppear (_ :) .

 chatRoom.joinChat(username: username) 

Agora compile e execute o aplicativo. Digite seu apelido e toque em retornar para ver ...



... que novamente nada mudou!

Espere, está tudo bem! Vá para a janela do terminal. Lá você verá a mensagem que Vasya se juntou ou algo parecido se o seu nome não for Vasya.

É ótimo, mas seria bom ter uma indicação de uma conexão bem-sucedida na tela do seu telefone.

Respondendo a mensagens recebidas


O servidor envia mensagens de ingresso do cliente para todos que estão no bate-papo, incluindo você. Felizmente, nosso aplicativo já tem tudo para exibir as mensagens recebidas na forma de células na tabela de mensagens no ChatRoomViewController .

Tudo o que você precisa fazer é usar o inputStream para "capturar" essas mensagens, convertê-las em instâncias da classe Message e passá-las para a tabela para exibição.

Para poder responder às mensagens recebidas, você precisa do ChatRoom para estar em conformidade com o protocolo StreamDelegate .

Para fazer isso, adicione esta extensão na parte inferior do arquivo ChatRoom.swift :

 extension ChatRoom: StreamDelegate { } 

Agora declare quem se tornará um delegado para inputStream.

Adicione esta linha ao método setupNetworkCommunication () antes das chamadas para agendar (em: forMode :):

 inputStream.delegate = self 

Agora adicione a implementação do método stream (_: handle :) à extensão:

 func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .hasBytesAvailable: print("new message received") case .endEncountered: print("The end of the stream has been reached.") case .errorOccurred: print("error occurred") case .hasSpaceAvailable: print("has space available") default: print("some other event...") } } 

Processamos mensagens recebidas


Portanto, estamos prontos para começar a processar as mensagens recebidas. O evento que nos interessa é .hasBytesAvailable , que indica que uma mensagem recebida chegou.

Escreveremos um método que processa essas mensagens. Abaixo do método recém-adicionado, escrevemos o seguinte:

 private func readAvailableBytes(stream: InputStream) { //1 let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength) //2 while stream.hasBytesAvailable { //3 let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength) //4 if numberOfBytesRead < 0, let error = stream.streamError { print(error) break } // Construct the Message object } } 

  1. Definimos o buffer no qual iremos ler os bytes recebidos.
  2. Giramos em loop, enquanto no fluxo de entrada há algo para ler.
  3. Chamamos read (_: maxLength :), que lê os bytes do fluxo e os coloca no buffer.
  4. Se a chamada retornou um valor negativo, retornamos um erro e saímos do loop.

Precisamos chamar esse método assim que tivermos dados no fluxo de entrada; portanto, vá para a instrução switch dentro do método stream (_: handle :) , localize a opção .hasBytesAvailable e chame esse método imediatamente após a instrução print:

 readAvailableBytes(stream: aStream as! InputStream) 

Neste local, temos um buffer preparado de dados recebidos!

Mas ainda precisamos transformar esse buffer no conteúdo da tabela de mensagens.

Coloque esse método em readAvailableBytes (stream :) .

 private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>, length: Int) -> Message? { //1 guard let stringArray = String( bytesNoCopy: buffer, length: length, encoding: .utf8, freeWhenDone: true)?.components(separatedBy: ":"), let name = stringArray.first, let message = stringArray.last else { return nil } //2 let messageSender: MessageSender = (name == self.username) ? .ourself : .someoneElse //3 return Message(message: message, messageSender: messageSender, username: name) } 

Primeiro, inicializamos String usando o buffer e o tamanho que passamos para esse método.

O texto estará em UTF-8; no final, liberaremos o buffer e dividiremos a mensagem pelo símbolo ':' para separar o nome do remetente e a própria mensagem.

Agora estamos analisando se essa mensagem veio de outro participante. No produto, você pode criar algo como um token exclusivo, isso é suficiente para a demonstração.

Finalmente, de toda essa economia, formamos uma instância da Mensagem e a devolvemos.

Para usar esse método, adicione o seguinte if-let no final do loop while no método readAvailableBytes (stream :) , imediatamente após o último comentário:

 if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) { // Notify interested parties } 

Agora tudo está pronto para passar para alguém Mensagem ... Mas para quem?

Crie o protocolo ChatRoomDelegate


Portanto, precisamos informar o ChatRoomViewController.swift sobre a nova mensagem, mas não temos um link para ela. Como ele contém um link forte do ChatRoom , podemos cair na armadilha de um ciclo de link forte.

Este é o lugar perfeito para criar um protocolo de delegação. O ChatRoom não se importa com quem precisa saber sobre novas postagens.

Na parte superior do ChatRoom.swift, adicione uma nova definição de protocolo:

 protocol ChatRoomDelegate: class { func received(message: Message) } 

Agora, dentro da classe ChatRoom, adicione um link fraco para armazenar quem se tornará o delegado:

 weak var delegate: ChatRoomDelegate? 

Agora vamos adicionar o método readAvailableBytes (stream :) , adicionando a seguinte linha dentro da construção if-let, sob o último comentário no método:

 delegate?.received(message: message) 

Volte para ChatRoomViewController.swift e adicione a seguinte extensão de classe, que garante a conformidade com o protocolo ChatRoomDelegate , imediatamente após MessageInputDelegate:

 extension ChatRoomViewController: ChatRoomDelegate { func received(message: Message) { insertNewMessageCell(message) } } 

O projeto original já contém o necessário, portanto, insertNewMessageCell (_ :) aceitará sua mensagem e exibirá a célula correta na visualização da tabela.

Agora atribua o controlador de exibição como um delegado adicionando-o a viewWillAppear (_ :) imediatamente após chamar super.viewWillAppear ()

 chatRoom.delegate = self 

Agora compile e execute o aplicativo. Digite um nome e toque em retornar.



Você verá uma célula sobre sua conexão com o bate-papo. Hooray, você enviou com sucesso uma mensagem para o servidor e recebeu uma resposta dele!

Postagem de mensagens


Agora que o ChatRoom pode enviar e receber mensagens, é hora de fornecer ao usuário a capacidade de enviar suas próprias frases.

No ChatRoom.swift, adicione o seguinte método no final da definição de classe:

 func send(message: String) { let data = "msg:\(message)".data(using: .utf8)! _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } outputStream.write(pointer, maxLength: data.count) } } 

Esse método é semelhante ao joinChat (nome de usuário :) , que escrevemos anteriormente, exceto que ele possui o prefixo msg na frente do texto (para indicar que esta é uma mensagem de bate-papo real).

Como queremos enviar mensagens pelo botão Enviar , retornamos ao ChatRoomViewController.swift e encontramos o MessageInputDelegate lá.

Aqui vemos o método sendWasTapped (message :) vazio. Para enviar uma mensagem, envie-a para o chatRoom:

 chatRoom.send(message: message) 

Na verdade, isso é tudo! Como o servidor receberá a mensagem e a encaminhará a todos, o ChatRoom será notificado sobre a nova mensagem da mesma maneira que ao ingressar no chat.

Compile e execute o aplicativo.



Se você não tem ninguém com quem conversar agora, abra uma nova janela do terminal e digite:

nc localhost 80

Isso conectará você ao servidor. Agora você pode se conectar ao chat usando o mesmo "protocolo":

iam:gregg

E assim - envie uma mensagem:

msg:Ay mang, wut's good?



Parabéns, você escreveu um cliente para o chat!

Nos limpamos


Se você já desenvolveu aplicativos que lêem / gravam arquivos ativamente, deve saber que bons desenvolvedores fecham arquivos quando terminam de trabalhar com eles. O fato é que a conexão através do soquete é fornecida pelo descritor de arquivo. Isso significa que, após a conclusão do trabalho, você precisa fechá-lo, como qualquer outro arquivo.

Para fazer isso, adicione o seguinte método ao ChatRoom.swift após definir send (message :) :

 func stopChatSession() { inputStream.close() outputStream.close() } 

Como você provavelmente adivinhou, esse método fecha os threads para que você não possa mais receber e enviar mensagens. Além disso, os threads são removidos do loop de execução em que os colocamos anteriormente.

Adicione uma chamada a esse método na seção .endEncountered da instrução switch dentro do fluxo (_: handle :) :

 stopChatSession() 

Volte para ChatRoomViewController.swift e faça o mesmo em viewWillDisappear (_ :) :

 chatRoom.stopChatSession() 

Isso é tudo! Agora com certeza!

Conclusão


Agora que você já domina os conceitos básicos de rede com soquetes, pode aprofundar seu conhecimento.

Soquetes UDP


Este aplicativo é um exemplo de comunicação de rede usando TCP, o que garante a entrega de pacotes ao destino.

No entanto, você pode usar soquetes UDP. Esse tipo de conexão não garante a entrega de pacotes para a finalidade pretendida, mas é muito mais rápido.

Isso é especialmente útil em jogos. Já experimentou um atraso? Isso significava que você tinha uma conexão ruim e muitos pacotes UDP foram perdidos.

Websockets


Outra alternativa ao HTTP em aplicativos é uma tecnologia chamada soquetes da web.

Diferentemente dos soquetes TCP comuns, os soquetes da Web usam HTTP para estabelecer comunicação. Com a ajuda deles, você pode obter o mesmo que os soquetes comuns, mas com conforto e segurança, como em um navegador.

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


All Articles