20 min read

[Jornada do DevOps] #3 - Aprofundando os estudos no Go

[Jornada do DevOps] #3 - Aprofundando os estudos no Go

Nesse conteúdo vou me aprofundar mais no Go, além do básico.

Precisamos entender os conceitos de:

  • Switches
  • Interfaces
  • Context
  • Goroutines
  • Channels
  • Buffer
  • Select
  • Mutex
  • Marshalling e Unmarshalling JSON
  • Logging
  • Real time communication
  • API Client

Borá! 😜

Switches

As instruções switch expressam condicionais em muitas ramificações.

Um básico de switch.

package main

import (
    "fmt"
    "time"
)

func main() {

    i := 2
    fmt.Print("Write ", i, " as ")
    switch i {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    }

    switch time.Now().Weekday() {
    case time.Saturday, time.Sunday:
        fmt.Println("It's the weekend")
    default:
        fmt.Println("It's a weekday")
    }

    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }

    whatAmI := func(i interface{}) {
        switch t := i.(type) {
        case bool:
            fmt.Println("I'm a bool")
        case int:
            fmt.Println("I'm an int")
        default:
            fmt.Printf("Don't know type %T\n", t)
        }
    }
    whatAmI(true)
    whatAmI(1)
    whatAmI("hey")
}
$ go run switch.go 
Write 2 as two
It's a weekday
It's after noon
I'm a bool
I'm an int
Don't know type string

Outro exemplo com dias:

package main
 
import (
	"fmt"
	"time"
)
 
func main() {
	today := time.Now()
 
	switch today.Day() {
	case 5:
		fmt.Println("Today is 5th. Clean your house.")
	case 10:
		fmt.Println("Today is 10th. Buy some wine.")
	case 15:
		fmt.Println("Today is 15th. Visit a doctor.")
	case 25:
		fmt.Println("Today is 25th. Buy some food.")
	case 31:
		fmt.Println("Party tonight.")
	default:
		fmt.Println("No information available for that day.")
	}
}

Com condições:

$ go run switch.go 
Write 2 as two
It's a weekday
It's after noon
I'm a bool
I'm an int
Don't know type string

Interfaces

Interfaces são coleções nomeadas de assinaturas de métodos.

Aqui temos uma interface básica para formas geométricas:

package main
import "fmt"
import "math"

type geometria interface {
    area() float64
    perim() float64
}

type quadrado struct {
    largura, altura float64
}
type círculo struct {
    raio float64
}

func (q quadrado) area() float64 {
    return q.largura * s.altura
}
func (q quadrado) perim() float64 {
    return 2*q.largura + 2*q.altura
}
A implementação para círculos.

func (c círculo) area() float64 {
    return math.Pi * c.raio * c.raio
}
func (c círculo) perim() float64 {
    return 2 * math.Pi * c.raio
}

func medir(g geometria) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}
func main() {
    q := quadrado{largura: 3, altura: 4}
    c := círculo{raio: 5}
    medir(q)
    medir(c)
}

Context

HTTP Client

A biblioteca padrão Go vem com excelente suporte para clientes e servidores HTTP no pacote net/http. Neste exemplo, vamos usá-lo para emitir solicitações HTTP simples.

Emita uma solicitação HTTP GET para um servidor. http.Get é um atalho conveniente para criar um objeto http.Client e chamar seu método Get; ele usa o objeto http.DefaultClient que tem configurações padrão úteis.

Vamos imprimir o status da resposta HTTP com as primeiras 5 linhas do corpo da resposta.

package main
import (
    "bufio"
    "fmt"
    "net/http"
)
func main() {

    resp, err := http.Get("http://blog.renkel.com.br")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    fmt.Println("Response status:", resp.Status)

scanner := bufio.NewScanner(resp.Body)
    for i := 0; scanner.Scan() && i < 5; i++ {
        fmt.Println(scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        panic(err)
    }
}
$ go run http-clients.go
Response status: 200 OK

HTTP Server

Escrever um servidor HTTP básico é fácil usando o pacote net/http.

Um conceito fundamental em net/http servidores são os manipuladores. Um manipulador é um objeto que implementa a http.Handlerinterface. Uma maneira comum de escrever um manipulador é usar o adaptador http.HandlerFunc em funções com a assinatura apropriada.

As funções que servem como manipuladores recebem a http.ResponseWritere a http.Request como argumentos. O gravador de resposta é usado para preencher a resposta HTTP. Aqui nossa resposta simples é apenas “hello\n”.

Registramos nossos manipuladores nas rotas do servidor usando http.HandleFunc. Ele configura o roteador padrão no net/httppacote e recebe uma função como argumento.

Finalmente, chamamos o ListenAndServe com a porta e um manipulador. nildiz para usar o roteador padrão que acabamos de configurar.

Execute o servidor em segundo plano e a rota /hello.

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, req *http.Request) {

    fmt.Fprintf(w, "hello\n")
}

