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 torná-lo assíncrono. Para isso, usaremos goroutines e canais.

Goroutines e Canais

Goroutines e canais são recursos avançados da linguagem Go. Goroutines são threads leves gerenciadas pelo próprio runtime da linguagem, e canais permitem a comunicação entre goroutines.

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

Esses dois recursos são um pouco avançados, por isso primeiro demonstrei como criar nosso pequeno cliente socket sem esses recursos.

Na verdade, é uma ótima prática resolver o problema da forma mais simples possível e só adicionar goroutines e canais se forem realmente necessários. Eles são fáceis de usar, mas têm um custo: o programa fica mais difícil de depurar.

Analisando o código-fonte

Lendo do standard input

Uma melhoria que fiz foi separar o código em pequenas funções, o que facilita 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 entra em um loop infinito lendo entradas do usuário. Quando o usuário pressiona enter, a função envia a string para o canal input. Se ocorrer algum erro na leitura de stdin, o programa é encerrado 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 a origem da leitura, para qual canal a escrita ocorre e que, no caso de erro, enviamos o erro para o canal errorChan em vez de encerrar o programa com um panic.

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 a conexão é estabelecida, retorna a conexão para ser usada nas outras partes do sistema.

É praticamente a mesma rotina que usávamos no programa anterior, mas agora separada 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

Na função main, resta a tarefa de iniciar as goroutines e gerenciar os canais. Para isso, usamos a palavra-chave select, que é semelhante ao switch case. No entanto, select espera 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 por mensagens de vários canais e tratamos o que chega em cada canal com a rotina apropriada.

Código-fonte

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

Vídeos com explicação

Conclusão

Agora, separado em funções e usando goroutines e canais, nosso cliente está muito mais responsivo. Ele não realiza apenas uma tarefa por vez; pode receber informações da rede ou do usuário e transmitir, tudo de forma assíncrona.

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

Cesar Gimenes

Última modificação