19

Creación de un chat con websocket con Go y Gorilla

Osmandi
osmandi
45018

Este es el objetivo:


Screenshot_20171204_054701.png

Contenido


  • ¿Qué es un websocket?
  • ¿Qué es Go y Gorilla?
  • ¿Qué es Docker y Now.sh?
  • Desarrollo del chat web socket
<h1>¿Qué es un websocket?</h1>

Un websocket es una conexión en tiempo real bidireccional entre un cliente y un servidor. Es muy utilizado para chat pero no es el único caso de uso. Otro serían los GPS.

“WebSocket es una tecnología que proporciona un canal de comunicación bidireccional y full-duplex sobre un único socket TCP. Está diseñada para ser implementada en navegadores y servidores web, pero puede utilizarse por cualquier aplicación cliente/servidor.” Wikipedia

Websocket con javascript

Para implementar websocket con JavasScript e inicar una conexión desde el navegador

var ws = new Websocket("ws://example.com/ws");

Esto envía una solicitud http, pero si usas una conexión segura debes usar "wss://"

WebSocket con Go

Los websockets no vienen incluidos en la biblioteca estándar de Go. Pero hay paquetes de terceros muy fáciles de conseguir y que hacen un excelente trabajo. El que usaremos es “gorilla/websocket”. Para instalarlo solo agregar en la terminal:

goget github.com/gorilla/websocket

¿Qué es Go y Gorilla?


<dl>

<dt>Go</dt>

<dd>Es un lenguaje de programación de código abierto orientado a eventos inspirado en C y creado por Google. golang.org</dd>

<dt>Gorilla</dt>

<dd>Es un conjunto de herramientas para el desarrollo web con Go. Se puede combinar con cualquier framework. http://www.gorillatoolkit.org/</dd>

</dl>

¿Qué es Docker y Now.sh?


<dl>

<dt>Docker</dt>

<dd>Es un programa de código abierto hecho en Go que crea un entorno virtualizado llamado contenedor. Facilita en gran medida el despliegue de las aplicaciones desde el desarrollo a producción resolviendo el problema de las dependencias y también muy usado en microservicios. Platzi tiene un excelente curso de docker que recomiendo mucho 😄</dd>

<dt>Now.sh</dt>

<dd>Now.sh es una plataforma como servicio (PaaS) que permite el despliegue de aplicaciones en NodeJS y aplicaciones Docker. El implementar Docker significa que puedes hacer deploy de cualquier lenguaje. Una de las ventajas de Now.sh es que en su versión gratuita puedes hacer tanto deploy quieras, pero debes estar consciente que todo lo que subas con ese plan será de código abierto. Platzi igualmente tiene un excelente curso de esto que también recomiendo mucho.</dd>

</dl>

Desarrollo del chat websocket


Lo primero que debemos hacer es crear las carpetas de nuestro proyecto.

