[Cliente e servidor socket em Golang, um exemplo de chat.]

Esta é a quarta versão do nosso pequeno cliente e servidor socket, nessa versão fiz algumas melhorias interessantes, agora temos um sistema de chat completo com comandos interessantes e algumas otimizações.

A ideia dessa versão é deixar as coisas mais acertadas, um dos canais de escrita foi removido, pois nesse caso ser mais eficiente enviar os dados diretamente. A reconexão automática também foi removida para deixar o código mais limpo já que adicionei bastante coisas novas.

Servidor

Vamos às mudanças do lado do servidor.

O handle que fica lendo os dados que estão vindo da conexão com o cliente agora usa um buffer com tamanho fixo, o efeito disso é que não ficamos mais esperando vir um enter, isso torna a leitura dos dados mais eficiente.

func handleReadConn(conn net.Conn, msgReadCh chan string, errCh chan error) {
	for {
		if msgReadCh == nil || conn == nil {
			return
		}
		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil {
			errCh <- err
			return
		}
		m := string(buf[:n])
		msgReadCh <- m
	}
}

Para o envio de mensagens do servidor para os clientes, no lugar de usar canais em uma goroutine separada, essa versão usa apenas uma função, o objetivo é tornar o envio mais simples. Não é necessário mais uma goroutine sendo que o sistema já tem a leitura dos dados separada tornando leitura e escrita concorrentes.

Para evitar duplicação de código o tratamento de erro de envio também ficou na função send e apenas um boolean é retornado para indicar se o envio foi bem-sucedido ou não.

func send(conn net.Conn, msg string) bool {
	_, err := conn.Write([]byte(msg))
	if err != nil {
		if err == io.EOF {
			fmt.Printf("%v Connection closed\n", conn.RemoteAddr())
			return false
		}
		fmt.Printf("%v Error: %v\n", conn.RemoteAddr(), err)
		return false
	}
	return true
}

Além disso, fiz várias mudanças no handler de conexões que pode interpretar alguns comandos básicos que eu desavergonhadamente copiei do protocolo do IRC, bem simplificados, mas estão lá, coisas como /nick para mudar o apelido de um usuário e /who para listar os usuários conectados.

Cliente

Do lado do cliente, a função que faz a leitura dos dados vindos da conexão também está usando um buffer de tamanho fixo nessa versão, isso torna a leitura muito mais rápida assim como no servidor.

func readConn(conn net.Conn) {
	for {
		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil {
			errorChan <- err
			return
		}
		m := string(buf[:n])
		output <- m
	}
}

Assim como no servidor o cliente também usa uma pequena função send para enviar os dados, mas nesse caso quando acontece um erro simplesmente efetuamos log do erro e fechamos a conexão.

func send(conn net.Conn, m string) {
	if conn == nil {
		return
	}
	_, err := conn.Write([]byte(m))
	if err != nil {
		fmt.Println(err)
		conn.Close()
		conn = nil
	}
}

No cliente também temos alguns comandos do usuário, os mais importantes são o /connect que estabelece uma conexão com o servidor e o /help que mostra os comandos disponíveis. Uma coisa interessante é que se o clietne estiver conectado o comando /help também chama o /help do servidor compondo uma única listagem de help.

Código-fonte

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

Vídeos com explicação

Conclusão

Com essas mudanças ganhamos muitas funcionalidades, agora temos um sistema de chat razoavelmente utilizável, mais rápido e bastante confiável. Os comandos sendo enviados entre o cliente e o servidor são um bom exemplo de comunicação e do que seria um protocolo primitivo, as mesmas coisas poderiam ser feitas usando protocolos mais sofisticados, enviando gRPC, JSON ou outros formatos.

Cesar Gimenes