Skip to content

Encoding

Gerasimos (Makis) Maropoulos edited this page Jun 29, 2019 · 2 revisions

With neffos you can send any type of data, remember? The neffos.Message.Body field is just a []byte. At short, the neffos.Message.Body is the raw data client/server sends, users of the neffos package can use any format to unmarshal on read and marshal to send, such as protocol-buffers, encoding/json, encoding/xml and etc.

In this section you will learn how to send JSON data per Room using the Emit with neffos.Marshal and how to read those data inside an event callback's with the neffos.Message.Unmarshal method. The neffos.DefaultMarshaler/Unnmarshaler will be used on neffos.Marshal and neffos.Message.Unmarshal if the value you are trying to send/read does not complete the neffos.MessageObjectMarshaler on send and neffos.MessageObjectUnmarshaler on read, therefore the default encoding that neffos using for struct values is the JSON one. Below you will find a brief outline of the above:

package neffos

var (
    DefaultMarshaler = json.Marshal
    DefaultUnmarshaler = json.Unmarshal
)

type (
    MessageObjectMarshaler interface {
        Marshal() ([]byte, error)
    }

    MessageObjectUnmarshaler interface {
        Unmarshal(body []byte) error
    }
)

type Message struct {
    // [...]
}

func (m *Message) Unmarshal(outPtr interface{}) error {
    if unmarshaler, ok := outPtr.(MessageObjectUnmarshaler); ok {
        return unmarshaler.Unmarshal(m.Body)
    }

    return DefaultUnmarshaler(m.Body, outPtr)
}

Create our userMessage structure

// file: main.go

// userMessage implements the `neffos.MessageBodyMarshaler` `neffos.MessageBodyUnmarshaler`.
type userMessage struct {
    From string `json:"from"`
    Text string `json:"text"`
}

// Defaults to `DefaultUnmarshaler & DefaultMarshaler` that are calling the json.Unmarshal & json.Marshal respectfully
// if the instance's Marshal and Unmarshal methods are missing,
// however for the example shake we complete the MessageBodyMarshaler and MessageBodyUnmarshaler too,
// these can help you customize the out and in format.
func (u *userMessage) Marshal() ([]byte, error) {
    return json.Marshal(u)
}

func (u *userMessage) Unmarshal(b []byte) error {
    return json.Unmarshal(b, u)
}

Define the server and client events

// file: main.go

var serverAndClientEvents = neffos.Namespaces{
    namespace: neffos.Events{
        neffos.OnNamespaceConnected: func(c *neffos.NSConn, msg neffos.Message) error {
            log.Printf("[%s] connected to namespace [%s].", c, msg.Namespace)
            return nil
        },
        neffos.OnNamespaceDisconnect: func(c *neffos.NSConn, msg neffos.Message) error {
            log.Printf("[%s] disconnected from namespace [%s].", c, msg.Namespace)
            return nil
        },

        neffos.OnRoomJoined: func(c *neffos.NSConn, msg neffos.Message) error {
            text := fmt.Sprintf("[%s] joined to room [%s].", c, msg.Room)
            log.Printf("\n%s", text)

            // notify others.
            if !c.Conn.IsClient() {
                c.Conn.Server().Broadcast(c, neffos.Message{
                    Namespace: msg.Namespace,
                    Room:      msg.Room,
                    Event:     "notify",
                    Body:      []byte(text),
                })
            }

            return nil
        },
        neffos.OnRoomLeft: func(c *neffos.NSConn, msg neffos.Message) error {
            text := fmt.Sprintf("[%s] left from room [%s].", c, msg.Room)
            log.Printf("\n%s", text)

            // notify others.
            if !c.Conn.IsClient() {
                c.Conn.Server().Broadcast(c, neffos.Message{
                    Namespace: msg.Namespace,
                    Room:      msg.Room,
                    Event:     "notify",
                    Body:      []byte(text),
                })
            }

            return nil
        },

        "chat": func(c *neffos.NSConn, msg neffos.Message) error {
            if !c.Conn.IsClient() {
                c.Conn.Server().Broadcast(c, msg)
            } else {
                var userMsg userMessage
                err := msg.Unmarshal(&userMsg)
                if err != nil {
                    log.Fatal(err)
                }
                fmt.Printf("%s >> [%s] says: %s\n", msg.Room, userMsg.From, userMsg.Text)
            }
            return nil
        },
        // client-side only event to catch any server messages comes from the custom "notify" event.
        "notify": func(c *neffos.NSConn, msg neffos.Message) error {
            if !c.Conn.IsClient() {
                return nil
            }

            fmt.Println(string(msg.Body))
            return nil
        },
    },
}