Es buena práctica hacer todos nuestros programas de go en ***~/go/src/***, asumiendo que estás allí ingresa el la terminal lo siguiente

mkdir chat-go && cd chat-go && mkdir public && mkdir src

  • chat-go: La carpeta de nuestro proyecto.
  • chat-go/public: Donde se guardará el frontend.
  • chat-go/src: Donde se guardará nuestro código en Go, es decir el backend.

Crear el archivo src/main.go con el editor de tu preferencia.

package main

// Importamos las librerías necesarias, aunque con solo guardar se importan automáticamente :Dimport (
	"log""net/http""github.com/gorilla/websocket"
)

// La primera variable es un mapa donde la clave es en realidad un puntero a un WebSocket, el valor es un booleano.// La segunda variable es un canal que actuará como una cola de mensajes enviados por los clientes.var clients = make(map[*websocket.Conn]bool) // Connected clientsvar broadcast = make(chan Message)           // Broadcast channel// Este es solo un objeto con métodos para tomar una conexión HTTP normal y actualizarla a un WebSocket var upgrader = websocket.Upgrader{}

// Definiremos un objeto para guardar nuestros mensajes, para interactuar con el servicio ***Gravatar*** que nos proporcionará un avatar único.type Message struct {
	Email    string`json:"email"`
	Username string`json:"username"`
	Message  string`json:"message"`
}

// Esta función manejará nuestras conexiones WebSocket entrantesfunc handleConnections(w http.ResponseWriter, r *http.Request) {

	// El método Upgrade() permite cambiar nuesra solicitud GET inicial a una completa en WebSocket, si hay un error lo mostramos en consola pero no salimos.
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Fatal(err)
	}

	// Para cerrar la conexión una vez termina la funcióndefer ws.Close()

	// Registramos nuestro nuevo cliente al agregarlo al mapa global de "clients" que fue creado anteriormente.
	clients[ws] = true// Bucle infinito que espera continuamente que se escriba  un nuevo mensaje en el WebSocket, lo desserializa de JSON a un objeto Message y luego lo arroja al canal de difusión.for {
		var msg Message

		// Read in a new message as JSON and map it to a Message object// Si hay un error, registramos ese error y eliminamos ese cliente de nuestro mapa global de clients
		err := ws.ReadJSON(&msg)
		if err != nil {
			log.Printf("error: %v", err)
			delete(clients, ws)
			break
		}

		// Send the newly received message to the broadcast channel
		broadcast <- msg
	}
}

// Ciclo que lee continuamente desde el canal broadcast y luego transmite el mesaje a todos nuestros clientes a través de su respectiva conexión WebSocket. Si hay un error cerramos la conexión y elminamos del mapa "clients".func handleMessages() {
	for {
		// Grab the next message from the broadcast channel
		msg := <-broadcast

		// Send it out to every client that is currently connectedfor client := range clients {
			err := client.WriteJSON(msg)
			if err != nil {
				log.Printf("error: %v", err)
				client.Close()
				delete(clients, client)
			}
		}
	}
}

func main() {
	// Para que al entrar en la aplicación el cliente tome el index.html
	fs := http.FileServer(http.Dir("../public"))
	http.Handle("/", fs)

	// "/ws" es la ruta que nos ayudará a manejar cualquier solicitud para iniciar un WebSocket.
	http.HandleFunc("/ws", handleConnections)

	// Creamos una goroutina que nos ayudará a manejar los mensajesgo handleMessages()

    // Iniciamos el servidor en localhost en el puerto 8080 y un mensaje que muetre por consola
	log.Println("http server started on :8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

Para el FrontEnd usaremos HTML5 y VueJS

Archivo public/index.html


<htmllang= "en">
<head>
    <metacharset= "UTF-8">
    <title>Simple chattitle>

    <linkrel ="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css">
    <linkrel ="stylesheet"href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <linkrel ="stylesheet"href="https://cdn.jsdelivr.net/emojione/2.2.6/assets/css/emojione.min.css"/>
    <linkrel ="stylesheet"href="/style.css">
head>

<body>
    <header>
        <nav>
            <divclass="nav-wrapper">
                <ahref="/"class="brand-logo right">Simple Chata>
            div>
        nav>
    header>
    <mainid="app">
        <divclass="row">
            <divclass="col s12">
                <divclass="card horizontal">
                    <divid="chat-messages"class="card-content"v-html="chatContent">
                    div>
                div>
            div>
        div>
        <divclass="row"v-if="joined">
            <divclass="input-field col s8">
                <inputtype="text"v-model="newMsg" @keyup.enter="send">
            div>
            <divclass="input-field col s4">
                <buttonclass="waves-effect waves-light btn" @click="send">
                    <iclass="material-icons right">chati>
                    Send
                button>
            div>
        div>
        <divclass="row"v-if="!joined">
            <divclass="input-field col s8">
                <inputtype="email"v-model.trim="email"placeholder="Email">
            div>
            <divclass="input-field col s8">
                <inputtype="text"v-model.trim="username"placeholder="Username">
            div>
            <divclass="input-field col s4">
                <buttonclass="waves-effect waves-light btn" @click="join()">
                    <iclass="material-icons right">donei>
                    Join
                button>
            div>
        div>
    main>
    <footerclass="page-footer">
    footer>

    <scriptsrc="https://unpkg.com/[email protected]/dist/vue.js">script>
    <scriptsrc="https://cdn.jsdelivr.net/emojione/2.2.6/lib/js/emojione.min.js">script>
    <scriptsrc="https://code.jquery.com/jquery-2.1.1.min.js">script>
    <scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/md5.js">script>
    <scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js">script>
    <scriptsrc="/app.js">script>
body>
html>

Para public/app.js

new Vue({
    el: '#app',

    data:{
        ws: null, // Our websocket
        newMsg: '', // Holds new messages to be sent to the server
        chatContent: '', // A running list of chat messages displayed on the screen
        email: null, // Email addres used for grabbing an avatar
        username: null, // Our username
        joined: false//True if email and username have been filled in
    },

    created: function(){
        var self = this;
        this.ws = new WebSocket('wss://' + window.location.host + '/ws');
        this.ws.addEventListener('message', function(e){
            var msg = JSON.parse(e.data);
            self.chatContent += '' 
                + ''">'// Avatar
                + msg.username
                + ''
                + emojione.toImage(msg.message) + ''; // Parse emojis

            var element = document.getElementById('chat-messages');
            element.scrollTop = element.scrollHeight; // Auto scroll to the bottom

        });            
    },

    methods: {
        send: function(){if(this.newMsg != ''){
                this.ws.send(
                    JSON.stringify({
                        email: this.email,
                        username: this.username,
                        message: $('').html(this.newMsg).text() // Strip out html

                    }
                ));
                this.newMsg = ''; // Reset newMsg
            }
        },

        join: function(){if(!this.email){
                Materialize.toast('You must enter an email', 2000);
                return
            }
            if (!this.username){
                Materialize.toast('You must choose a username', 2000);
                return
            }
            this.email = $('').html(this.email).text();
            this.username = $('

Para public/style.css

body{
    display: flex;
    min-height: 100vh;
    flex-direction: column;
}

main{
    flex: 10 auto;
}

#chat-messages{
    min-height: 10vh;
    height: 60vh;
    width: 100%;
}

Correr el localhost

A partir de aquí ya debes poder correr en localhost, para ello debes estar dentro de la carpeta chat-go/src e ingresar en la terminal

go run main.go

Ingresas en el navegador localhost:8080 y si todo salió bien tienes tu chat en Go y Gorilla con WebSocket lista

Implementando Docker

Archivo Dockerfile

FROM golang
EXPOSE8080
WORKDIRgo/src
COPY./ .
CMDgo get github.com/gorilla/websocket && cd src && go run main.go

  • FROM: Imagen de Docker Golang a usar.
  • EXPOSE: Puerto expuesto desde el contenedor.
  • WORKDIR: Carpeta donde se ubicará el contenedor al instanciarse.
  • COPY: Todo el código lo vamos a incluir al contenedor con esta orden.
  • CMD: Comando que se ejecuta por defecto al iniciar el contenedor. En este caso, descarga la librería gorilla/websocket y ejecuta la aplicación.

Deploy a now.sh

Es preferible usar Now por terminal, haciendo así te ubicas dentro de la carpeta chat-go e ingresas en la terminal

Nota Si vas a hacer deploy a now.sh, debes cambiar al protocolo seguro, es decir de ws:// a wss://

now --docker

Conclusiones


  • EL desarrollo fue bastante sencillo y con pocas líneas de código, algo que es muy significativo cuando se esté desarrollando aplicaciones más complejas.
  • El lapso entre enviar el mensaje a que sea distribuido entre todos los clientes es bastante corto, sin embargo no he realizado ninguna prueba de rendimiento.
  • La compilación y ejecución de la aplicación sólo toma unos milisegundos.
  • Repo de GitHub
  • https://chat-go-v2-iudthzulys.now.sh/

Notas:

  • Estoy usando una versión gratuita de Now.sh así que si nadie está usando la aplicación tardará un poco en cargar, pero solo unos segundos.
  • Si colocas tu correo real, aparecerá tu avatar por el servicio gravatar.
  • Puedes agregar emojis fácilmente con https://es.piliapp.com/facebook-symbols/.
  • Puedes chatear con cualquiera que esté conectado en el momento.
  • Si no hay nadie conectado, puedes abrir varias ventanas en el mismo navegador.
  • Al momento de escribir este tutorial, no hay respaldo de contenido de la conversación. Al refrescar la página pierdes lo que has escrito.
  • Otro detalle es que hay un pequeño error en el FrontEnd cuando el chat se extiende, he estado tocando un par de cosas buscando cómo funciona y viendo en qué parte falla la aplicación. Así que mejor lo dejo así y continúo mis pruebas en otro contenedor, cuando encuentre la falla arreglo este. Lo siento por eso 😛
  • Si vas a hacer este tutorial, no copies y pegues hazlo por tu propia cuenta. Si tienes muchos errores, tranquilo(a) es normal cuando se esta aprendiendo algo nuevo 😉

Déjame tu comentario, ¿qué te pareció? ¿Llevaste a cabo el tutorial?

Realiza un chat websocket con tu lenguaje de BackEnd preferido y me cuentas tu experiencia

Saludos y gracias por leer este tutorial 😄

Referencias y créditos


Escribe tu comentario
+ 2