[Cliente e servidor socket em Golang com ping e pong.]

Continuando com o papo sobre cliente e servidor resiliente é hora de trabalhar um pouco mais no servidor e adicionar o enviou de mensagens de ping ou keep alive.

Além disso, melhoramos fiz algumas melhorias no servidor que agora assim como o cliente usa goroutines e canais.

Servidor

lendo da conexão

Nessa versão o servidor usa uma goroutine para escrita e outra para leitura dos dados vindos da conexão com o cliente. Quando se usa goroutines e canais dessa forma uma coisa muito importante e se deve ter o máximo de atenção  é ter certeza que as goroutines estão sendo finalizadas.

Caso contrario você pode acabar com um leak de goroutines. Ou seja, goroutines que não estão fazendo nada, mas como não foram finalizadas estão ocupando RAM. 

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

Eu particularmente gosto da ideia de ter uma goroutine responsável por enviar dados para o cliente que, por sua vez, lê de um canal. Além da modularidade, tem a vantagem de ser assíncrono e você pode inclusive usar um buffer 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
		}
	}
}

Handle para controle dos canais

Um padrão bem comum é esse loop infinito com o select que fica tratando os canais, é o coração do nosso servidor e é necessário porque como agora  tudo é assíncrono precisamos ter um controlador dos estados em que o programa pode estar em qualquer momento, lendo, recebendo, tratando erro, etc.

Além da estrutura do servidor ter melhorado com os canais, note às duas variáveis pingInterval e maxPingInterval que servem para controlar de quanto em quanto tempo enviaremos um ping para o cliente e qual o tempo máximo antes de derrubarmos 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("Sending ping")
				msgWriteCh <- "ping\n"
			}
			if time.Since(lastMsgTime) > maxPingInterval {
				fmt.Println("Inactive connection, closing")
				return
			}
		case msg := <-msgReadCh:
			lastMsgTime = time.Now()
			if msg == "pong\n" {
				fmt.Println("Received pong")
				continue
			}
			fmt.Printf("%v %q\n", conn.RemoteAddr(), msg)
			msgWriteCh <- msg
		case err := <-errCh:
			if err == io.EOF {
				fmt.Printf("%v Connection closed\n", conn.RemoteAddr())
				return
			}
			fmt.Println("Error reading from connection", err)
			return
		}
	}
}

Cliente

O cliente dessa vez não mudou quase nada, apenas incluímos a resposta do ping.

case m := <-output:
	if m == "ping\n" {
		fmt.Println("Received ping")
		fmt.Println("Sending pong")
		conn.Write([]byte("pong\n"))
		continue
	}

Código-fonte

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

Vídeos com explicação

Conclusão

Com o ping/pong garantimos que a conexão está sempre ativa, é um detalhe, mas vai evitar gastos desnecessários com cliente que fica com a conexão aberta sem enviar e receber, se houver um bug e algum cliente parar de responder o servidor vai se encarregar de terminar a conexão e liberar os recursos.

Cesar Gimenes