Desenvolvimento

26 out, 2018

Usando Socket.io com Golang, Vue e Android

Publicidade

Buenas! Resolvi escrever o artigo para demostrar num nível superficial como “conversar” com linguagens distintas em tempo real usando o socket.io. Exemplificarei a construção do Socket Server com Golang e depois mostrarei como realizar a conexão do Client com Vue.js e Java.

O principal motivo que me levou a pesquisar e desenvolver algo desse tipo foi a curiosidade de tornar todos os lados reativos. Eu simplesmente trabalhava com frameworks reativos na web, mas como eu poderia tornar tudo reativo e garantir a transmissão dos dados em tempo real? Isso que irei mostrar neste artigo.

Não entrarei em detalhes das linguagens e bibliotecas que irei usar no exemplo. Apenas demonstrarei uma das inúmeras possibilidades de criar esse tipo de aplicação.

Criando o socket server com Golang

Mãos a obra! O primeiro passo é baixar e instalar a linguagem na sua máquina local. Para isso, basta seguir os seguintes passos:

Faça o download de acordo com seu sistema operacional neste link e siga o doc de instalação do site mesmo.

Vamos ao código!

Precisamos baixar a lib do socket.io para golang com esse comando no terminal da sua máquina:

go get github.com/googollee/go-socket.io

A documentação se encontra neste link.

Bom, feito isso agora é só começar a escrever! Crie um arquivo chamado main.go num diretório de sua preferência e abra ele no editor que você costuma usar.

Primeiro passo é colocar o packege e definir os imports que usaremos. Entre eles, o socket.io.

package main
import (
   "encoding/json"
   "log"
   "net/http"
   "github.com/googollee/go-socket.io"
)
...

Precisamos definir a estrutura da mensagem e do server que usaremos na comunicação. O customServer vai servir para conseguirmos manipular o CORS da aplicação. Sem isso você pode enfrentar alguns problemas ao colocar em algum servidor em cloud.

...
type Msg struct {
   Msg   string `json:"msg, omitempty"`
   Room  string `json:"room, omitempty"`
   Red   int    `json:"red, omitempty"`
   Green int    `json:"green, omitempty"`
   Blue  int    `json:"blue, omitempty"`
}
type CustomServer struct {
   Server *socketio.Server
}
...

Próximo passo é definir a função que irá manipular o CORS dessa maneira:

...
func (s *CustomServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   w.Header().Set("Access-Control-Allow-Credentials", "true")
   origin := r.Header.Get("Origin")
   w.Header().Set("Access-Control-Allow-Origin", origin)
   s.Server.ServeHTTP(w, r)
}
...

Estamos quase lá! Precisamos ainda da função main(). Essa é a função que é executada quando chamamos o arquivo main.go e onde está configurada a rota para o serviço ser chamado pelo cliente.

...
func main() {
   ioServer := configureSocketIO()
   wsServer := new(CustomServer)
   wsServer.Server = ioServer
   println("Serviço sendo escutado na porta 5000...")
   http.Handle("/socket.io/", wsServer)
   http.ListenAndServe(":5000", nil)
}
...

E, por fim, precisamos de uma última função para configurar todos os eventos do websocket.

...
func configureSocketIO() *socketio.Server {
   server, err := socketio.NewServer(nil)
   if err != nil {
      log.Fatal(err)
   }
   server.On("connection", func(so socketio.Socket) {
      log.Println("on connection")
      so.On("join", func(msg string) {
         so.Join(msg)
      })
      so.On("message", func(msg string) {
         msgS := Msg{}
         err := json.Unmarshal([]byte(msg), &msgS)
         if err == nil {
            log.Println(msgS.Room)
         }
         log.Println(msg)
         so.BroadcastTo(msgS.Room, "message", msgS)
      })
      so.On("disconnection", func() {
         log.Println("on disconnect")
      })
   })
   server.On("error", func(so socketio.Socket, err error) {
      log.Println("error:", err)
   })
   return server
}

Para ver se tudo está funcionando é só ir no diretório do main.go pelo terminal e digitar o seguinte comando:

go run main.go

O código completo para quem quiser usá-lo:

package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/googollee/go-socket.io"
)

// Msg é a definição da estrutura de mensagem que iremos passar em ambos os lados da aplicação
type Msg struct {
	Msg   string `json:"msg, omitempty"`
	Room  string `json:"room, omitempty"`
	Red   int    `json:"red, omitempty"`
	Green int    `json:"green, omitempty"`
	Blue  int    `json:"blue, omitempty"`
}