Create and run our Server

// file: main.go

func startServer() {
    server := neffos.New(gobwas.DefaultUpgrader, serverAndClientEvents)
    server.IDGenerator = func(w http.ResponseWriter, r *http.Request) string {
        if userID := r.Header.Get("X-Username"); userID != "" {
            return userID
        }

        return neffos.DefaultIDGenerator(w, r)
    }

    server.OnUpgradeError = func(err error) {
        log.Printf("ERROR: %v", err)
    }

    server.OnConnect = func(c *neffos.Conn) error {
        if c.WasReconnected() {
            log.Printf("[%s] connection is a result of a client-side re-connection, with tries: %d", c.ID(), c.ReconnectTries)
        }

        log.Printf("[%s] connected to the server.", c)

        // if returns non-nil error then it refuses the client to connect to the server.
        return nil
    }

    server.OnDisconnect = func(c *neffos.Conn) {
        log.Printf("[%s] disconnected from the server.", c)
    }

    log.Printf("Listening on: %s\nPress CTRL/CMD+C to interrupt.", addr)
    http.Handle("/", http.FileServer(http.Dir("./browser")))
    http.Handle(endpoint, server)
    log.Fatal(http.ListenAndServe(addr, nil))
}

Create our (Go) Client side

// file: main.go

func startClient() {
    scanner := bufio.NewScanner(os.Stdin)

    fmt.Print("Please specify a username: ")
    if !scanner.Scan() {
        return
    }
    username := scanner.Text()

    // init the websocket connection by dialing the server.
    client, err := neffos.Dial(
        // Optional context cancelation and deadline for dialing.
        nil,
        // The underline dialer, can be also a gobwas.Dialer/DefautlDialer or a gorilla.Dialer/DefaultDialer.
        // Here we wrap a custom gobwas dialer in order to send the username among, on the handshake state,
        // see `startServer().server.IDGenerator`.
        gobwas.Dialer(gobwas.Options{Header: gobwas.Header{"X-Username": []string{username}}}),
        // The endpoint, i.e ws://localhost:8080/path.
        addr+endpoint,
        // The namespaces and events, can be optionally shared with the server's.
        serverAndClientEvents)

    if err != nil {
        log.Fatal(err)
    }

    defer client.Close()

    go func() {
        <-client.NotifyClose
        os.Exit(0)
    }()

    // connect to the "default" namespace.
    c, err := client.Connect(nil, namespace)
    if err != nil {
        log.Fatal(err)
    }
    askRoom:
    fmt.Print("Please specify a room to join, i.e room1: ")
    if !scanner.Scan() {
        log.Fatal(scanner.Err())
    }
    roomToJoin := scanner.Text()

    room, err := c.JoinRoom(nil, roomToJoin)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Fprint(os.Stdout, ">> ")

    for {
        if !scanner.Scan() {
            log.Printf("ERROR: %v", scanner.Err())
            break
        }

        text := scanner.Text()

        if text == "exit" {
            break
        }

        if text == "leave" {
            room.Leave(nil)
            goto askRoom
        }

        // username is the connection's ID ==
        // room.String() returns -> NSConn.String() returns -> Conn.String() returns -> Conn.ID()
        // which generated by server-side via `Server#IDGenerator`.
        userMsg := userMessage{From: username, Text: text}
        room.Emit("chat", neffos.Marshal(userMsg))

        fmt.Fprint(os.Stdout, ">> ")
    }
}

