6 min read

[Jornada do DevOps] #7 - Real Time Communication (Melody e Centrifugo) em Golang

[Jornada do DevOps] #7 - Real Time Communication (Melody e Centrifugo) em Golang

Só recapitulando, nosso último estudo foi sobre Loggins com Golang. Lembra?

Nesse artigo vamos estudar sobre o Real Time Communication, seguindo o roadmap do Golang.

No roadmap ele segure trabalhar com Melody e Centrifugo, então... Vamos entender e testar.

O Websockets fornece uma abordagem fácil e compacta e pode ser usado em praticamente qualquer linguagem de programação.

Vamos estudar o pacote Melody.

Criando servidor de mensagens em tempo real com Melody em Go

Neste tópico vamos criar um aplicativo de bate-papo em tempo real em Golang usando Websockets. Vamos usar o Docker, já que está sendo um requisito para qualquer vaga por aí.

Pré-requisitos

  • Golang
  • Experiência com a sintaxe básica do Go
  • Docker

Configurando nosso projeto Go

Vamos começar criando os arquivos necessários para o projeto. Isso pode ser feito usando os seguintes comandos:

mkdir public
touch main.go Dockerfile public/index.html public/styles.css public/main.js

Nota: O comando mkdir cria um novo diretório e o comando touch é usado para criar novos arquivos.

Servidor HTTP básico

Vamos implementar um servidor HTTP básico com o pacote Gin.

Para isso, primeiro vamos precisar instalar as dependências necessárias:

go mod init rtc
go get github.com/gin-gonic/gin
go get gopkg.in/olahol/melody.v1

O servidor terá um único GET Endpoint para lidar com todas as solicitações de Websocket e também servirá um diretório estático que será usado como frontend.

package main

import (
	static "github.com/gin-contrib/static"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.Use(static.Serve("/", static.LocalFile("./public", true)))

	r.GET("/ws", func(c *gin.Context) {
		m.HandleRequest(c.Writer, c.Request)
	})

	r.Run(":5000")
}

Aqui ainda tive erro dos imports static e gin, tive que instalá-los.

go get github.com/gin-contrib/static
go get github.com/gin-gonic/gin

Como você pode ver, o servidor está servindo estaticamente o diretório public que você criamos anteriormente e expondo um único GET Endpoint com url /ws.

O servidor é então iniciado usando o comando run, que usa a porta como argumento.

go run .

Implementando o servidor Websockets

Agora que você temos um servidor HTTP básico, vamos continuar implementando a funcionalidade Websockets.

E agora que vamos usar a tal esperada, biblioteca Melody, um framework Websockets minimalista para a linguagem de programação Golang.

Vamos ver?

Como temos apenas um evento Websocket que é acionado sempre que você envia uma mensagem de chat, precisamos só transmitir a mensagem para os outros clientes.

package main

import (
	static "github.com/gin-contrib/static"
	"github.com/gin-gonic/gin"
	"gopkg.in/olahol/melody.v1"
)

func main() {
	r := gin.Default()
	m := melody.New()

	r.Use(static.Serve("/", static.LocalFile("./public", true)))

	r.GET("/ws", func(c *gin.Context) {
		m.HandleRequest(c.Writer, c.Request)
	})

	m.HandleMessage(func(s *melody.Session, msg []byte) {
		m.Broadcast(msg)
	})

	r.Run(":5000")
}

A função HandleMessage() é usada para receber todas as mensagens enviadas para o endpoint Websockets GET. Depois disso, você transmite as mensagens para todos os clientes conectados.

Criando o front-end

O próximo passo é criar um layout básico com o arquivo index.html. O layout contém um campo de entrada para o nome de usuário e uma área de texto para as mensagens, bem simples né. :)

<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
    <title>Websockets Chat - Aprendendo Go - Jornada do DevOps - Gustavo Kennedy Renkel</title>
    <link rel="stylesheet" href="styles.css">
    <script type="text/javascript" src="https://cdn.socket.io/socket.io-1.4.5.js"></script>
</head>
<body>
    <div class="container">
            <div class="row">
                <div class="col-md-6 offset-md-3 col-sm-12">
                    <h1 class="text-center">Chat com Socket IO </h1>
                    <br>
                    <div id="status"></div>
                    <div id="chat">
                        <input type="text" name="username" id="username" class="form-control" placeholder="Informe o nome...">
                        <br>
                        <div class="card">
                            <div id="messages" class="card-block"></div>
                        </div>
                        <br>
                        <textarea id="textarea" name="inputMessage" class="form-control" placeholder="Digite a mensagem..."></textarea>
                        <br>
                        <button id="send" class="btn">Enviar</button>
                    </div>
                </div>
            </div>
    </div>
    <script type="text/javascript" src="main.js"></script>
</body>
</html>

Também incluimos o arquivo main.js na parte inferior para que possa implementar a funcionalidade do cliente websockets para o frontend.

Primeiro, preicsamos obter os elementos do layout frontend do DOM Javascript usando a função document.querySelector().

