Imagem criada por rawpixel.comNesta 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 {
Aqui, definimos a classe
ChatRoom e declaramos as propriedades que precisamos.
- 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.
- Em seguida, definimos o nome de usuário.
- 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() {
Aqui está o que fazemos aqui:
- primeiro, definimos duas variáveis para fluxos de soquete sem usar o gerenciamento automático de memória
- 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 tradutorA 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) {
- Primeiro, formamos nossa mensagem usando nosso próprio “protocolo”
- Salve o nome para referência futura.
- withUnsafeBytes (_ :) fornece uma maneira conveniente de trabalhar com um ponteiro inseguro dentro de um fechamento.
- 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) {
- Definimos o buffer no qual iremos ler os bytes recebidos.
- Giramos em loop, enquanto no fluxo de entrada há algo para ler.
- Chamamos read (_: maxLength :), que lê os bytes do fluxo e os coloca no buffer.
- 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? {
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) {
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.