Create our (Javascript browserify) Client side

In this example, we make an exception and we include the browser-side as well, so you can have a small taste of it.

Read more about neffos.js.

// file: browser/app.js

const neffos = require('neffos.js');

var scheme = document.location.protocol == "https:" ? "wss" : "ws";
var port = document.location.port ? ":" + document.location.port : "";
var wsURL = scheme + "://" + document.location.hostname + port + "/echo";

var outputTxt = document.getElementById("output");

function addMessage(msg) {
    outputTxt.innerHTML += msg + "\n";
}

function handleError(reason) {
    console.log(reason);
    window.alert(reason);
}

class UserMessage {
    constructor(from, text) {
        this.from = from;
        this.text = text;
    }
}


async function handleNamespaceConnectedConn(nsConn) {
    const roomToJoin = prompt("Please specify a room to join, i.e room1: ");
    nsConn.joinRoom(roomToJoin);

    let inputTxt = document.getElementById("input");
    let sendBtn = document.getElementById("sendBtn");

    sendBtn.disabled = false;
    sendBtn.onclick = function () {
        const input = inputTxt.value;
        inputTxt.value = "";

        switch (input) {
            case "leave":
                nsConn.room(roomToJoin).leave();
                // or room.leave(); 
                break;
            default:
                const userMsg = new UserMessage(nsConn.conn.ID, input);
                nsConn.emit("chat", neffos.marshal(userMsg));
                addMessage("Me: " + input);
        }
    };
}

async function runExample() {
    // You can omit the "default" and simply define only Events, the namespace will be an empty string"",
    // however if you decide to make any changes on this example make sure the changes are reflecting inside the ../server.go file as well.
    try {
        const username = prompt("Please specify a username: ");

        const conn = await neffos.dial(wsURL, {
            default: { // "default" namespace.
                _OnNamespaceConnected: function (nsConn, msg) {
                    addMessage("connected to namespace: " + msg.Namespace);
                    handleNamespaceConnectedConn(nsConn);
                },
                _OnNamespaceDisconnect: function (nsConn, msg) {
                    addMessage("disconnected from namespace: " + msg.Namespace);
                },
                _OnRoomJoined: function (nsConn, msg) {
                    addMessage("joined to room: " + msg.Room);
                },
                _OnRoomLeft: function (nsConn, msg) {
                    addMessage("left from room: " + msg.Room);
                },
                notify: function (nsConn, msg) {
                    addMessage(msg.Body);
                },
                chat: function (nsConn, msg) { // "chat" event.
                    const userMsg = msg.unmarshal()
                    addMessage(userMsg.from + ": " + userMsg.text);
                }
            }
        }, {
            headers: {
                'X-Username': username
            },
            // if > 0 then on network failures it tries to reconnect every 5 seconds, defaults to 0 (disabled).
            reconnect: 5000
        });

        conn.connect("default");

    } catch (err) {
        handleError(err);
    }
}

runExample();

Third-party Requirements

How to run

Open a terminal window instance and execute:

$ cd ./browser
# build the browser-side client: ./browser/bundle.js which ./browser/index.html imports.
$ npm install && npm run-script build 
$ cd ../
$ go run main.go server # start the neffos websocket server.

Open some web browser windows and navigate to http://localhost:8080, each window will ask for a username and a room to join, each window(client connection) and server get notified for namespace connected/disconnected, room joined/left and chat events.

To start the go client side just open a new terminal window and execute:

$ go run main.go client

It will ask you for username and a room to join as well, it acts exactly the same as the ./browser/app.js browser-side application.

Read the full source code of this example by navigating to the repository's _examples/basic directory.

You can continue by learning how to send and receive Protobufs.