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.