Cliente e servidor socket em Golang, um exemplo de chat.
Esta é a quarta versão do nosso cliente e servidor socket. Nessa versão, fiz algumas melhorias interessantes. Agora, temos um sistema de chat completo com comandos úteis e algumas otimizações.
O objetivo desta versão é aprimorar o sistema. Removi um dos canais de escrita para enviar os dados diretamente, aumentando a eficiência. Também eliminei a reconexão automática para deixar o código mais limpo, já que adicionei várias novidades.
Servidor
Vamos às mudanças no servidor.
O handle que lê os dados da conexão com o cliente agora usa um buffer de tamanho fixo. Isso evita esperar por um enter, tornando a leitura 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 enviar mensagens do servidor para os clientes, substituímos o uso de canais em uma goroutine por uma única função. Isso simplifica o envio, já que a leitura dos dados está separada, permitindo leitura e escrita concorrentes.
Para evitar duplicação de código, o tratamento de erro de envio ficou na função send, que retorna um boolean indicando se o envio foi bem-sucedido.
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. Agora, ele interpreta comandos básicos inspirados no protocolo do IRC. Por exemplo, /nick
muda o apelido do usuário e /who
lista os usuários conectados.
Cliente
No cliente, a função que lê os dados da conexão também usa um buffer de tamanho fixo. Isso torna a leitura mais rápida, 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 usa uma função send para enviar dados. Se ocorrer um erro, registramos o 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, temos comandos importantes como /connect
, que estabelece uma conexão com o servidor, e /help
, que mostra os comandos disponíveis. Se o cliente estiver conectado, o comando /help
também chama o /help
do servidor, criando uma única lista de ajuda.
Código-fonte
Aqui está o código-fonte do nosso servidor e cliente.
Vídeos com explicação
Conclusão
Com essas mudanças, adicionamos muitas funcionalidades. Agora, temos um sistema de chat utilizável, mais rápido e confiável. Os comandos entre cliente e servidor exemplificam uma boa comunicação e um protocolo simples. As mesmas funcionalidades poderiam ser implementadas com protocolos mais sofisticados, como gRPC ou JSON.