GOSF
Go Socket.IO Framework or GOSF is an easy-to-use framework for developing Socket.IO APIs in Google's Go programming language (GoLang).
Features
- Socket.IO v2 Support
- Request/Response Acknowledgement-Based Cycle
- Broadcast/Room Support
- Microservices Using Socket.IO
- Plugable Architecture
- App, Client, and Request Contexts
- Standardized Message Format
Getting Started
Starting a GOSF server is very easy. Download the framework, include it in your project, and follow the examples in this getting started guide to get up and running quickly. You can also download the sample app to get a more in-depth example and great starting point for your own project.
Download
Installing the framework for use in your project is simple. Run this shell command to get started in your GoLang environment.
go get -u "github.com/ambelovsky/gosf"
import "github.com/ambelovsky/gosf"
Once GOSF has been downloaded, import the package into your project and get to reading the rest of this documentation!
Your First Server
package main
import "github.com/ambelovsky/gosf"
func echo(client *gosf.Client, request *gosf.Request) *gosf.Message {
return gosf.NewSuccessMessage(request.Message.Text)
}
func init() {
// Listen on an endpoint
gosf.Listen("echo", echo)
}
func main() {
// Start the server using a basic configuration
gosf.Startup(map[string]interface{}{"port": 9999})
}
The given sample will start a server that responds on an "echo" endpoint and returns the same message received from the client back to the client.
Configuring a server using GOSF can be done in just a few lines of code. This is the simplest setup to get your server up and running.
First, we configure a method called echo. This method takes a client argument using the standard Client type from GOSF and a request argument using the standard Request type from GOSF. It then builds a response and returns that response. The framework picks up the returned response and sends that message back to the client automatically.
Routing Requests With Listen
In the init() method, a listener is configured using the standard Listen method from GOSF. gosf.Listen("echo", echo)
tells the framework to keep an eye out for anyone calling the server at the echo endpoint socket.emit('echo', ...)
. When a message comes in on the echo endpoint, run the echo method that we created. This completes a full request/response cycle.
Starting the Server
Using the Go main() method, the server is started using the standard Startup method from GOSF. This method takes a configuration. In this case, we've passed in a port that we want the server to listen on for new SocketIO requests.
Your First Client
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.slim.js"></script>
<script>
var socket = io.connect('ws://localhost:9999', { transports: ['websocket'] });
socket.emit('echo', { text: 'Hello world.' }, function(response) {
console.log(response);
});
</script>
Coding a Socket.IO client is easy. In the given example, be sure you have the console in your browser open to see the response message. If you're not used to this JavaScript code, take a look at the resources provided at socket.io for more information.
In this example, we are using the acknowledgement or ack model. In an ack model, the callback function is registered at the time of the emit as the third argument of the emit. When the server responds to this emit, its response will be returned directly to the callback function provided.
Going Further
An example server project to help you get started quickly can be found at github.com/ambelovsky/gosf-sample-app. It is highly recommended that you start with this project as the base for your own to get a sense for how to organize your project and work with GOSF.
Handling Connections
Starting the Server
A server is started using the Startup(config map[string]interface{})
function of GOSF.
Be sure that all plugins and listeners have been registered before calling the Startup
function.
Configuration Options
property | type | default | description |
---|---|---|---|
host | string | "" | host the server should bind to, listens to all requests by default |
port | int | 9999 | the port the server should listen on for new socket.io endpoint requests |
path | string | "/" | path this socket.io server should be accessible on, root path by default |
secure | bool | false | whether or not to use WSS (web-socket secure) |
enableCORS | string | nil | useless at the moment |
rejectInvalidHostnames | bool | false | whether or not to reject invalid hostnames when using a secure connection |
ssl-key | string | "" | file system path to the SSL key, required if secure is true |
ssl-cert | string | "" | file system path to the SSL certificate, required if secure is true |
Client Connect
After a client establishes a connection, their connection remains open. If a client established a secure connection, the connection continues to be secure for the life of the connection. In the event of an unexpected disconnect, reconnects are handled automatically by the client.
Currently, the only way to know when a connection has been established is through the use of Hooks.
Related Hooks
Connects have one event hook: OnConnect
. For more information on event hooks, see Hooks.
Client Disconnect
When a client disconnects, their client session is destroyed. It is recommended that tokens and other persistent session information be held in a separate datastore.
Currently, the only way to know when a disconnect occurs is through the use of Hooks.
Related Hooks
Disconnects have one event hook: OnDisconnect
. For more information on event hooks, see Hooks.
Message Protocol
To keep communication clean, GOSF uses a strict message protocol.
Message Type
type Message
The standard message type that GOSF passes to and expects from clients and microservices.
property | json output | type | description |
---|---|---|---|
ID | id | int | a message identifier sent by the client and returned by the server |
GUID | guid | string | a message identifier sent by the client and returned by the server |
UUID | uuid | string | a message identifier sent by the client and returned by the server |
Success | success | bool | whether the client's request was successfully processed |
Text | text | string | short message describing success/fail status |
Meta | meta{} | map[string]interface{} | relevant information related to the data set returned to the client |
Body | body{} | map[string]interface{} | complete data set returned to the client |
Message API
There are many functions available to help when working with a standard GOSF Message
.
NewSuccessMessage
NewSuccessMessage([text string], [body map[string]interface{}]) *Message
Generates a new Message
based on the given text status string and body data map. It sets the Success
status indicator to true. This convenience function helps to refactor basic responses.
In this function, the text
and body
parameters are both optional.
NewFailureMessage
NewFailureMessage([text string], [body map[string]interface{}]) *Message
Generates a new Message
based on the given text status string and body data map. It sets the Success
status indicator to false. This convenience function helps to refactor basic responses.
In this function, the text
and body
parameters are both optional.
StructToMap
StructToMap(input interface{}) map[string]interface{}
Returns a map based on the given struct. Helpful for converting instances of Message
into maps.
MapToStruct
MapToStruct(input map[string]interface{}, output interface{}) error
Accepts an input map and an output interface{}. When passing an instance of Message
as the output parameter, this function helps convert maps into a standard Message
.
Message.WithoutMeta
(m *Message) WithoutMeta() *Message
Returns a copy of the current Message
without the Meta
map. This is helpful when you want to move meta data among microservices but do not want it exposed to a connected client.
Request/Response Cycle
The request/response cycle describes how data flows from the client to the server and back to the client through GOSF. Socket.IO's connections are stateful, which means that data can be pushed directly to the client without being instigated by a request. However, GOSF models a basic request/response cycle to make instigated flows easy.
For details on pushing data to clients outside of the request/response cycle, see Broadcast Messages.
Endpoint Request
gosf.Listen("sample", sampleController)
Server listening for requests on "sample" endpoint
socket.emit("sample", {id: 1, text: "Here's some user data...", body: {age: 32}})
Client sending a request to "sample" endpoint
When a client sends a message into your Socket.IO server, they will emit that message to an endpoint. In the example Listen("myEndpoint", myController)
, "myEndpoint" is the name of the endpoint that the client is submitting a request to.
Requests are expected to pass standard JSON objects with the following optional properties.
Properties
property | type | description |
---|---|---|
id | int | an ID to be returned for request/response matching |
text | string | short message describing request |
body | Object{} | complete data set received by the server |
Related Hooks
Requests have two event hooks: OnBeforeRequest
and OnAfterRequest
. For more information on event hooks, see Hooks.
Controller Function
func myFirstController(client *gosf.Client, request *gosf.Request) *gosf.Message {
var statement string
// Parsing arguments in the body element
if val, ok := request.Message.Body["statement"]; ok {
statement = val.(string)
}
// Default value
if statement == "" {
statement = "that I love you."
}
// Construct response
response := new(gosf.Message)
response.Success = true
response.Text = "Hello World, I'd just like to say " + statement
// Send response back to the client
return response
}
Standard controller function template
Controllers are generally the component of an application that connect the view or consumer input/output with the data model. It is where the logic of the application is stored. GOSF builds the controller concept directly into its request/response cycle to take an MVC (model-view-controller) style of approach. In this paradigm, Message.Body
acts as your view, and the data models interacting with other data sources are left up to you.
GOSF provides a standard template for controller functions. Once a controller function is built, it can be provided to Listen
along with an endpoint name as described in Endpoint Request. For example, Listen("myEndpoint", myController)
.
Notice that the controller in the code sample creates a Message
response and simply returns it. This is the standard way to return a response to the client. If you prefer not to return a response to the client, then return nil
.
The standard Message
object accepts the following optional properties.
Properties
property | type | description |
---|---|---|
Success | bool | whether the client's request was successfully processed |
Text | string | short message describing success/fail status |
Meta | map[string]interface{} | relevant information related to the data set returned to the client |
Body | map[string]interface{} | complete data set returned to the client |
Endpoint Response
func echo(client *gosf.Client, request *gosf.Request) *gosf.Message {
response := new(gosf.Message)
response.Success = true
response.Text = request.Message.Text
return response
}
Controller function returning a Message
socket.on('echo', function(response) {
console.log(response);
});
socket.emit('echo', { text: 'Hello world.' });
Client emitting with a registered event handler
An endpoint response is automatically sent to the requesting client's registered event handler anytime a controller returns a Message
.
If the request is sent with an id
property (integer only), then the same message ID will be returned to the client with the response. This helps the client match requests with responses in cases where an acknowledgement response is not used.
If a controller returns nil
, then no endpoint response will be sent. If the client does not register an event handler to catch a response, no response will be received.
If a Message
is returned to the client, then the client receives a response object with the following optional properties.
Properties
property | type | description |
---|---|---|
id | int | an ID to be returned for request/response matching |
success | bool | whether the client's request was successfully processed |
text | string | short message describing results |
meta | Object{} | meta data returned to the client |
body | Object{} | complete data set returned to the client |
Related Hooks
Responses have two event hooks: OnBeforeResponse
and OnAfterResponse
. For more information on event hooks, see Hooks.
Acknowledgement Response
socket.emit('echo', { text: 'Hello world.' }, function(response) {
console.log(response);
});
Client emitting with an ack callback function
In GOSF, acknowledgement or ack responses are sent back to the requesting client automatically when a controller returns a Message
. Ack responses are only received by the callback on the client side that was registered at the time of the emit. This makes it easy to match requests with responses on the client side without the need to mess with message ID matching.
On the client side, if you have an ack callback registered with the emit and an event handler registered with the same endpoint name, both the ack and the event handler will receive a copy of the response.
If a controller returns nil
, then no ack response will be sent. If the client does not register an event handler with the emit, then no ack response will be received.
Error Messages
Errors are returned as a part of the standard message response for clients. Simply set Message.Success
to false to tell clients that an error has occurred. Use NewFailureMessage to quickly generate an error message with Message.Success
preset.
It's a recommended best practice to develop your own error codes, which should be delivered to the client in Message.Text
while Message.Body
can be used to contain error details. Short error codes in Message.Text
allow the client to build an easily programmable reaction on the client side.
Use Message.Meta
to transmit potentially sensitive error information among Microservices. Message.Meta
can be hidden from client endpoint view by calling Message.WithoutMeta.
Broadcast Messages
Broadcasting messages is commonly necessary for statefully connected Socket.IO applications, but it's not a standard part of the request/response cycle. Broadcast messages are messages that get sent to more than one connected client. You can broadcast to all connected clients or to a room of joined connected clients from anywhere in your application.
This ability to share messages makes it easy to communicate mass updates made by a single user or push information to users that may have been generated by an automated system. In GOSF, you can send broadcast messages globally by working with the Broadcast
function, or you can send broadcasts from the controller level by working with the Client.Broadcast
function.
GOSF considers it a best-practice to join every user to a room uniquely identified by the notation user-
followed by a user's ID or username. For example, user-193
. When communicating with a user outside the standard request/response cycle, broadcasting to the user's room makes certain that the Message
is received by all of that user's connected devices.
Global Broadcasts
func example() {
message := new(gosf.Message)
message.Success = true
message.Text = "Hello World"
gosf.Broadcast("", "example", message)
}
Broadcasting a message to all clients listening on the "example" endpoint
func example() {
message := new(gosf.Message)
message.Success = true
message.Text = "Hello World"
gosf.Broadcast("chat", "example", message)
}
Broadcasting a message to all clients in a specific room listening on the "example" endpoint
Broadcast(room string, endpoint string, message *Message)
Broadcast messages to multiple connected clients.
Attributes
attribute | type | description |
---|---|---|
room | string | grouping of connected clients to send this broadcast to |
endpoint | string | endpoint that connected clients are listening on |
message | *Message | message to broadcast to connected clients |
To send a message to all connected clients, simply pass an empty string instead of a specific room name.
Related Hooks
Global broadcasts have two event hooks: OnBeforeGlobalBroadcast
and OnAfterGlobalBroadcast
. For more information on event hooks, see Hooks.
Client Broadcasts
func echo(client *gosf.Client, request *gosf.Request) *gosf.Message {
response := new(gosf.Message)
response.Success = true
response.Text = request.Message.Text
client.Broadcast("friends", request.Endpoint, response)
return nil
}
Broadcasting the response to all clients in the room "friends" except the requesting client
(c *Client) Broadcast(room string, endpoint string, message *Message)
Broadcast messages to multiple connected clients excluding the requesting client.
Attributes
attribute | type | description |
---|---|---|
room | string | grouping of connected clients to send this broadcast to |
endpoint | string | endpoint that connected clients are listening on |
message | *Message | message to broadcast to connected clients |
To send a message to all connected clients, simply pass an empty string instead of a specific room name.
Related Hooks
Client broadcasts have two event hooks: OnBeforeBroadcast
and OnAfterBroadcast
. For more information on event hooks, see Hooks.
Joining a Room
(c *Client) Join(room string)
In order for a broadcast message sent to a room to be received by a client, the client must be joined to the room receiving the broadcast message. This function will join the current client to a broadcast room.
Attributes
attribute | type | description |
---|---|---|
room | string | name of the room to join this client connection to |
Leaving a Room
(c *Client) Leave(room string)
If you'd no longer like a client to receive broadcasts meant for a specific room, you can remove them from that room with this function.
Attributes
attribute | type | description |
---|---|---|
room | string | name of the room to remove this client connection from |
Leaving All Rooms
(c *Client) LeavAll()
LeaveAll() removes a client from all rooms they were previously joined to. This function can be safely called on a client with no rooms assignments.
Microservices
Microservice architecture is important in environments where smaller packagable features are a concern. Generally, microservices are called using RPC, but GOSF connects to microservices using Socket.IO.
When creating a Socket.IO microservice in another framework or language, remember that GOSF generates standard messages using the GOSF Message Protocol.
Because microservices are expected to communicate with the same GOSF Message Protocol, there's no need to worry about any of the traditional microservice protocol buffers. If you prefer strict typing, you can convert Body
and Meta
maps in the incoming Message
using MapToStruct and in the outgoing message using StructToMap.
Creating a Microservice
package main
import "github.com/ambelovsky/gosf"
func echo(client *gosf.Client, request *gosf.Request) *gosf.Message {
return gosf.NewSuccessMessage(request.Message.Text)
}
func init() {
// Listen on an endpoint
gosf.Listen("echo", echo)
}
func main() {
// Start the server using a basic configuration
gosf.Startup(map[string]interface{}{"port": 5001})
}
A microservice is built as a standard GOSF API using a standard request/response cycle. You can also develop a microservice in other languages as long as the JSON object that gets returned can map to a GOSF Message type.
In this example, we create a microservice that echos the given value back to the caller.
Consuming a Microservice
import (
"log"
"github.com/ambelovsky/gosf"
)
func init() {
gosf.RegisterMicroservice("utils", "127.0.0.1", 5001, false)
}
func main() {
request := gosf.NewSuccessMessage("Echo this...")
msUtils := gosf.GetMicroservice("utils")
if msUtils == nil {
panic("unable to get a reference to utils microservice")
}
if response, err := msUtils.Call("echo", request); err != nil {
log.Println(err.Error())
} else {
log.Println(response.Text)
}
}
Register a microservice by calling the RegisterMicroservice
function on init. Once registered, the microservice will be available in the App.Microservices
registry by the name given when the microservice was registered.
You can make a call to any of the microservice's endpoints after registration using App.Microservices["..."].Call(endpoint string, request *Message)
.
It is also possible to get a reference to a registered microservice using GetMicroservice(name string) *Microservice
.
Microservice API
Many built-in functions are available for working with microservices.
RegisterMicroservice
RegisterMicroservice(name string, host string, port int, secure bool) error
Connects to a running microservice and adds the microservice reference to the GOSF App.Microservices registry.
DeregisterMicroservice
DeregisterMicroservice(name string)
Disconnects and removes the microservice from the GOSF App.Microservices registry.
GetMicroservice
GetMicroservice(name string) *Microservice
Returns a reference to a registered microservice or nil
if the microservice could not be found.
Microservice.Lob
(m *Microservice) Lob(endpoint string, message *Message) error
Sends a message to the microservice endpoint without requiring a response.
Microservice.Call
(m *Microservice) Call(endpoint string, message *Message) (*Message, error)
Sends a message to a microservice endpiont expecting a response to be returned. Microservice responses timeout if not answered in 2 seconds or less.
Microservice.Listen
(m *Microservice) Listen(endpoint string, callback func(message *Message))
Listens for broadcast or non-request/response cycle messages to be delivered from the server.
Microservice.Connect
(m *Microservice) Connect() (*Microservice, error)
Connects a previously disconnected microservice.
Microservice.Connected
(m *Microservice) Connected() bool
Checks current connection status with the microservice. If the connection to a microservice is interrupted, GOSF will automatically reconnect when the microservice comes back online.
Microservice.Disconnect
(m *Microservice) Disconnect()
Deliberately disconnects from a microservice without attempting to reconnect.
Plugin Development
An example plugin project to help you get started quickly with plugins can be found at github.com/ambelovsky/gosf-sample-plugin. This sample project uses all of the hooks to create a basic console logging system and exposes an Echo app method to applications that register the plugin.
Hooks
It is possible to take advantage of hooks from anywhere in your application. However, it is a best-practice that hooks only be registered from within plugins. This keeps functionality modularly contained. For more information on plugin development, see Plugin Development.
OnConnect
gosf.OnConnect(func(client *gosf.Client, request *gosf.Request) {
log.Println("Client connected.")
})
Registering an event handler with the OnConnect hook
OnConnect(callback func(client *Client, request *Request))
Every time a client connects to the server, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
request | *Request |
OnDisconnect
gosf.OnDisconnect(func(client *gosf.Client, request *gosf.Request) {
log.Println("Client disconnected.")
})
Registering an event handler with the OnDisconnect hook
OnDisconnect(callback func(client *Client, request *Request))
Every time a client disconnects from the server, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
request | *Request |
OnBeforeRequest
gosf.OnBeforeRequest(func(client *gosf.Client, request *gosf.Request) {
log.Println("Request received for " + request.Endpoint + " endpoint.")
})
Registering an event handler with the OnBeforeRequest hook
OnBeforeRequest(callback func(client *Client, request *Request))
Before the server sends a request to a controller, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
request | *Request |
OnAfterRequest
gosf.OnAfterRequest(func(client *gosf.Client, request *gosf.Request, response *gosf.Message) {
log.Println("Request for " + request.Endpoint + " endpoint was processed by the controller.")
})
Registering an event handler with the OnAfterRequest hook
OnAfterRequest(callback func(client *Client, request *Request, response *Message))
After the controller has finished processing a request, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
request | *Request |
response | *Message |
OnBeforeResponse
gosf.OnBeforeResponse(func(client *gosf.Client, request *gosf.Request, response *gosf.Message) {
log.Println("Response for " + request.Endpoint + " endpoint is being prepared.")
})
Registering an event handler with the OnBeforeResponse hook
OnBeforeResponse(callback func(client *Client, request *Request, response *Message))
After the controller has finished processing a request and before a response is sent back to the client, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
request | *Request |
response | *Message |
OnAfterResponse
gosf.OnAfterResponse(func(client *gosf.Client, request *gosf.Request, response *gosf.Message) {
log.Println("Response for " + request.Endpoint + " endpoint was sent.")
})
Registering an event handler with the OnAfterResponse hook
OnAfterResponse(callback func(client *Client, request *Request, response *Message))
After a response is sent back to the client, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
request | *Request |
response | *Message |
OnBeforeBroadcast
gosf.OnBeforeBroadcast(func(endpoint string, room string, response *gosf.Message) {
log.Println("Broadcast for " + endpoint + " endpoint is preparing to send to " + getRoom(room) + ".")
})
Registering an event handler with the OnBeforeBroadcast hook
OnBeforeBroadcast(callback func(endpoint string, room string, response *Message))
Before a global broadcast message is sent, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
endpoint | string |
room | string |
response | *Message |
OnAfterBroadcast
gosf.OnAfterBroadcast(func(endpoint string, room string, response *gosf.Message) {
log.Println("Broadcast for " + endpoint + " endpoint was sent to " + getRoom(room) + ".")
})
Registering an event handler with the OnAfterBroadcast hook
OnAfterBroadcast(callback func(endpoint string, room string, response *Message))
After a global broadcast message has been sent, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
endpoint | string |
room | string |
response | *Message |
OnBeforeClientBroadcast
gosf.OnBeforeClientBroadcast(func(client *gosf.Client, endpoint string, room string, response *gosf.Message) {
log.Println("Broadcast for " + endpoint + " endpoint is preparing to send to " + getRoom(room) + ".")
})
Registering an event handler with the OnBeforeClientBroadcast hook
OnBeforeClientBroadcast(callback func(client *Client, endpoint string, room string, response *Message))
Before a client broadcast message is sent, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
endpoint | string |
room | string |
response | *Message |
OnAfterClientBroadcast
gosf.OnAfterClientBroadcast(func(client *gosf.Client, endpoint string, room string, response *gosf.Message) {
log.Println("Broadcast for " + endpoint + " endpoint was sent to " + getRoom(room) + ".")
})
Registering an event handler with the OnAfterClientBroadcast hook
OnAfterClientBroadcast(callback func(client *Client, endpoint string, room string, response *Message))
After a client broadcast message has been sent, this hook gets called. Pass a callback function you would like called everytime this hook fires.
Callback Attributes
The following attributes can be consumed by your callback function.
attribute | type |
---|---|
client | *Client |
endpoint | string |
room | string |
response | *Message |
Roadmap
- List and manipulate connections outside of a request/response context
- Server initiated requests (inverse request/response cycle)
- Session context support
- Automatic cluster plugin
- Traffic throttle plugin
- Permission system plugin
- Logging plugin
Recently Added
- Microservice support
- Expose environmental variables in App context
- Event/hook support
- Broadcast support
- Client ack (acknowledgement) request support
Author
Aaron Belovsky is a senior technologist, avid open source contributor, and author of GOSF.
Help support ongoing development effort with a donation to the following BTC wallet: 13pkXi8bW5bKjWewQYtv36CUgwhgQEn7eA
License
MIT