func headers(w http.ResponseWriter, req *http.Request) {

    for name, headers := range req.Header {
        for _, h := range headers {
            fmt.Fprintf(w, "%v: %v\n", name, h)
        }
    }
}

func main() {

    http.HandleFunc("/hello", hello)
    http.HandleFunc("/headers", headers)

    http.ListenAndServe(":8090", nil)
}
$ go run http-servers.go &

$ curl localhost:8090/hello
hello

Context

E por fim o Context. No exemplo anterior, analisamos a configuração de um servidor HTTP simples . Os servidores HTTP são úteis para demonstrar o uso de context.

Um context.Contexté criado para cada solicitação pelo net/httpmaquinário e está disponível com o método Context().

Aguarde alguns segundos antes de enviar uma resposta ao cliente. Isso pode simular algum trabalho que o servidor está fazendo. Enquanto estiver trabalhando, fique de olho no canal do contexto Done() para um sinal de que devemos cancelar o trabalho e retornar o mais rápido possível.

O método Err() do contexto retorna um erro que explica porque o Done()canal foi fechado.

Como antes, registramos nosso manipulador na rota “/hello” e começamos a servir.

Execute o servidor em segundo plano.

Simule uma solicitação de cliente para /hello, pressionando Ctrl+C logo após começar a sinalizar o cancelamento.

package main

import (
    "fmt"
    "net/http"
    "time"
)

func hello(w http.ResponseWriter, req *http.Request) {

    ctx := req.Context()
    fmt.Println("server: hello handler started")
    defer fmt.Println("server: hello handler ended")

    select {
    case <-time.After(10 * time.Second):
        fmt.Fprintf(w, "hello\n")
    case <-ctx.Done():

        err := ctx.Err()
        fmt.Println("server:", err)
        internalError := http.StatusInternalServerError
        http.Error(w, err.Error(), internalError)
    }
}

func main() {

    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8090", nil)
}
$ go run context-in-http-servers.go &

$ curl localhost:8090/hello
server: hello handler started
^C
server: context canceled
server: hello handler ended

Goroutines

Uma goroutine é uma thread leve de execução.

Suponha que temos uma função de chamada f(s). Aqui está como nós chamamos ela da maneira usual, executando sincronicamente.

Para chamar essa função em uma goroutine, use go f(s). Essa nova goroutine executará simultaneamente com a que foi chamada.

Você pode também iniciar uma goroutine para uma função de chamada anônima.

Nossas duas goroutines são executadas de forma assíncrona em goroutines separadas agora, assim a goroutine cai até aqui. Esse código Scanln exige que pressionamos uma tecla antes de sair do programa.

Quando executarmos esse programa, nós vemos primeiramente a saída da chamada bloqueada, então a saída intercalada das duas goroutines. Essa intercalação reflete a execução simultânea das goroutines pelo tempo de execução do Go.

package main

import "fmt"

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

    // Suponhamos que temos uma função de chamada `f(s)`. Aqui está
    // como nós chamaríamos ela de maneira usual, executando 
    // sincronicamente.    
    f("direto")

    // Para chamar essa função na goroutine, use
    // `go f(s)`. Essa nova goroutine executará
    // simultaneamente com a que foi chamada.   
    go f("goroutine")

    // Você também pode iniciar uma goroutine para uma
    // função de chamada anônima.    
    go func(msg string) {
        fmt.Println(msg)
    }("indo")

    // Nossas duas goroutines estão rodando de forma assíncrona
    // em goroutines separadas agora, assim a execução cai até
    // aqui. Este código `Scan1n` exige que pressionamos uma
    // tecla antes de sair do programa.
    var input string
    fmt.Scanln(&input)
    fmt.Println("pronto")
}

Channels

Canais são pipes que conectam goroutines concorrentes. Você pode enviar valores para os canais de uma goroutine e receber esses valores em outra goroutine. Canais são um poderoso primitivo que são muito subjacentes à funcionalidade do Go.

