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.

Cesar Gimenes

Última modificação