// CustomServer irá ser utilizado para facilitar a alteração do CORS do serviço
type CustomServer struct {
	Server *socketio.Server
}

// ServeHTTP é a funcção que irá substituir o CORS default do serviço
func (s *CustomServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Credentials", "true")
	origin := r.Header.Get("Origin")
	w.Header().Set("Access-Control-Allow-Origin", origin)
	s.Server.ServeHTTP(w, r)
}

// main é onde a aplicação é executada, é a primeira função que é chamada ao executar o arquivo main.go
func main() {
	ioServer := configureSocketIO()

	wsServer := new(CustomServer)
	wsServer.Server = ioServer

	println("Serviço sendo escutado na porta 5000...")
	http.Handle("/socket.io/", wsServer)
	http.ListenAndServe(":5000", nil)
}

// configureSocketIO é onde definimos os métodos do web socket, como connection por exemplo
func configureSocketIO() *socketio.Server {
	server, err := socketio.NewServer(nil)
	if err != nil {
		log.Fatal(err)
	}

	server.On("connection", func(so socketio.Socket) {

		log.Println("on connection")

		so.On("join", func(msg string) {
			so.Join(msg)
		})

		so.On("message", func(msg string) {
			msgS := Msg{}
			err := json.Unmarshal([]byte(msg), &msgS)
			if err == nil {
				log.Println(msgS.Room)
			}
			log.Println(msg)
			so.BroadcastTo(msgS.Room, "message", msgS)
		})

		so.On("disconnection", func() {
			log.Println("on disconnect")
		})
	})

	server.On("error", func(so socketio.Socket, err error) {
		log.Println("error:", err)
	})

	return server
}

Criando o client com Vue.js

Precisamos instalar algumas coisas aqui também para conseguirmos usar esse cara como CLI. Vamos lá!

Baixe o NodeJS no link: https://nodejs.org/en/ e realize a instalação de acordo com o manual do Node para seu sistema.

Abra o terminal e digite o seguinte comando:

npm install -g vue-cli

Feito isso, precisamos iniciar um novo projeto. Vá até um diretório de sua preferência e execute o comando:

vue init webpack realtimeapp

O webpack é o nosso template e o realtimeapp é o nome da aplicação.

Agora temos que instalar mais alguns pacotes com o npm para conseguirmos criar a aplicação. O primeiro deles é a lib UI que utilizei na aplicação. O nome da biblioteca é Vuetify. Segue o link:

Depois basta intalar a lib do socket.io da mesma forma que fizemos com o golang.

npm install vuetify --save

npm install material-design-icons-iconfont --save

npm install socket.io --save

Feito essas instalações, mãos a obra!

Abra a pasta criada com o vue no seu editor. Procure pela pasta src, a estrutura interna dela ficou desse jeito:

Vá na pasta router e abra o arquivo index.js. Altere o arquivo para ficar dessa forma:

import Vue from 'vue'
import Router from 'vue-router'
import CardsControl from '@/components/CardsControl'
import Control from '@/components/Control'
import MobileControl from '@/components/MobileControl'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'

Vue.use(Vuetify)

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'CardsControl',
      component: CardsControl
    },
    {
      path: '/controle',
      name: 'Control',
      component: Control
    },
    {
      path: '/mobile',
      name: 'Mobile',
      component: MobileControl
    }
  ]
})

Alguns erros podem aparecer, pois os componentes que foram importados ainda não foram criados. Vamos criar primeiramente o componente CardsControl.

Vá no diretório src/components/ e crie um arquivo chamado CadsControl.vue. Feito isso, deixe ele igual ao código abaixo.