Cria um novo canal com make(chan tipo-val). Canais são digitados pelos valores que eles transmitem.

Envia um valor para um canal usando a sintaxe canal <-. Aqui enviamos "ping" para o canal mensagens que fizemos acima, a partir de uma nova goroutine.

A sintaxe <-canalrecebe um valor de um canal. Aqui nós vamos receber a mensagem "ping" que enviamos acima e imprimi-lo.

Quando nós executarmos o programa a mensagem "ping" é passada com sucesso de uma goroutine para outra através de nosso canal.

Por padrão envia e recebe blocos até que o emissor e receptor estejam prontos. Essa propriedade nos permitiu esperar até o final de nosso programa pela mensagem "ping" sem ter que usar qualquer outra sincronização.

// _Canais_ são pipes que conectam goroutines concorrentes.
// Você pode enviar valores para os canais de uma goroutine
// e receber esses valores em outra goroutine. Canais são
// poderosos primitivos que são muito subjacentes à
// funcionalidade do Go.

package main

import "fmt"

func main() {

    // Cria um novo canal com `make(chan tipo-val)`.
    // Canais são digitados pelos valores que eles transmitem.    
    mensagens := make(chan string)
    
    // _Envia_ um valor para um canal usando a sintaxe `canal <-`.
    // Aqui nós enviamos `"ping"` para os canais das `mensagens`
    // que fizemos anteriormente, a partir de uma nova goroutine.    
    go func() { mensagens <- "ping" }()
    
    // A sintaxe `<-canal` _recebe_ um valor do
    // canal. Aqui nós vamos receber a mensagem do `"ping"`
    // que enviamos acima e imprimi-lo.                                          
    msg := <-mensagens
    fmt.Println(msg)
}
$ go run canais.go
ping

Buffer

Por padrão canais são não buferizados, significando que eles só aceitarão envios (chan <-) se houver um receptor correspondente (<- chan) pronto para receber o valor enviado. Canais buferizados aceitam um número limitado de valores sem um receptor correspondente para esses valores.

Aqui nós fizemos um canal de strings buferizadas em 2 valores.

Por esse canal ser buferizado, nós podemos enviar esses valores em um canal sem um receptor concorrente correspondente.

Depois nós podemos receber esses dois valores, como de costume.

// Por padrão canais são _não buferizados_, significando que eles
// só aceitarão envios (`chan <-`) se houver um receptor
// correspondente (`<- chan`) pronto para receber o
// valor enviado. _Canais buferizados_ aceitam um número
// limitado de valores sem um receptor correspondente para
// esses valores.

package main

import "fmt"

func main() {

    // Aqui nós `fizemos` um canal de strings buferizado
    // 2 valores.    
    mensagens := make(chan string, 2)

    	
    // Por esse canal estar buferizado, nós podemos enviar
    // esses valores em um canal sem um receptor concorrente
    // correspondente.    
    mensagens <- "buferizado"
    mensagens <- "canal"

    // Depois nós podemos receber esses dois valores, como de costume.    
    fmt.Println(<-mensagens)
    fmt.Println(<-mensagens)
}
$ go run buferizacao-canal.go
buferizado
canal

Select

O select do Go permite que você espere em múltiplos canais de operações. Combinar goroutines e canais com o select é um poderoso recurso do Go.

Para nosso exemplo nós iremos selecionar através de dois canais.

Cada canal irá receber um valor depois de uma certa quantidade de tempo, para simular e.x. o bloqueamento de operações RPC executando em goroutines simultâneas.

Nós iremos usar o select para esperar esses valores simultâneamente, imprimindo cada um como ele chega.

Receber os valores "um" e então "dois" como esperado.

Note que o tempo total de execução é apenas ~2 segundos uma vez que tanto o segundo 1 e 2 executam o Sleeps concorrentemente.

// O _select_ do Go permite que você espere em múltiplos canais
// de operações. Combinar goroutines e canais com
// o select é um poderoso recurso do Go.

package main

import "time"
import "fmt"

func main() {

	// Para o nosso exemplo nós iremos selecionar através de dois canais.    
    c1 := make(chan string)
    c2 := make(chan string)

    // Cada canal receberá um valor depois de uma certa quantidade
    // de tempo, para simular e.x. o bloqueamento operações RPC
    // executando em goroutines simultâneas.    
    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "um"
    }()
    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "dois"
    }()

    // Nós iremos utilizar o `select` para esperar esses valores
    // simultâneamente, imprimindo cada um como ele chega.    
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("recebido", msg1)
        case msg2 := <-c2:
            fmt.Println("recebido", msg2)
        }
    }
}
$ time go run select.go
recebido um
recebido dois

