Cliente e servidor socket em Golang.
No grupo de estudos de Go, surgiu uma dúvida sobre como escrever um cliente persistente que se reconecte a um servidor socket quando a conexão falhar.
A principal dificuldade das pessoas parece ser entender o conceito de máquina de estados. Por isso, criaremos alguns exemplos de um cliente persistente, começando por uma máquina de estados simples, sem goroutines nem canais.
Antes disso, criaremos um pequeno servidor socket para nosso cliente se conectar: o famoso echo server.
Servidor Socket
package main
import (
"bufio"
"fmt"
"io"
"net"
)
func handler(conn net.Conn) {
for {
m, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Println("Connection closed")
conn.Close()
return
}
fmt.Println("Error reading from connection", err)
return
}
_, err = conn.Write([]byte(m))
if err != nil {
fmt.Println("Error writing to connection")
return
}
fmt.Printf("%v %q\n", conn.RemoteAddr(), m)
}
}
func main() {
fmt.Println("Listening on port 8080")
ln, _ := net.Listen("tcp", ":8080")
for {
conn, _ := ln.Accept()
fmt.Println("Connection accepted")
go handler(conn)
}
}
Nosso servidor tem uma tarefa simples: escutar na porta 8080 por conexões. Quando uma conexão chega, ele a aceita e chama o handler, que trata a conexão. Cada handler roda em uma goroutine, permitindo aceitar múltiplas conexões simultâneas.
O handler recebe dados da conexão até encontrar um enter (\n
) e envia de volta ao cliente o que foi recebido. Se ocorrer um EOF, o handler fecha a conexão e finaliza a goroutine.
O servidor serve apenas como ponto de conexão para o cliente. Agora, veremos como criar um cliente persistente.
Cliente Socket
package main
import (
"bufio"
"fmt"
"net"
"os"
"time"
)
func main() {
RECONNECT:
for {
fmt.Println("Connecting to server...")
conn, err := net.Dial("tcp", ":8080")
if err != nil {
fmt.Println(err)
time.Sleep(time.Second * 1)
continue
}
fmt.Println("Connection accepted")
for {
var m string
fmt.Print("> ")
reader := bufio.NewReader(os.Stdin)
m, err = reader.ReadString('\n')
if err != nil {
fmt.Println(err)
continue RECONNECT
}
fmt.Printf("Sending: %q\n", m)
_, err = conn.Write([]byte(m + "\n"))
if err != nil {
fmt.Println(err)
continue RECONNECT
}
reader = bufio.NewReader(conn)
m, err = reader.ReadString('\n')
if err != nil {
fmt.Println(err)
continue RECONNECT
}
fmt.Printf("Received: %q\n", m)
}
}
}
Este primeiro cliente é simples, mas persistente. Ele utiliza uma única máquina de estados, sem goroutines nem canais. Por ser um exemplo didático, há repetição de código.
O cliente possui dois loops aninhados, funcionando como uma máquina de estados. Quando o programa inicia, está no estado desconectado.
Ele tenta conectar ao servidor. Se a conexão falhar, espera um segundo e reinicia o loop usando continue
.
Cliente Conectado
Se a conexão for bem-sucedida, o cliente entra no estado conectado e inicia o loop principal, mudando para o estado lendo mensagem do terminal. Nesse estado, espera o usuário digitar uma mensagem e pressionar enter.
Em seguida, passa para o estado enviando, onde envia a mensagem para o servidor.
Se tudo ocorrer bem, muda para o estado lendo mensagem do servidor, que lê a resposta do servidor e a exibe na tela.
Persistindo a Conexão
Se ocorrer algum erro em qualquer um desses estados, o cliente registra o erro e reinicia o primeiro loop, voltando ao estado desconectado. Para isso, utiliza continue
com o label RECONNECT
. Sem o label, o continue
reiniciaria apenas o loop interno.
Este cliente é intencionalmente simples. Sua máquina de estados não se ramifica. Ele realiza uma tarefa por vez: ao ler do terminal, não lê do servidor ou tenta reconectar, e assim por diante.
Código-Fonte
Aqui estão os códigos-fonte do nosso servidor e cliente:
Vídeos com Explicação
Conclusão
Espero que este tutorial tenha esclarecido dúvidas sobre máquinas de estado e clientes persistentes. Esta versão foi simplificada propositalmente. Em breve, abordarei este problema com recursos mais avançados.