[Cliente e servidor socket em Golang.]

Surgiu uma dúvida no grupo de estudos de Go de como escrever um cliente persistente que se reconectasse a um servidor socket quando por algum motivo a conexão falhar.

A maior dificuldade das pessoas parece ser com o conceito de máquina de estados, então criaremos alguns exemplos de como seria um cliente persistente começando por uma máquina de estados simples sem goroutines nem canais.

Mas antes 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)
	}
}

A tarefa do nosso servidor é bem simples, ele fica escutando na porta 8080 por conexões, quando uma conexão chega ele aceita e imediatamente chama o handler que fica tratando a conexão. Cada handler é um goroutine de forma que podemos aceitar multiplas conexões simultâneas.

A tarefa do handler é ficar recebendo tudo que vem da conexão esperando por um enter (\n) e então escrever o que foi recebido de volta para o cliente. Caso venha um EOF, o handler termina e fecha a conexão e termina a goroutine.

O servidor está aqui apenas para termos um lugar para o cliente conectar, 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)
		}
	}
}

Esse primeiro cliente é mais simples, mas já é persistente, propositalmente fiz tudo em uma única máquina de estados sem goroutines nem canais e como é um exemplo para ser didático não me importei com alguma repetição de código.

O cliente basicamente consiste em dois loops aninhados, mas pense nisso como uma máquina de estados, quando o programa inicia estamos no estado desconectado

Então fazemos uma tentativa de conexão ao servidor, se a conexão falhar esperamos um segundo e saltamos novamente para o início do loop usando a palavra reservada continue.

Cliente conectado

Se a conexão for bem sucedida, então estamos no estado conectado e entramos no loop principal e imediatamente estamos no estado lendo mensagem do terminal que fica esperando o usuário digitar uma mensagem e apertar o enter.

Então vamos para o estado enviando que enviar a mensagem para o servidor.

Se tudo correr bem então vamos para o estado lendo mensagem do servidor que fica que lê a resposta do servidor e imprime na tela.

Persistindo a conexão

Se algo sair errado em qualquer uma desses estados logamos o erro e saltamos para o início do primeiro loop que nos coloca no estado desconectado novamente. Para isso usamos a palavra reservada continue, mas dessa vez com um label RECONNECT, pois a se usarmos apenas continue sem o label o salto seria para o início do loop mais interno.

Propositalmente esse cliente é bem simples, a maquina de estados dele nunca se ramifica. Ele faz sempre uma coisa por vez, ou seja, quando está em um estado, não está fazendo nenhuma das outras tarefas, se esta lendo do terminal, não está lendo do servidor, não está tentando se reconectar, etc., só faz uma coisa por vez.

Código-fonte

Aqui vai o código-fonte do nosso servidor e cliente.

Vídeos com explicação

Conclusão

Espero que esse pequeno tutorial tenha ajudado a esclarecer algumas dúvidas sobre máquinas de estado e clientes persistentes. Essa versão foi propositalmente mais simplificada, abordarei esse problema com alguns recursos mais sofisticados.

Cesar Gimenes