Cliente e servidor socket em Golang com goroutines e canais.

Dando continuidade ao exemplo sobre cliente socket persistente em Golang fiz algumas alterações no código-fonte para ilustrar como melhorar nosso código para ser assíncrono e para isso usaremos goroutines e canais.

Goroutines e Canais

Goroutines e Canais são recursos sofisticados da linguagem Go, goroutines são threads leves, gerenciadas pelo próprio runtime da linguagem e canais é como o Go se comunica entre processos. 

No nosso caso usaremos para comunicação entre as goroutines do nosso programa.

Esses dois recursos são um bocado avançados e esse foi o motivo de eu primeiro demonstrar como fazer o nosso pequeno cliente socket sem esses recursos. 

Na verdade, é uma ótima prática primeiro resolver o problema da forma mais simples possível e só adicionar goroutines e canais se forem realmente necessários.  São fáceis de usar, mas, vem com um preço, seu programa ficará bem mais difícil de depurar.

Analisando o código-fonte

Lendo do standard input

Uma melhoria é que eu também separei código em pequenas funções, isso deve facilitar a leitura.

Primeiro a função responsável por ler as entradas do usuário.

func readStdin() {
	for {
		reader := bufio.NewReader(os.Stdin)
		m, err := reader.ReadString('\n')
		if err != nil {
			panic(err)
		}
		input <- m
	}
}

Essa função fica em um loop infinito lendo entradas vindas do usuário, quando o usuário pressiona enter a função envia a string para o canal input. Caso aconteça algum erro na leitura de stdin não fazemos nenhum tratamento de erro além de fechar o programa com um panic.

Lendo da rede

A função que lê da conexão de rede é muito parecida com a função que lê as entradas do usuário. As únicas diferenças são de onde estão efetuando a leitura, para qual canal estão escrevendo e também e no caso da função a seguir se um erro acontecer no lugar de simplesmente sair do programa com um panic enviamos o erro para o canal errorChan.

func readConn(conn net.Conn) {
	for {
		reader := bufio.NewReader(conn)
		m, err := reader.ReadString('\n')
		if err != nil {
			errorChan <- err
			return
		}
		output <- m
	}
}

Gerenciando a conexão

Finalmente temos uma função para gerenciar a conexão, ela fica em loop tentando conectar ao servidor e quando uma conexão é estabelecida ela retorna a conexão para usarmos nas outras partes do sistema.

É praticamente a mesma rotina que efetuávamos no programa anterior só que separada agora  em uma função.

func connect() net.Conn {
	var (
		conn net.Conn
		err  error
	)
	for {
		fmt.Println("Connecting to server...")
		conn, err = net.Dial("tcp", ":8080")
		if err == nil {
			break
		}
		fmt.Println(err)
		time.Sleep(time.Second * 1)
	}
	fmt.Println("Connection accepted")
	return conn
}

Gerenciando os canais

Para a função main, restou a tarefa de carregar as goroutines e gerenciar os canais. Para isso usamos a palavra-chave select que é muito parecida com switch case, mas no caso de select serve para esperar o retorno de múltiplos canais.

func main() {

	go readStdin()

RECONNECT:
	for {
		conn := connect()

		go readConn(conn)

		for {
			select {
			case m := <-output:
				fmt.Printf("Received: %q\n", m)

			case m := <-input:
				fmt.Printf("Sending: %q\n", m)
				_, err := conn.Write([]byte(m + "\n"))
				if err != nil {
					fmt.Println(err)
					conn.Close()
					continue RECONNECT
				}
			case err := <-errorChan:
				fmt.Println("Error:", err)
				conn.Close()
				continue RECONNECT
			}
		}
	}
}

Basicamente ficamos em loop esperando pelo retorno de vários canais e tratamos o que vir nesses canais usando a rotina apropriada.

Código-fonte

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

Vídeos com explicação

Conclusão

Agora, separado em funções, usando goroutines e canais nosso cliente está bem mais responsivo, ele não fica mais apenas uma coisa por vez, pode receber informações da rede ou do usuário e transmitir, tudo de forma assincronia.

Na próxima versão a melhoria mais obvia será adicionar uma rotina de ping/keep alive para termos certeza que a conexão está funcionando mesmo que o programa não esteja recebendo nem transmitindo nada.

Cesar Gimenes

Última modificação