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.