Skip to content

Getting started

Gerasimos (Makis) Maropoulos edited this page Jun 28, 2019 · 25 revisions

Neffos provides a comprehensive API to work with. Identical API and keywords between server and client sides.

Let's start by building a small echo application between server and a client. Client will send something and server will respond with a prefix of "echo back: ".

Supposedly we have both server and client side in the same project (or package).

Create a new echo.go file.

package main

Import the neffos go package and, optionally, the log standard go package in order to help us print incoming messages.

import (
    "log"

    "github.com/kataras/neffos"
)

Register events

Incoming events are being captured through callbacks. A callback declaration is a type of func(*neffos.NSConn, neffos.Message) error.

Note that in this example, we share the same event's callback across client and server sides, depending on your app's requirements you may want to separate those events.

func onEcho(c *neffos.NSConn, msg neffos.Message) error {
    body := string(msg.Body)
    log.Println(body)

    if !c.Conn.IsClient() {
        // this block will only run on a server-side callback.
        newBody := append([]byte("echo back: "), msg.Body...)
        return neffos.Reply(newBody)
    }

    return nil
}

The neffos.Reply is just a helper function which writes back to the connection the same incoming neffos.Message with a custom body data.

Alternatively you may Emit a remote event, depending on your app's needs, this is how:

c.Emit("echo", newBody)

Which is the same as:

c.Conn.Write(neffos.Message{Namespace: "v1", Event: "echo", Body: newBody})

Callbacks are registered via the Namespaces & Events event-driven API. Let's add an "echo" event on a namespace called "v1". Note that Events can be registered without a namespace as well but it's highly recommented that you put them under a non-empty namespace for future maintainability.

var events = neffos.Namespaces{
    "v1": neffos.Events{
        "echo": onEcho,
    },
}

Create the Client

A neffos (Go) client expects a compatible neffos.Dialer to dial the neffos websocket server.

Among the neffos import statement, add one of the two built-in compatible Upgraders & Dialers. For example the neffos/gorilla sub-package helps us to adapt the gorilla websocket implementation into neffos.

Read more about Upgraders and Dialers.

import (
    // [...]
    "github.com/kataras/neffos/gorilla"
)

Creating a websocket client is done by the Dial package-level function.

The Dial function accepts a context.Context as its first parameter which you can use to manage dialing timeout.

The url as its third input parameter is the endpoint which our websocket server waits for incoming requests, i.e ws://localhost:8080/echo.

The final parameter you have to pass to create both client and server is a ConnHandler (Events or Namespaces with Events).

ctx := context.Background()
client, err := neffos.Dial(ctx, gorilla.DefaultDialer, "ws://localhost:8080/echo", events)

Connect the Client to a Namespace

The client must be connected to one or more server namespaces (even if a namespace is empty "") in order to initialize the communication between them.

Let's connect our client to the "v1" namespace and emit the remote (in this case, server side's) "echo" event with the data of "Greeetings!".

c, err := client.Connect(ctx, "v1")
c.Emit("echo", []byte("Greetings!"))

The client's onEcho will be fired and it will print the message of echo back: Greetings!.

Note that event emitting and firing are asynchronous operations, if you exit the program immediately after it, there is not enough time to fire back the "echo" event that the server-side remotely fired from its side.

However, if you need to block until response is received use the c.Ask instead:

responseMessage, err := c.Ask(ctx, "echo", []byte("Greetings!"))

In that case, the responseMessage.Body is expected to be []byte("echo back: Greetings!").

Create the Server

Creating a websocket server is done by the neffos.New package-level function.

A neffos server expects a compatible neffos.Upgrader to upgrade HTTP incoming connection to WebSocket.

websocketServer := neffos.New(gorilla.DefaultUpgrader, events)

Run the Server

A websocket server should run inside an http endpoint, so clients can connect. We need to create an http web server and add a route which will serve and upgrade the incoming requests using the neffos server. For that, you can choose between standard package net/http and a high-performant package like iris.

The neffos.Server completes the http.Handler interface.

func (*Server) ServeHTTP(http.ResponseWriter, *http.Request)

Assuming that we want our websocket clients to communicate with our websocket server through the localhost:8080/echo endpoint, follow the below guide.

If you choose net/http, this is how you register the neffos server to an http endpoint:

import (
    // [...]
    "net/http"
)

func main() {
    // [websocketServer := neffos.New...]
    router := http.NewServeMux()
    router.Handle("/echo", websocketServer)

    http.ListenAndServe(":8080", router)
}

Otherwise install iris using the go get -u github.com/kataras/iris terminal command. Iris has its own adapter for neffos, it lives inside its iris/websocket sub-package, we need to import that as well.

import (
    // [...]
    "github.com/kataras/iris"
    "github.com/kataras/iris/websocket"
)

func main() {
    // [websocketServer := neffos.New...]
    app := iris.New()
    app.Get("/echo", websocket.Handler(websocketServer))

    app.Run(iris.Addr(":8080"))
}

Putting it all together

The final program, which contains both server and client side is just 75 lines of code.

package main

import (
    "context"
    "log"
    "net/http"
    "os"

    "github.com/kataras/neffos"
    "github.com/kataras/neffos/gorilla"
)

var events = neffos.Namespaces{
    "v1": neffos.Events{
        "echo": onEcho,
    },
}

func onEcho(c *neffos.NSConn, msg neffos.Message) error {
    body := string(msg.Body)
    log.Println(body)

    if !c.Conn.IsClient() {
        newBody := append([]byte("echo back: "), msg.Body...)
        return neffos.Reply(newBody)
    }

    return nil
}

func main() {
    args := os.Args[1:]
    if len(args) == 0 {
        log.Fatalf("expected program to start with 'server' or 'client' argument")
    }
    side := args[0]

    switch side {
    case "server":
        runServer()
    case "client":
        runClient()
    default:
        log.Fatalf("unexpected argument, expected 'server' or 'client' but got '%s'", side)
    }
}

func runServer() {
    websocketServer := neffos.New(gorilla.DefaultUpgrader, events)

    router := http.NewServeMux()
    router.Handle("/echo", websocketServer)

    log.Println("Serving websockets on localhost:8080/echo")
    log.Fatal(http.ListenAndServe(":8080", router))
}

func runClient() {
    ctx := context.Background()
    client, err := neffos.Dial(ctx,gorilla.DefaultDialer,"ws://localhost:8080/echo",events)
    if err != nil {
        panic(err)
    }

    c, err := client.Connect(ctx, "v1")
    if err != nil {
        panic(err)
    }

    c.Emit("echo", []byte("Greetings!"))

    // a channel that blocks until client is terminated,
    // i.e by CTRL/CMD +C.
    <-client.NotifyClose
}