-
-
Notifications
You must be signed in to change notification settings - Fork 49
Getting started
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.
- Client connection is initialized through the
neffos.Dial
(orneffos.dial
on javascript side) method. - Server can dismiss with an error any incoming client through its
server.OnConnect -> return err != nil
callback.
-
Emit
sends data and fires a specific event back to the remote side. - A client can NOT communicate directly to the rest of the clients.
- Server is the only one which is able to send messages to one or more clients.
- 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"
)
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,
},
}
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))
}
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)
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!")
.
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)
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)
}
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!
Home | About | Project | Getting Started | Technical Docs | Copyright © 2019-2023 Gerasimos Maropoulos. Documentation terms of use.