<template>
<v-app id="chat1">
  <v-container>
    <v-layout row wrap justify-center>
      <v-flex xs12 md6 lg4 xl3>
        <v-layout row wrap justify-center>
          <v-flex xs12 sm5>
            <span class="text-xs-left">Altura do cartão é: {{ msg }}</span>
          </v-flex>
          <v-flex xs12 sm6 v-bind:style="{ marginTop: top + 'px' }">
            <v-card>
              <v-card-title primary-title>
                <div>
                  <h3 class="headline mb-0 text-xs-left">Primeiro card</h3>
                  <div class="text-xs-left">Escutando na sala CHAT</div>
                </div>
              </v-card-title>
            </v-card>
          </v-flex>
        </v-layout>
      </v-flex>
      <v-flex xs12 md6 lg4 xl3>
        <v-layout row wrap justify-center>
          <v-flex xs12 sm5>
            <span class="text-xs-left">Altura do cartão é: {{ msg1 }}</span>
          </v-flex>
          <v-flex xs12 sm6 v-bind:style="{ marginTop: top1 + 'px' }">
            <v-card>
              <v-card-title primary-title>
                <div>
                  <h3 class="headline mb-0 text-xs-left">Segundo card</h3>
                  <div class="text-xs-left">Escutando na sala CHAT1</div>
                </div>
              </v-card-title>
            </v-card>
          </v-flex>
        </v-layout>
      </v-flex>
    </v-layout>
  </v-container>
  <v-tooltip left>
    <v-btn fab dark large color="purple" id="button" slot="activator" @click="controle">
      <v-icon dark>videogame_asset</v-icon>
    </v-btn>
    <span>Acessar controle de cards</span>
  </v-tooltip>
</v-app>
</template>

<script>
import io from 'socket.io-client'
export default {
  name: 'CardsControl',
  data: () => ({
    msg: 'Aguardando...',
    msg1: 'Aguardando...',
    isStart: true,
    socket: io('http://192.168.200.88:5000'),
    // socket: io('https://websocketservice.vinicius.rs/socket.io/'),
    top: '15',
    top1: '15'
  }),
  methods: {
    controle () {
      window.open('/controle', '_blank', 'toolbar=yes,scrollbars=yes,resizable=yes,top=500,left=500,width=500,height=700')
    }
  },
  mounted () {
    var that = this
    that.socket.on('connect', function (val) {
      setTimeout(() => {
        that.socket.emit('join', 'chat', function () {})
      }, 2000)
      setTimeout(() => {
        that.socket.emit('join', 'chat1', function () {})
      }, 2000)
      that.socket.on('message', function (val) {
        switch (val.room) {
          case 'chat':
            that.top = val.msg
            that.msg = val.msg
            break
          case 'chat1':
            that.top1 = val.msg
            that.msg1 = val.msg
            break
          default:
        }
      })
    })
  }
}
</script>

<style scoped>
#button {
  position: fixed;
  bottom: 10px;
  right: 10px;
}
</style>

Crie mais dois arquivos:

Control.vue

<template>
<v-app id="app">
  <v-container>
    <v-layout row wrap justify-center align-center>
      <v-flex xs12 sm10 md8 lg5 xl4>
        <v-btn @click="start" name="button">START/RESTART</v-btn>
        <v-btn @click="pause" name="button">PAUSE</v-btn>
        <v-btn @click="resume" name="button">RESUME</v-btn>
        <v-container fluid grid-list-lg>
          <v-layout row wrap>
            <v-flex xs12>
              <v-subheader class="pl-0">Altura do card</v-subheader>
              <v-slider color="primary" :max="500" :min="1" v-model="slider" thumb-label="always"></v-slider>
            </v-flex>
          </v-layout>
        </v-container>
      </v-flex>
    </v-layout>
    <v-layout row wrap justify-center align-center>
      <v-flex xs12 sm10 md8 lg5 xl4>
        <v-btn @click="start1" name="button">START/RESTART</v-btn>
        <v-btn @click="pause1" name="button">PAUSE</v-btn>
        <v-btn @click="resume1" name="button">RESUME</v-btn>
        <v-container fluid grid-list-lg>
          <v-layout row wrap>
            <v-flex xs12>
              <v-subheader class="pl-0">Altura do card</v-subheader>
              <v-slider color="primary" :max="500" :min="1" v-model="slider1" thumb-label="always"></v-slider>
            </v-flex>
          </v-layout>
        </v-container>
      </v-flex>
    </v-layout>
  </v-container>
</v-app>
</template>