real	0m2.245s

Mutex

Para um estado mais complexo, podemos usar um mutex para acessar dados com segurança em várias goroutines.

O contêiner contém um mapa de contadores; como queremos atualizá-lo simultaneamente de várias goroutines, adicionamos um Mutex para sincronizar o acesso. Observe que os mutexes não devem ser copiados, portanto, se isso struct for passado, deve ser feito por ponteiro.

Bloqueie o mutex antes de acessar counters; desbloqueie-o no final da função usando uma instrução defer.

Execute várias goroutines simultaneamente; note que todos eles acessam o mesmo Container e dois deles acessam o mesmo contador.

A execução do programa mostra que os contadores foram atualizados conforme o esperado.

Em seguida, veremos como implementar essa mesma tarefa de gerenciamento de estado usando apenas goroutines e canais.

package main

import (
    "fmt"
    "sync"
)

type Container struct {
    mu       sync.Mutex
    counters map[string]int
}

func (c *Container) inc(name string) {

    c.mu.Lock()
    defer c.mu.Unlock()
    c.counters[name]++
}

func main() {
    c := Container{

        counters: map[string]int{"a": 0, "b": 0},
    }

    var wg sync.WaitGroup

    doIncrement := func(name string, n int) {
        for i := 0; i < n; i++ {
            c.inc(name)
        }
        wg.Done()
    }

    wg.Add(3)
    go doIncrement("a", 10000)
    go doIncrement("a", 10000)
    go doIncrement("b", 10000)

    wg.Wait()
    fmt.Println(c.counters)
}
$ go run mutexes.go
 map[a:20000 b:10000]

Marshalling & Unmarshalling JSON

Go oferece suporte integrado para codificação e decodificação JSON, incluindo tipos to e from integrados e personalizados.

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type response1 struct {
    Page   int
    Fruits []string
}

type response2 struct {
    Page   int      `json:"page"`
    Fruits []string `json:"fruits"`
}

func main() {

    bolB, _ := json.Marshal(true)
    fmt.Println(string(bolB))

    intB, _ := json.Marshal(1)
    fmt.Println(string(intB))

    fltB, _ := json.Marshal(2.34)
    fmt.Println(string(fltB))

    strB, _ := json.Marshal("gopher")
    fmt.Println(string(strB))

    slcD := []string{"apple", "peach", "pear"}
    slcB, _ := json.Marshal(slcD)
    fmt.Println(string(slcB))

    mapD := map[string]int{"apple": 5, "lettuce": 7}
    mapB, _ := json.Marshal(mapD)
    fmt.Println(string(mapB))

    res1D := &response1{
        Page:   1,
        Fruits: []string{"apple", "peach", "pear"}}
    res1B, _ := json.Marshal(res1D)
    fmt.Println(string(res1B))

    res2D := &response2{
        Page:   1,
        Fruits: []string{"apple", "peach", "pear"}}
    res2B, _ := json.Marshal(res2D)
    fmt.Println(string(res2B))

    byt := []byte(`{"num":6.13,"strs":["a","b"]}`)

    var dat map[string]interface{}

    if err := json.Unmarshal(byt, &dat); err != nil {
        panic(err)
    }
    fmt.Println(dat)

    num := dat["num"].(float64)
    fmt.Println(num)

    strs := dat["strs"].([]interface{})
    str1 := strs[0].(string)
    fmt.Println(str1)

    str := `{"page": 1, "fruits": ["apple", "peach"]}`
    res := response2{}
    json.Unmarshal([]byte(str), &res)
    fmt.Println(res)
    fmt.Println(res.Fruits[0])

    enc := json.NewEncoder(os.Stdout)
    d := map[string]int{"apple": 5, "lettuce": 7}
    enc.Encode(d)
}
$ go run json.go
true
1
2.34
"gopher"
["apple","peach","pear"]
{"apple":5,"lettuce":7}
{"Page":1,"Fruits":["apple","peach","pear"]}
{"page":1,"fruits":["apple","peach","pear"]}
map[num:6.13 strs:[a b]]
6.13
a
{1 [apple peach]}
apple
{"apple":5,"lettuce":7}

