Skip to content

Getting started

Gerasimos (Makis) Maropoulos edited this page Jul 5, 2019 · 25 revisions

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

Here is a quick outline of the flow you'll use:

All features are always in-sync between server and client side connections, each side gets notified of mutations.

End-developers can implement deadlines to actions like Conn#Connect, Conn#Ask, NSConn#Disconnect, NSConn#Ask, NSConn#JoinRoom, Room#Leave and etc. Methods that have to do with remote side response accept a context.Context as their first argument.

Dialing

  1. Client connection is initialized through the neffos.Dial (or neffos.dial on javascript side) method.
  2. Server can dismiss with an error any incoming client through its server.OnConnect -> return err != nil callback.

Send data through events

  1. Emit sends data and fires a specific event back to the remote side.
  2. A client can NOT communicate directly to the rest of the clients.
  3. Server is the only one which is able to send messages to one or more clients.
  4. To send data to all clients use the Conn.Server().Broadcast method. If its first argument is not nil then it sends data to all except that one connection ID.

Now, 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,
    },
}

Using a struct value

You can also register events based on a Go structure using the NewStruct function which returns a compatible neffos.ConnHandler. Like the neffos.Namespaces, neffos.Events and neffos.WithTimeout can be passed on New and Dial package-level functions.

Example Code:

Please read the comments.

type serverConn struct {
    // Dynamic field, a new "serverConn" instance is
    // created on each new connection to this namespace.
    Conn *neffos.NSConn
    // A static field is allowed, if filled before server ran then
    // is set on each new "serverConn" instance.
    SuffixResponse string
}

func (c *serverConn) OnChat(msg neffos.Message) error {
    c.Conn.Emit("ChatResponse", append(msg.Body, []byte(c.SuffixResponse)...))
    return nil
}

type clientConn struct {
    Conn *neffos.NSConn
}

func (s *clientConn) ChatResponse(msg neffos.Message) error {
    log.Printf("Echo back from server: %s", string(msg.Body))
    return nil
}
func startServer() {
    controller := new(serverConn)
    controller.SuffixResponse = " Static Response Suffix for shake of the example"

    // This will convert a structure to neffos.Namespaces based on the struct's methods.
    // The methods can be func(msg neffos.Message) error if
    // the structure contains a *neffos.NSConn field,
    // otherwise they should be like any event callback:
    // func(nsConn *neffos.NSConn, msg neffos.Message) error.
    // If contains a field of type *neffos.NSConn then a new controller is
    // created on each new connection to this namespace
    // and static fields(if any) are set on runtime with the NSConn itself.
    // If it's a static controller (does not contain a NSConn field)
    // then it just registers its functions as regular events without performance cost.
    events := neffos.NewStruct(controller).
        // This sets for the "default" namespace,
        // alternatively you can add a `Namespace() string` to the serverConn struct
        // or leave it empty for empty namespace.
        SetNamespace("default").
        // Optionally, sets read and write deadlines on the underlying network connection.
        // After a read or write have timed out, the websocket connection is closed.
        // For example:
        // If a client or server didn't receive or sent something
        // for 20 seconds this connection will be terminated.
        SetTimeouts(20*time.Second, 20*time.Second).
        // This will convert the "OnChat" method to a "Chat" event instead.
        SetEventMatcher(neffos.EventTrimPrefixMatcher("On"))

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

    log.Println("Listening on: ws://localhost:8080\nPress CTRL/CMD+C to interrupt.")
    log.Fatal(http.ListenAndServe(":8080", websocketServer))
}

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)
}

Access the http.Request from Conn:

func(c *neffos.NSConn, msg neffos.Message) error {
    req := c.Conn.Socket().Request()
}

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()

    //
    // You can either use the websocketServer.IDGenerator:
    // (http.ResponseWriter, *http.Request) string
    // as expected.
    //
    // However, if you want to use a specific Iris one:
    // func(ctx iris.Context) string
    // set it as a second input argument on the `websocket.Handler` func.
    // irisIDGenenerator = func(ctx iris.Context) string {
    //    return ctx.GetHeader("X-Username")
    // }
    // websocket.Handler(websocketServer, irisIDGenerator)
    //

    app.Get("/echo", websocket.Handler(websocketServer))

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

Access the iris.Context from Conn:

import "github.com/kataras/iris/websocket"

func(c *neffos.NSConn, msg neffos.Message) error {
    ctx := websocket.GetContext(c.Conn)
}

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
}

Run it

$ go run echo.go server
> 2019/06/29 02:24:41 Serving websockets on localhost:8080/echo
> 2019/06/29 02:25:34 Greetings!
$ go run echo.go client
> 2019/06/29 02:25:34 echo back: Greetings!