<script>
import io from 'socket.io-client'
export default {
  name: 'Control',
  data: () => ({
    socket: io('http://192.168.200.88:5000'),
    // socket: io('http://localhost:5000/socket.io/'),
    slider: 45,
    interval: null,
    val: 0,
    slider1: 45,
    interval1: null,
    val1: 0
  }),
  watch: {
    slider: function (val) {
      let o = {
        'msg': val.toString(),
        'room': 'chat'
      }
      this.socket.emit('message', JSON.stringify(o), function (data) {})
    },
    slider1: function (val) {
      let o = {
        'msg': val.toString(),
        'room': 'chat1'
      }
      this.socket.emit('message', JSON.stringify(o), function (data) {})
    }
  },
  methods: {
    start () {
      clearInterval(this.interval)
      this.val = 0
      this.interval = setInterval(() => {
        this.val++
        let o = {
          'msg': this.val.toString(),
          'room': 'chat'
        }
        this.socket.emit('message', JSON.stringify(o), function (data) {})
      }, 100)
    },
    pause () {
      clearInterval(this.interval)
    },
    resume () {
      clearInterval(this.interval)
      this.interval = setInterval(() => {
        this.val++
        let o = {
          'msg': this.val.toString(),
          'room': 'chat'
        }
        this.socket.emit('message', JSON.stringify(o), function (data) {})
      }, 100)
    },
    start1 () {
      clearInterval(this.interval1)
      this.val1 = 0
      this.interval1 = setInterval(() => {
        this.val1++
        let o = {
          'msg': this.val1.toString(),
          'room': 'chat1'
        }
        this.socket.emit('message', JSON.stringify(o), function (data) {})
      }, 100)
    },
    pause1 () {
      clearInterval(this.interval1)
    },
    resume1 () {
      clearInterval(this.interval1)
      this.interval1 = setInterval(() => {
        this.val1++
        let o = {
          'msg': this.val1.toString(),
          'room': 'chat1'
        }
        this.socket.emit('message', JSON.stringify(o), function (data) {})
      }, 100)
    }
  },
  mounted () {
    var that = this
    this.socket.on('connect', function (val) {
      setTimeout(() => {
        that.socket.emit('join', 'chat', function () {})
        that.socket.emit('join', 'chat1', function () {})
      }, 2000)
      that.socket.on('message', function (val) {
        // Connected, let's sign-up for to receive messages for this room
        // document.getElementById('image').src = 'data:image/gif;base64,' + val
      })
    })
  }
}
</script>

<!-- Add 'scoped' attribute to limit CSS to this component only -->
<style scoped>
.hello {
  width: 100%;
  height: 500px;
  position: absolute;
  /*it can be fixed too*/
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
  /*this to solve 'the content will not be cut when the window is smaller than the content': */
  max-width: 100%;
  max-height: 100%;
  overflow: auto;
}
</style>

MobileControl.vue

<template>
<v-app>
  <v-container>
    <v-card class="e4">
      <v-responsive :style="{ background: `rgb(${red}, ${green}, ${blue})` }" height="300px"></v-responsive>
      <v-card-text>
        <v-container fluid grid-list-lg>
          <v-layout row wrap>
            <v-flex xs9>
              <v-slider v-model="red" :max="255" label="R"></v-slider>
            </v-flex>

            <v-flex xs3>
              <v-text-field v-model="red" class="mt-0" type="number"></v-text-field>
            </v-flex>

            <v-flex xs9>
              <v-slider v-model="green" :max="255" label="G"></v-slider>
            </v-flex>

            <v-flex xs3>
              <v-text-field v-model="green" class="mt-0" type="number"></v-text-field>
            </v-flex>

            <v-flex xs9>
              <v-slider :max="255" v-model="blue" label="B"></v-slider>
            </v-flex>

            <v-flex xs3>
              <v-text-field v-model="blue" class="mt-0" type="number"></v-text-field>
            </v-flex>
          </v-layout>
        </v-container>
      </v-card-text>
    </v-card>
    <v-layout row wrap justify-center align-center>
      <v-flex xs12 sm10 md8 lg5 xl4>
        <v-btn @click="start" name="button">Show me the controls</v-btn>
        <v-btn @click="hide" name="button">Hide the controls</v-btn>
      </v-flex>
    </v-layout>
  </v-container>
</v-app>
</template>