Cobrimos o básico do JSON aqui, precisamos aprofundar mais na postagem do blog JSON and Go e os documentos do pacote JSON, posso criar uma postagem específica para JSON no GO.

Logging (Zap, Logrus)

Não preciso dizer o quão importante é o registro em log. Os logs são usados ​​por todos os aplicativos da Web de produção para ajudar os desenvolvedores e as operações:

  • Detectar bugs no código do aplicativo
  • Descubra problemas de desempenho
  • Faça análises post-mortem de interrupções e incidentes de segurança

Os dados que você realmente registra dependerão do tipo de aplicativo que você está construindo.

O que não registrar

Em geral, você não deve registrar nenhuma forma de dados comerciais confidenciais ou informações de identificação pessoal. Isso inclui, mas não se limita a:

  • Nomes
  • endereços IP
  • Números de cartão de crédito

Essas restrições podem tornar os logs menos úteis do ponto de vista da engenharia, mas tornam seu aplicativo mais seguro. Em muitos casos, regulamentações como GDPR e HIPAA podem proibir o registro de dados pessoais.

Apresentando o pacote de logs

A biblioteca padrão Go possui um logpacote integrado que fornece os recursos de registro mais básicos. Embora não tenha níveis de log (como depuração, aviso ou erro), ele ainda fornece tudo o que você precisa para configurar uma estratégia básica de log.

package main

import "log"

func main() {
    log.Println("Hello world!")
}

O código acima imprime o texto "Hello world!" ao erro padrão, mas também inclui a data e a hora, o que é útil para filtrar mensagens de log por data.

Registrando em um arquivo

Se você precisar armazenar mensagens de log em um arquivo, poderá fazê-lo criando um novo arquivo ou abrindo um arquivo existente e definindo-o como saída do log. Aqui está um exemplo:

package main

import (
    "log"
    "os"
)