const input = document.querySelector('#textarea')
const messages = document.querySelector('#messages')
const username = document.querySelector('#username')
const send = document.querySelector('#send')

Em seguida, vamos conectar ao servidor Websockets e cria um ouvinte para o evento onmessage. O evento acionará uma função para inserir a nova mensagem que recebeu no chat.

const url = "ws://" + window.location.host + "/ws";
const ws = new WebSocket(url);

ws.onmessage = function (msg) {
    insertMessage(JSON.parse(msg.data))
};

Agora que podemos receber as mensagens recebidas, é hora de enviar mensagens colocando um evento onclick no botão enviar.

send.onclick = () => {
    const message = {
		username: username.value,
		content: input.value,
    }

    ws.send(JSON.stringify(message));
    input.value = "";
};

O próximo passo é implementar a função insertMessage() que irá inserir a mensagem recebida na área de texto utilizando o Javascript DOM.

/**
 * Insert a message into the UI
 * @param {Message that will be displayed in the UI} messageObj
 */
function insertMessage(messageObj) {
	// Create a div object which will hold the message
	const message = document.createElement('div')

	// Set the attribute of the message div
	message.setAttribute('class', 'chat-message')
	console.log("name: " +messageObj.username + " content: " + messageObj.content)
	message.textContent = `${messageObj.username}: ${messageObj.content}`

	// Append the message to our chat div
	messages.appendChild(message)

	// Insert the message as the first message of our chat
	messages.insertBefore(message, messages.firstChild)
}

Tudo isso resulta no seguinte arquivo main.js:

const input = document.querySelector('#textarea')
const messages = document.querySelector('#messages')
const username = document.querySelector('#username')
const send = document.querySelector('#send')

const url = "ws://" + window.location.host + "/ws";
const ws = new WebSocket(url);

ws.onmessage = function (msg) {
    insertMessage(JSON.parse(msg.data))
};

send.onclick = () => {
    const message = {
		username: username.value,
		content: input.value,
	}

    ws.send(JSON.stringify(message));
    input.value = "";
};

/**
 * Insert a message into the UI
 * @param {Message that will be displayed in the UI} messageObj
 */
function insertMessage(messageObj) {
	// Create a div object which will hold the message
	const message = document.createElement('div')

	// Set the attribute of the message div
	message.setAttribute('class', 'chat-message')
	console.log("name: " +messageObj.username + " content: " + messageObj.content)
	message.textContent = `${messageObj.username}: ${messageObj.content}`

	// Append the message to our chat div
	messages.appendChild(message)

	// Insert the message as the first message of our chat
	messages.insertBefore(message, messages.firstChild)
}

Por último, você adicionará alguns CSS ao arquivo styles.css.

#messages{
    height:300px;
    overflow-y: scroll;
}

Testando o aplicativo

Vamos testar? Agora que terminamos o aplicativo, podemos executá-lo usando o seguinte comando.

go run main.go

Agora podemos acessar o localhost:5000 em duas guias diferentes do navegador e começar a conversar entre si.

Nota: aqui no meu ambiente não consegui acessar por "localhost:5000", acessando no "http://127.0.0.1:5000/" deu certo.

Veja os prints:

Aba 1
Aba 2

Conteinerizando o aplicativo

A última etapa agora é conteinerizar o aplicativo usando o Docker. Para isso, podemos usar um Dockerfile simples que compila e executa a aplicação.

FROM golang:latest

# Define o diretório atual dentro do container
WORKDIR /app

RUN GO111MODULE=on

# Copia o go mod e o aquivo sum 
COPY go.mod go.sum ./

# Baixa todas as dependências
RUN go mod download

COPY . . 

# Builda a aplicação
RUN go build -o main .

# Expoe a porta 5000
EXPOSE 5000

# Comando para rodar o executável
CMD ["./main"]

Construir a imagem pode ser feito usando o comando build. O sinalizador -t é usado para fornecer à imagem uma tag personalizada.

docker build -t websocketschat .

Agora podemos executar a imagem usando o comando run. O sinalizador -p é usado para definir a porta que deve ser exposta à máquina host.

docker run -p 5000:5000 websocketschat

Todo o projeto e muitos outros projetos Golang podem ser encontrados no repositório lá no Github.

Finalizado o uso do Melody, já conseguimos entender a funcionalidade dele.

Resumo

Conseguimos entender o real-time messaging no Golang. Neste estudo usamos o pacote Melody, mas vi outros tutoriais mostrando outros, como o Gorilla Websocket, Beaver, Redis e o Centrifugo.

Acredito que o Melody atenda as principais dificuldades para implementar um servidor de mensagem em tempo real, mesmo em produção.

Se você utiliza outro pacote, deixa nos comentários qual e por quê utiliza.

No próximo artigo vamos entender API Clients, será a [Jornada do DevOps] #8 - API Client com GraphQL em Golang.

Acompanhe!

Até a próxima.