Pular para o conteúdo principal

victorstein.dev

52/100 Dias de Golang - Chat Golang

Table of Contents

# Chat Golang

Continuando no assunto de redes, vamos melhorar o script do dia 51 e fazer um chat, esse chat deve receber várias conexões e cada mensagem deve ser recebida por todos os usuários conectados no server - a famosa mensagem em broadcasting. Nosso server deve guardar uma lista de clientes conectados, deve ter uma função handleConnection() para lidar com cada conexão e uma função broadcaster() que é responsável por enviar a mensagem para todos os usuários conectados. As duas função serão goroutines e irão se comunicar via chanels. O nosso client será bem simples vai se conectar ao servidor, ficar recebendo e enviando as mensagens, a função que lê as mensagems deve ser uma goroutine também.

# Client

Vamos começar com o mais simples, a função que fica recebendo as mensagens:

go func() {
	reader := bufio.NewReader(conn)
	for {
		msg, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("Conexão encerrada.")
			os.Exit(0)
		}
		fmt.Print(msg)
	}
}()

Aqui criamos uma goroutine que ficar lendo as mensagens. O \n é usado no ReadString('\n') porque ele indica o delimitador de fim da mensagem, no caso do nosso chat aqui quando o user apertar a tecla enter será gerado um \n. E a função para enviarmos as mensagens segue a mesma lógica:

input := bufio.NewReader(os.Stdin)
	for {
		text, _ := input.ReadString('\n')
		_, err := conn.Write([]byte(text))
		if err != nil {
			fmt.Println("Erro ao enviar mensagem.")
			break
		}
	}

Aqui mudamos o Reader para o os.Stdin do terminal, ficamos lendo o input e enviamos para a conexão. Esse conn é a conexão estabelecida com o servidor. Veja como fica o client completo:

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	conn, err := net.Dial("tcp", "0.0.0.0:8000")
	if err != nil {
		fmt.Println("Erro ao conectar:", err)
		return
	}
	defer conn.Close()

	go func() {
		reader := bufio.NewReader(conn)
		for {
			msg, err := reader.ReadString('\n')
			if err != nil {
				fmt.Println("Conexão encerrada.")
				os.Exit(0)
			}
			fmt.Print(msg)
		}
	}()

	input := bufio.NewReader(os.Stdin)
	for {
		text, _ := input.ReadString('\n')
		_, err := conn.Write([]byte(text))
		if err != nil {
			fmt.Println("Erro ao enviar mensagem.")
			break
		}
	}
}

## Server

O server tem algumas funções a mais que o client. Primeiro vamos começar criando um map e um canal. O map será responsável por guardar os users conectados e o canal será a ponte de conexão entre as goroutines.

var clients = make(map[net.Conn]string)
var messages = make(chan string)

Agora vamos incrementar um pouco a função handleConnection. Logo que a conexão é estabelecida o server pergunta o nome do client. Esse nome é salvo no map de clients e fica em um for loop infinito recebendo mensagens e adicionado ao canal de messages. Todas as strings enviada ao canal de messages será enviada aos clients conectados.

func handleConnection(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)

	// Nome do cliente
	conn.Write([]byte("Digite seu nome: "))
	name, _ := reader.ReadString('\n')
	name = name[:len(name)-1]
	clients[conn] = name

	messages <- fmt.Sprintf("%s entrou no chat", name)

	for {
		msg, err := reader.ReadString('\n')
		if err != nil {
			delete(clients, conn)
			messages <- fmt.Sprintf("%s saiu do chat", name)
			break
		}
		messages <- fmt.Sprintf("%s: %s", name, msg)
	}
}

A função brodcaster é responsável por receber as mensagens através do canal messages e enviar para todos os usuários. Caso exista um erro o client é fechado e a referência do map é deletada.

func broadcaster() {
	for {
		msg := <-messages
		for client := range clients {
			_, err := client.Write([]byte(msg))
			if err != nil {
				client.Close()
				delete(clients, client)
			}
		}
	}
}

No main somente criamos o servidor na porta 8000, criamos a goroutine e ficamos em um for infinito escutando conexões. Veja o server.go completo

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

var clients = make(map[net.Conn]string)
var messages = make(chan string)

func handleConnection(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)

	// Nome do cliente
	conn.Write([]byte("Digite seu nome: "))
	name, _ := reader.ReadString('\n')
	name = name[:len(name)-1]
	clients[conn] = name

	messages <- fmt.Sprintf("%s entrou no chat", name)

	for {
		msg, err := reader.ReadString('\n')
		if err != nil {
			delete(clients, conn)
			messages <- fmt.Sprintf("%s saiu do chat", name)
			break
		}
		messages <- fmt.Sprintf("%s: %s", name, msg)
	}
}

func broadcaster() {
	for {
		msg := <-messages
		for client := range clients {
			_, err := client.Write([]byte(msg))
			if err != nil {
				client.Close()
				delete(clients, client)
			}
		}
	}
}

func main() {
	listener, err := net.Listen("tcp", "0.0.0.0:8000")
	if err != nil {
		fmt.Println("Erro ao iniciar o servidor:", err)
		os.Exit(1)
	}
	defer listener.Close()

	fmt.Println("Servidor iniciado na porta 8000")

	go broadcaster()

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Erro ao aceitar conexão:", err)
			continue
		}
		go handleConnection(conn)
	}
}

Para rodar, temos que abrir 3 terminais. Um para o server e outros dois para os clients. Daí é só ficar digitando e ficar vendo o chat.