func main() {
    // If the file doesn't exist, create it or append to the file
    file, err := os.OpenFile("logs.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
    if err != nil {
        log.Fatal(err)
    }

    log.SetOutput(file)

    log.Println("Hello world!")
}

Quando executamos o código, o seguinte é escrito para logs.txt.

2019/12/09 17:22:47 Hello world!

Como mencionado anteriormente, você pode basicamente enviar seus logs para qualquer destino que implemente a interface io.Writer, então você tem muita flexibilidade ao decidir onde registrar as mensagens em seu aplicativo.

Escolhendo uma estrutura de registro

Decidir qual framework usar pode ser um desafio, pois há várias opções para escolher.

As duas estruturas de log mais populares para Go são glog e logrus. A popularidade do glog é surpreendente, pois não é atualizado há vários anos. logrus é melhor mantido e usado em projetos populares como o Docker, então vamos nos concentrar nele.

Introdução ao Logrus

Instalar o logrus é simples:

go get "github.com/Sirupsen/logrus"

Uma grande coisa sobre o logrus é que ele é completamente compatível com o pacote log da biblioteca padrão, então você pode substituir suas importações de log em todos os lugares log "github.com/sirupsen/logrus"e ele simplesmente funcionará!

Vamos modificar nosso exemplo anterior "hello world" que usava o pacote de log e usar o logrus:

package main

import (
  log "github.com/sirupsen/logrus"
)

func main() {
    log.Println("Hello world!")
}

A execução deste código produz a saída:

INFO[0000] Hello world!

Não poderia ser mais fácil! 😜

Fazendo login em JSON

logrusé adequado para logs estruturados em JSON que — como JSON é um padrão bem definido — facilita a análise de seus logs por serviços externos e também torna a adição de contexto a uma mensagem de log relativamente simples por meio do uso de campos, conforme mostrado abaixo:

package main

import (
    log "github.com/sirupsen/logrus"
)

func main() {
    log.SetFormatter(&log.JSONFormatter{})
    log.WithFields(
        log.Fields{
            "foo": "foo",
            "bar": "bar",
        },
    ).Info("Something happened")
}

A saída de log gerada será um objeto JSON que inclui a mensagem, o nível de log, o carimbo de data/hora e os campos incluídos.

{"bar":"bar","foo":"foo","level":"info","msg":"Something happened","time":"2019-12-09T15:55:24+01:00"}

Níveis de registro

Ao contrário do pacote de log padrão, o logrus suporta níveis de log.

O logrus tem sete níveis de log: Trace, Debug, Info, Warn, Error, Fatal e Panic. A gravidade de cada nível aumenta à medida que você desce na lista.

log.Trace("Something very low level.")
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// Calls os.Exit(1) after logging
log.Fatal("Bye.")
// Calls panic() after logging
log.Panic("I'm bailing.")

Ao definir um nível de registro em um registrador, você pode registrar apenas as entradas necessárias, dependendo do seu ambiente. Por padrão, o logrus registrará qualquer coisa que seja Info ou superior (Warn, Error, Fatal ou Panic).

package main

import (
    log "github.com/sirupsen/logrus"
)

func main() {
    log.SetFormatter(&log.JSONFormatter{})

    log.Debug("Useful debugging information.")
    log.Info("Something noteworthy happened!")
    log.Warn("You should probably take a look at this.")
    log.Error("Something failed but I'm not quitting.")
}

A execução do código acima produzirá a seguinte saída:

{"level":"info","msg":"Something noteworthy happened!","time":"2019-12-09T16:18:21+01:00"}
{"level":"warning","msg":"You should probably take a look at this.","time":"2019-12-09T16:18:21+01:00"}
{"level":"error","msg":"Something failed but I'm not quitting.","time":"2019-12-09T16:18:21+01:00"}

Observe que a mensagem de nível de depuração não foi impressa. Para incluí-lo nos logs, defina log.Level como igual log.DebugLevel:

log.SetLevel(log.DebugLevel)

Real time communication - Websocket (Melody, Centrifugo)

Pesquisei e encontrei alguns módulos para websocket no Go, alguns são Melody, Centrifugo e Gorilla. O Melody é baseado no Gorilla.

Achei o mais interessante, com mais exemplos o Centrifugo. Então vou aprendê-lo e usá-lo. 😁

Centrifugo

Centrifugo é um servidor de mensagens em tempo real escalável de código aberto de maneira agnóstica de idioma.
O Centrifugo funciona em conjunto com aplicativos escritos em qualquer linguagem de programação – tanto no backend quanto no frontend. Ele é executado como um serviço autônomo hospedado em seu hardware e se adapta bem a arquiteturas monolíticas e de microsserviços.
Centrifugo é rápido e escalável para suportar milhões de conexões simultâneas de clientes. Ele fornece vários transportes em tempo real para escolher e um conjunto de recursos para simplificar a criação de aplicativos em tempo real.

As mensagens em tempo real podem ajudar a criar aplicativos interativos em que os eventos podem ser entregues aos usuários quase imediatamente após serem reconhecidos pelo back-end do aplicativo, enviando dados para uma conexão persistente – alcançando assim uma latência de entrega mínima.

Bate-papos, comentários ao vivo, jogos multiplayer, métricas de streaming podem ser construídas em cima de um sistema de mensagens em tempo real.

Centrifugo lida com conexões persistentes de clientes em WebSocket bidirecional, SockJS e SSE unidirecional (EventSource), HTTP-streaming, transportes GRPC e fornece API para publicar mensagens para clientes online em tempo real.

Instando Centrifugo

O servidor Centrifugo é escrito em linguagem Go. É um software de código aberto, o código fonte está disponível no Github.

Para um desenvolvimento local, a maneira mais simples de obter o Centrifugo é a partir de uma versão binária (ou seja, um único arquivo executável totalmente contido).

Versões binárias disponíveis no Github. Faça o download da versão mais recente para o seu sistema operacional, descompacte-a e pronto. Centrifugo é pré-construído para:

  • Linux 64 bits (linux_amd64)
  • Linux 32 bits (linux_386)
  • Linux ARM de 64 bits (linux_arm64)
  • Mac OS (darwin_amd64)
  • MacOS no Apple Silicon (darwin_arm64)
  • Windows (windows_amd64)
  • FreeBSD (freebsd_amd64)
  • ARM v6 (linux_armv6)

Os arquivos contêm um único arquivo binário compilado estaticamente centrifugoque está pronto para ser executado:

./centrifugo -h

Veja a versão do Centrifugo:

./centrifugo version

Centrifugo requer um arquivo de configuração com várias chaves secretas. Se você é novo no Centrifugo, existe um comando genconfig que gera um arquivo de configuração mínimo para começar:

./centrifugo genconfig

Ele cria um arquivo de configuração config.jsoncom alguns valores de opção gerados automaticamente em um diretório atual (por padrão).

Aqui vamos construir um aplicativo de navegador muito simples com Centrifugo. Ele funciona de forma que os usuários se conectem ao Centrifugo via WebSocket, se inscrevam em um canal e comecem a receber todas as mensagens publicadas nesse canal. No nosso caso, enviaremos um valor de contador para todos os inscritos do canal para atualizá-lo em todas as abas abertas do navegador em tempo real.

Depois de ter Centrifugo disponível em sua máquina, você pode gerar o arquivo de configuração mínimo necessário com o seguinte comando:

./centrifugo genconfig

Este comando auxiliar irá gerar um arquivo config.json no diretório de trabalho com conteúdo como este:

config.json

{  "token_hmac_secret_key": "46b38493-147e-4e3f-86e0-dc5ec54f5133",  "admin_password": "ad0dff75-3131-4a02-8d64-9279b4f1c57b",  "admin_secret": "583bc4b7-0fa5-4c4a-8566-16d3ce4ad401",  "api_key": "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6",  "allowed_origins": []}

Agora podemos iniciar um servidor. Vamos iniciá-lo com uma interface web de administração integrada:

./centrifugo --config=config.json --admin

Também podemos habilitar a interface da Web de administração não usando o sinalizador  --admin, mas adicionando a opção "admin": true ao arquivo de configuração JSON:

config.json

{  "token_hmac_secret_key": "46b38493-147e-4e3f-86e0-dc5ec54f5133",  "admin": true,  "admin_password": "ad0dff75-3131-4a02-8d64-9279b4f1c57b",  "admin_secret": "583bc4b7-0fa5-4c4a-8566-16d3ce4ad401",  "api_key": "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6",  "allowed_origins": []}

E então rodando apenas com um caminho para um arquivo de configuração:

./centrifugo --config=config.json

Vamos abrir http://localhost:8000. Vamos ver o painel web de administração do Centrifugo. Insira o valor admin_passworddo arquivo de configuração para efetuar login.

Painel da Web do administrador

Dentro do painel de administração, você deve ver que um nó Centrifugo está em execução e não possui clientes conectados.

Agora vamos criar o index.htmlarquivo com nosso aplicativo simples:

<html>
    <head>
        <title>Centrifugo quick start</title>
    </head>
    <body>
        <div id="counter">-</div>
        <script src="https://cdn.jsdelivr.net/gh/centrifugal/[email protected]/dist/centrifuge.min.js"></script>
        <script type="text/javascript">
            const container = document.getElementById('counter')
            const centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket");
            centrifuge.setToken("<TOKEN>");
            
            centrifuge.on('connect', function(ctx) {
                console.log("connected", ctx);
            });

            centrifuge.on('disconnect', function(ctx) {
                console.log("disconnected", ctx);
            });

            centrifuge.subscribe("channel", function(ctx) {
                container.innerHTML = ctx.data.value;
                document.title = ctx.data.value;
            });

            centrifuge.connect();
        </script>
    </body>
</html>

Observe que estamos usando o centrifuge-js2.8.4 neste exemplo, é melhor você usar sua versão mais recente no momento da leitura deste tutorial.

Acima, criamos uma instância de um cliente (chamado Centrifuge) passando o endereço de endpoint WebSocket padrão do Centrifugo para ele, então nos inscrevemos em um canal chamado channel e fornecemos uma função de retorno de chamada para processar mensagens recebidas em tempo real. Em seguida, chamamos o método .connect() para iniciar uma conexão WebSocket.

Agora você precisa servir este arquivo com um servidor HTTP. Em um aplicativo Javascript do mundo real, você servirá seus arquivos HTML com um servidor web de sua escolha – mas para este exemplo simples podemos usar um servidor de arquivos estáticos Centrifugo integrado simples:

./centrifugo serve --port 3000

Como alternativa, se você tiver o Python 3 instalado:

python3 -m http.server 3000

Esses comandos iniciam um servidor web de arquivo estático simples que serve o diretório atual na porta 3000. Certifique-se de que você ainda tenha o servidor Centrifugo em execução. Abra http://localhost:3000/.

Agora, se você observar as ferramentas do desenvolvedor do navegador ou nos logs do Centrifugo, notará que uma conexão não pode ser estabelecida com sucesso:

2021-09-01 10:17:33 [INF] request Origin is not authorized due to empty allowed_origins origin=http://localhost:3000

Isso porque não definimos allowed_origins na configuração. Modifique allowed_origins assim:

{  ...  "allowed_origins": [    "http://localhost:3000"  ]}

Origens permitidas é uma opção de segurança para solicitações originadas de navegadores da Web – veja mais detalhes nos documentos de configuração do servidor. Reinicie o Centrifugo após modificar allowed_origins.

Agora, se você recarregar uma janela do navegador com um aplicativo, deverá ver novos logs de informações na saída do servidor:

2021-02-26 17:47:47 [INF] invalid connection token error="jwt: token format is not valid" client=45a1b8f4-d6dc-4679-9927-93e41c14ad932021-02-26 17:47:47 [INF] disconnect after handling command client=45a1b8f4-d6dc-4679-9927-93e41c14ad93 command="id:1 params:\"{\\\"token\\\":\\\"<TOKEN>\\\"}\" " reason="invalid token" user=

Ainda não conseguimos conectar.

Isso porque o cliente deve fornecer um JWT (JSON Web Token) válido para se autenticar.

Esse token deve ser gerado em seu back-end e passado para um lado do cliente (sobre variáveis ​​de modelo ou usando uma chamada AJAX separada - da maneira que você preferir).

Como em nosso exemplo simples não temos um backend de aplicativo, podemos gerar rapidamente um token de exemplo para um usuário usando o subcomando centrifugo gentoken. Assim:

./centrifugo gentoken -u 123722

– onde -u é o sinalizador que define o ID do usuário. A saída deve ser assim:

HMAC SHA-256 JWT for user 123722 with expiration TTL 168h0m0s:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM3MjIiLCJleHAiOjE1OTAxODYzMTZ9.YMJVJsQbK_p1fYFWkcoKBYr718AeavAk3MAYvxcMk0M

– você terá outro valor de token, pois este é baseado em gerado aleatoriamente token_hmac_secret_key a partir do arquivo de configuração que criamos no início deste tutorial.

Agora podemos copiar o JWT HMAC SHA-256 gerado e colá-lo na chamada centrifuge.setToken em vez do <TOKEN> reservado no index.html. Ou seja:

centrifuge.setToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM3MjIiLCJleHAiOjE1OTAxODYzMTZ9.YMJVJsQbK_p1fYFWkcoKBYr718AeavAk3MAYvxcMk0M");

É isso! Se você recarregar a guia do navegador - a conexão será estabelecida com sucesso e o cliente se inscreverá em um canal.

Abra as ferramentas do desenvolvedor e olhe no painel de quadros do WebSocket, você deve ver algo assim:

OK, a última coisa que precisamos fazer aqui é publicar um novo valor de contador em um canal e garantir que nosso aplicativo funcione corretamente.

Podemos fazer isso pela API Centrifugo enviando uma solicitação HTTP para o endpoint da API padrão http://localhost:8000/api, mas vamos fazer isso primeiro pelo painel da Web do administrador.

Abra o painel da web de administração do Centrifugo em outra guia do navegador ( http://localhost:8000/) e vá para a seção Actions. Selecione a ação de publicação, insira o nome do canal no qual você deseja publicar - no nosso caso, isso é uma string channel e insira na área data o JSON assim:

{    "value": 1}
Publicação do administrador

Clique no botão Submit e confira a guia do navegador do aplicativo – o valor do contador deve ser recebido e exibido imediatamente.

Abra várias abas do navegador com nosso aplicativo e certifique-se de que todas as abas recebam uma mensagem assim que você a publicar.

BTW, vamos também ver como você pode publicar dados para canalizar a API Centrifugo a partir de um terminal usando a ferramneta curl:

curl --header "Content-Type: application/json" \  --header "Authorization: apikey aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6" \  --request POST \  --data '{"method": "publish", "params": {"channel": "channel", "data": {"value": 2}}}' \  http://localhost:8000/api

– onde para Authorization o cabeçalho definimos o valor api_key do arquivo de configuração do Centrifugo gerado acima.

Conseguimos! 😱👏 Isso é o básico de um aplicativo de navegador em tempo real com Centrifugo e seu cliente Javascript.

API Client (GraphQL - graphql-go, gqlgen)

Sobre o GraphQL no GO, vou criar um novo conteúdo de estudo, específico para ele. Acredito que tenha muito conteúdo básico para aprender.

No próximo episódio vamos ver então [Jornada do DevOps] #4 - GraphQL com Golang.

Vamos ver como usar a biblioteca graphql-go ou gqlgen para criar uma API GraphQL simples com Golang.

Assine a newsletter para acompanhar. 😜