<script>
import io from 'socket.io-client'
export default {
  data: () => ({
    socket: io('http://192.168.200.88:5000'),
    // socket: io('http://localhost:5000/socket.io/'),
    red: 64,
    green: 128,
    blue: 0
  }),
  watch: {
    red: function (val) {
      let o = {
        'msg': this.rgbToHex(val, this.green, this.blue),
        'room': 'mobileControl',
        'red': val,
        'green': this.green,
        'blue': this.blue
      }
      this.socket.emit('message', JSON.stringify(o), function (data) {})
    },
    green: function (val) {
      let o = {
        'msg': this.rgbToHex(this.red, val, this.blue),
        'room': 'mobileControl',
        'red': this.red,
        'green': val,
        'blue': this.blue
      }
      this.socket.emit('message', JSON.stringify(o), function (data) {})
    },
    blue: function (val) {
      let o = {
        'msg': this.rgbToHex(this.red, this.green, val),
        'room': 'mobileControl',
        'red': this.red,
        'green': this.green,
        'blue': val
      }
      this.socket.emit('message', JSON.stringify(o), function (data) {})
    }
  },
  methods: {
    componentToHex (c) {
      var hex = c.toString(16)
      return hex.length === 1 ? '0' + hex : hex
    },
    rgbToHex (r, g, b) {
      return '#' + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b)
    },
    start () {
      let o = {
        'msg': 'controls',
        'room': 'mobileControl'
      }
      this.socket.emit('message', JSON.stringify(o), function (data) {})
    },
    hide () {
      let o = {
        'msg': 'hide',
        'room': 'mobileControl'
      }
      this.socket.emit('message', JSON.stringify(o), function (data) {})
    }
  },
  mounted () {
    var that = this
    this.socket.on('connect', function (val) {
      setTimeout(() => {
        that.socket.emit('join', 'mobileControl', function () {})
        that.socket.emit('join', 'mobileControl1', function () {})
      }, 2000)
    })
  }
}
</script>

Por fim, altere o App.vue para ficar dessa forma:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>

Para ver se tudo está funcionando, abra o terminal no diretório do app e digite:

npm run dev

Criando o client com JAVA (Android)

Estamos quase no final! Baixe o Android Studio neste link.

Depois é só seguir o documento de instalação do site mesmo!

Inicie um novo projeto dentro do Android Studio. Vá em Gradle Scripts, build.gradle (Module: app). No final do arquivo tem um bloco chamado:

dependencies {
   ...
}

Dentro dele, adicione essa linha:

...
implementation 'com.github.nkzawa:socket.io-client:0.3.0'
...

Pronto! Com a biblioteca importada podemos escrever algumas linhas de código para a aplicação funcionar.

No arquivo activity_main.xml, adicione os seguintes códigos:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <android.support.v7.widget.CardView
        android:id="@+id/cardView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" >

    </android.support.v7.widget.CardView>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Uauu, the controls"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

No MainActivity.java, inclua o seguinte:

package br.com.websocket;

import android.app.Activity;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.CardView;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;

import org.json.JSONException;
import org.json.JSONObject;

public class MainActivity extends AppCompatActivity {

    private Socket mSocket;
    {
        try {
            mSocket = IO.socket("http://172.17.9.72:5000/socket.io/");
        } catch (java.net.URISyntaxException e) {}
    }

    private TextView textView;
    private CardView cardView;
    private Button button;

    private Emitter.Listener onNewMessage = new Emitter.Listener() {
        @Override
        public void call(final Object... args) {
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    JSONObject data = (JSONObject) args[0];
                    String username;
                    try {
                        username = data.getString("msg");
                    } catch (JSONException e) {
                        return;
                    }

                    if (username.equals("controls")){
                        button.setVisibility(View.VISIBLE);
                    } else if (username.equals("hide")){
                        button.setVisibility(View.GONE);
                    } else {
                        textView.setText(username);
                        cardView.setBackgroundColor(Color.parseColor(username));
                    }

                    // add the message to view
                    //addMessage(username, message);
                }
            });
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.textView = findViewById(R.id.textView);
        this.cardView = findViewById(R.id.cardView);
        this.button = findViewById(R.id.button);

        this.button.setVisibility(View.GONE);

        this.cardView.setBackgroundColor(Color.parseColor("#636161"));

        mSocket.on("message", onNewMessage);
        mSocket.connect();

        mSocket.emit("join", "mobileControl");

    }

    public Activity getActivity() {
        return this;
    }
}

Execute o código no seu Android ou em um emulador. Rode o server golang com o comando:

go run main.go

Inicie o client do Vue com:

npm run dev

Tudo pronto! Agora seu serviço de web socket está configurado e pronto para ser usado de acordo com o que sua imaginação mandar.

Com a biblioteca do socket.io conseguimos facilitar muito a comunicação entre as linguagens e garantir as operações praticamente em tempo real. Não que usar websockets sem o auxilio da biblioteca também não seja algo possível e fácil. O socket.io auxilia principalmente quando precisamos configurar salas diferentes para cada conexão, algo que é um pouco mais complicado ao optar por não usar nada que ajude nesse tipo de serviço.