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.