Cliente e servidor socket em Golang com ping e pong.
Continuando o papo sobre cliente e servidor resiliente, é hora de aprimorar o servidor e adicionar o envio de mensagens de ping ou keep alive.
Além disso, fizemos melhorias no servidor. Agora, assim como o cliente, ele utiliza goroutines e canais.
Servidor
Lendo da Conexão
Nesta versão, o servidor utiliza uma goroutine para escrever e outra para ler os dados da conexão com o cliente. Ao usar goroutines e canais dessa forma, é essencial garantir que as goroutines sejam finalizadas corretamente.
Caso contrário, você pode ter um leak de goroutines, ou seja, goroutines que não estão realizando nenhuma tarefa, mas continuam ocupando memória.
func handleReadConn(conn net.Conn, msgReadCh chan string, errCh chan error) {
for {
if msgReadCh == nil {
return
}
m, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
errCh <- err
return
}
msgReadCh <- m
}
}
Escrevendo na Conexão
Gosto da ideia de ter uma goroutine responsável por enviar dados para o cliente, que lê de um canal. Além da modularidade, isso permite que a operação seja assíncrona e que um buffer possa ser utilizado no canal, se necessário.
func handleWriteConn(conn net.Conn, msgWriteCh chan string, errCh chan error) {
for {
m, ok := <-msgWriteCh
if !ok {
return
}
_, err := conn.Write([]byte(m))
if err != nil {
errCh <- err
return
}
}
}
Controle dos Canais
Um padrão comum é utilizar um loop infinito com select
para tratar os canais. Este é o coração do nosso servidor, necessário para controlar os estados do programa, como leitura, recebimento e tratamento de erros.
Além disso, o servidor agora possui as variáveis pingInterval
e maxPingInterval
, que controlam a frequência do envio de pings e o tempo máximo antes de encerrar a conexão.
func handler(conn net.Conn) {
pingInterval := time.Second * 5
maxPingInterval := time.Second * 15
msgReadCh := make(chan string)
msgWriteCh := make(chan string)
errCh := make(chan error)
lastMsgTime := time.Now()
defer func() {
close(msgReadCh)
close(msgWriteCh)
close(errCh)
conn.Close()
}()
go handleReadConn(conn, msgReadCh, errCh)
go handleWriteConn(conn, msgWriteCh, errCh)
for {
select {
case <-time.After(pingInterval):
if time.Since(lastMsgTime) > pingInterval {
fmt.Println("Enviando ping")
msgWriteCh <- "ping\n"
}
if time.Since(lastMsgTime) > maxPingInterval {
fmt.Println("Conexão inativa, encerrando")
return
}
case msg := <-msgReadCh:
lastMsgTime = time.Now()
if msg == "pong\n" {
fmt.Println("Pong recebido")
continue
}
fmt.Printf("%v %q\n", conn.RemoteAddr(), msg)
msgWriteCh <- msg
case err := <-errCh:
if err == io.EOF {
fmt.Printf("%v Conexão encerrada\n", conn.RemoteAddr())
return
}
fmt.Println("Erro ao ler da conexão", err)
return
}
}
}
Cliente
O cliente permaneceu praticamente o mesmo, apenas adicionamos a resposta ao ping.
case m := <-output:
if m == "ping\n" {
fmt.Println("Ping recebido")
fmt.Println("Enviando pong")
conn.Write([]byte("pong\n"))
continue
}
Código-fonte
Aqui estão os códigos-fonte do nosso servidor e cliente.
Vídeos com Explicação
Conclusão
Com o ping/pong, garantimos que a conexão esteja sempre ativa. Esse detalhe evita gastos desnecessários com clientes que mantêm a conexão aberta sem enviar ou receber dados. Se ocorrer um erro e algum cliente parar de responder, o servidor encerrará a conexão e liberará os recursos.