socket.io library. current version.
This commit is contained in:
parent
9322a52075
commit
52ee6ec03c
|
@ -0,0 +1,157 @@
|
|||
golang socket.io
|
||||
================
|
||||
|
||||
golang implementation of [socket.io](http://socket.io) library, client and server
|
||||
|
||||
You can check working chat server, based on this library, at (funstream)[http://funstream.tv]
|
||||
|
||||
Examples directory contains simple client and server.
|
||||
|
||||
### Installation
|
||||
|
||||
go get github.com/graarh/golang-socketio
|
||||
|
||||
### Simple server usage
|
||||
|
||||
```go
|
||||
//create
|
||||
server := gosocketio.NewServer(transport.GetDefaultWebsocketTransport())
|
||||
|
||||
//handle connected
|
||||
server.On(gosocketio.OnConnection, func(c *gosocketio.Channel, args interface{}) {
|
||||
log.Println("New client connected")
|
||||
//join them to room
|
||||
c.Join("chat")
|
||||
})
|
||||
|
||||
type Message struct {
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
//handle custom event
|
||||
server.On("send", func(c *gosocketio.Channel, msg Message) string {
|
||||
//send event to all in room
|
||||
c.BroadcastTo("chat", "message", msg)
|
||||
return "OK"
|
||||
})
|
||||
|
||||
//setup http server
|
||||
serveMux := http.NewServeMux()
|
||||
serveMux.Handle("/socket.io/", server)
|
||||
log.Panic(http.ListenAndServe(":80", serveMux))
|
||||
```
|
||||
|
||||
### Javascript client for this server
|
||||
|
||||
```javascript
|
||||
var socket = io('ws://yourdomain.com', {transports: ['websocket']});
|
||||
|
||||
// listen for messages
|
||||
socket.on('message', function(message) {
|
||||
|
||||
console.log('new message');
|
||||
console.log(message);
|
||||
});
|
||||
|
||||
socket.on('connect', function () {
|
||||
|
||||
console.log('socket connected');
|
||||
|
||||
//send something
|
||||
socket.emit('send', {name: "my name", message: "hello"}, function(result) {
|
||||
|
||||
console.log('sended successfully);
|
||||
console.log(result)
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Server, detailed usage
|
||||
|
||||
```go
|
||||
//create server instance, you can setup transport parameters or get the default one
|
||||
//look at websocket.go for parameters description
|
||||
server := gosocketio.NewServer(transport.GetDefaultWebsocketTransport())
|
||||
|
||||
// --- this is default handlers
|
||||
|
||||
//on connection handler, occurs once for each connected client
|
||||
server.On(gosocketio.OnConnection, func(c *gosocketio.Channel, args interface{}) {
|
||||
//client id is unique
|
||||
log.Println("New client connected, client id is ", c.Id())
|
||||
|
||||
//you can join clients to rooms
|
||||
c.Join("room name")
|
||||
|
||||
//of course, you can list the clients in the room, or account them
|
||||
channels, _ := c.List(data.Channel)
|
||||
log.Println(len(channels), "clients in room")
|
||||
})
|
||||
//on disconnection handler, if client hangs connection unexpectedly, it will still occurs
|
||||
server.On(gosocketio.OnDisconnection, func(c *gosocketio.Channel, args interface{}) {
|
||||
//this is not necessary, client will be removed from rooms
|
||||
//automatically on disconnect
|
||||
//but you can remove client from room whenever you need to
|
||||
c.Leave("room name")
|
||||
|
||||
log.Println("Disconnected")
|
||||
})
|
||||
//error catching handler
|
||||
server.On(gosocketio.OnError, func(c *gosocketio.Channel, args interface{}) {
|
||||
log.Println("Error occurs")
|
||||
})
|
||||
|
||||
// --- this is custom handler
|
||||
|
||||
//custom event handler
|
||||
server.On("handle something", func(c *gosocketio.Channel, channel Channel) string {
|
||||
log.Println("Something successfully handled")
|
||||
|
||||
//you can return result of handler, in this case
|
||||
//handler will be converted from "emit" to "ack"
|
||||
return "result"
|
||||
})
|
||||
|
||||
//you can get client connection by it's id
|
||||
channel, _ := server.GetChannel("client id here")
|
||||
//and send the event to the client
|
||||
type MyEventData struct {
|
||||
Data: string
|
||||
}
|
||||
channel.Emit("my event", MyEventData{"my data"})
|
||||
|
||||
//or you can send ack to client and get result back
|
||||
result, err := channel.Ack("my custom ack", MyEventData{"ack data"}, time.Second * 5)
|
||||
|
||||
//you can broadcast to all clients
|
||||
server.BroadcastToAll("my event", MyEventData{"broadcast"})
|
||||
|
||||
//or for clients joined to room
|
||||
server.BroadcastTo("my room", "my event", MyEventData{"room broadcast"})
|
||||
|
||||
//setup http server like this for handling connections
|
||||
serveMux := http.NewServeMux()
|
||||
serveMux.Handle("/socket.io/", server)
|
||||
log.Panic(http.ListenAndServe(":80", serveMux))
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```go
|
||||
//connect to server, you can use your own transport settings
|
||||
c, err := gosocketio.Dial("localhost:80", transport.GetDefaultWebsocketTransport())
|
||||
|
||||
//do something, handlers and functions are same as server ones
|
||||
|
||||
//close connection
|
||||
c.Close()
|
||||
```
|
||||
|
||||
### Roadmap
|
||||
|
||||
1. Tests
|
||||
2. Travis CI
|
||||
3. http longpoll transport
|
||||
4. pure http (short-timed queries) transport
|
||||
5. binary format
|
|
@ -0,0 +1,64 @@
|
|||
package gosocketio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorWaiterNotFound = errors.New("Waiter not found")
|
||||
)
|
||||
|
||||
/**
|
||||
Processes functions that require answers, also known as acknowledge or ack
|
||||
*/
|
||||
type ackProcessor struct {
|
||||
counter int
|
||||
counterLock sync.Mutex
|
||||
|
||||
resultWaiters map[int](chan string)
|
||||
resultWaitersLock sync.RWMutex
|
||||
}
|
||||
|
||||
/**
|
||||
get next id of ack call
|
||||
*/
|
||||
func (a *ackProcessor) getNextId() int {
|
||||
a.counterLock.Lock()
|
||||
defer a.counterLock.Unlock()
|
||||
|
||||
a.counter++
|
||||
return a.counter
|
||||
}
|
||||
|
||||
/**
|
||||
Just before the ack function called, the waiter should be added
|
||||
to wait and receive response to ack call
|
||||
*/
|
||||
func (a *ackProcessor) addWaiter(id int, w chan string) {
|
||||
a.resultWaitersLock.Lock()
|
||||
a.resultWaiters[id] = w
|
||||
a.resultWaitersLock.Unlock()
|
||||
}
|
||||
|
||||
/**
|
||||
removes waiter that is unnecessary anymore
|
||||
*/
|
||||
func (a *ackProcessor) removeWaiter(id int) {
|
||||
a.resultWaitersLock.Lock()
|
||||
delete(a.resultWaiters, id)
|
||||
a.resultWaitersLock.Unlock()
|
||||
}
|
||||
|
||||
/**
|
||||
check if waiter with given ack id is exists, and returns it
|
||||
*/
|
||||
func (a *ackProcessor) getWaiter(id int) (chan string, error) {
|
||||
a.resultWaitersLock.RLock()
|
||||
defer a.resultWaitersLock.RUnlock()
|
||||
|
||||
if waiter, ok := a.resultWaiters[id]; ok {
|
||||
return waiter, nil
|
||||
}
|
||||
return nil, ErrorWaiterNotFound
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package gosocketio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type caller struct {
|
||||
Func reflect.Value
|
||||
Args reflect.Type
|
||||
Out bool
|
||||
}
|
||||
|
||||
var (
|
||||
ErrorCallerNotFunc = errors.New("f is not function")
|
||||
ErrorCallerNot2Args = errors.New("f should have 2 args")
|
||||
ErrorCallerMaxOneValue = errors.New("f should return not more than one value")
|
||||
)
|
||||
|
||||
/**
|
||||
Parses function passed by using reflection, and stores its representation
|
||||
for further call on message or ack
|
||||
*/
|
||||
func newCaller(f interface{}) (*caller, error) {
|
||||
fVal := reflect.ValueOf(f)
|
||||
if fVal.Kind() != reflect.Func {
|
||||
return nil, ErrorCallerNotFunc
|
||||
}
|
||||
|
||||
fType := fVal.Type()
|
||||
if fType.NumIn() != 2 {
|
||||
return nil, ErrorCallerNot2Args
|
||||
}
|
||||
|
||||
if fType.NumOut() > 1 {
|
||||
return nil, ErrorCallerMaxOneValue
|
||||
}
|
||||
|
||||
return &caller{
|
||||
Func: fVal,
|
||||
Args: fType.In(1),
|
||||
Out: fType.NumOut() == 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
/**
|
||||
returns function parameter as it is present in it using reflection
|
||||
*/
|
||||
func (c *caller) getArgs() interface{} {
|
||||
return reflect.New(c.Args).Interface()
|
||||
}
|
||||
|
||||
/**
|
||||
calls function with given arguments from its representation using reflection
|
||||
*/
|
||||
func (c *caller) callFunc(h *Channel, args interface{}) []reflect.Value {
|
||||
a := []reflect.Value{reflect.ValueOf(h), reflect.ValueOf(args).Elem()}
|
||||
|
||||
return c.Func.Call(a)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package gosocketio
|
||||
|
||||
import (
|
||||
"funstream/libs/socket.io/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
webSocketProtocol = "ws://"
|
||||
socketioUrl = "/socket.io/?EIO=3&transport=websocket"
|
||||
)
|
||||
|
||||
/**
|
||||
Socket.io client representation
|
||||
*/
|
||||
type Client struct {
|
||||
methods
|
||||
Channel
|
||||
}
|
||||
|
||||
/**
|
||||
connect to host and initialise socket.io protocol
|
||||
*/
|
||||
func Dial(host string, tr transport.Transport) (*Client, error) {
|
||||
c := &Client{}
|
||||
c.initChannel()
|
||||
c.initMethods()
|
||||
|
||||
var err error
|
||||
c.conn, err = tr.Connect(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go inLoop(&c.Channel, &c.methods)
|
||||
go outLoop(&c.Channel, &c.methods)
|
||||
go pinger(&c.Channel)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
/**
|
||||
Close client connection
|
||||
*/
|
||||
func (c *Client) Close() {
|
||||
CloseChannel(&c.Channel, &c.methods)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"funstream/libs/socket.io"
|
||||
"funstream/libs/socket.io/transport"
|
||||
"log"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Id int `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func sendJoin(c *gosocketio.Client) {
|
||||
log.Println("Acking /join")
|
||||
result, err := c.Ack("/join", Channel{"main"}, time.Second*5)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
log.Println("Ack result to /join: ", result)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
c, err := gosocketio.Dial("localhost:3811", transport.GetDefaultWebsocketTransport())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.On("/message", func(h *gosocketio.Channel, args Message) {
|
||||
log.Println("--- Got chat message: ", args)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.On(gosocketio.OnDisconnection, func(h *gosocketio.Channel, args interface{}) {
|
||||
log.Fatal("Disconnected")
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.On(gosocketio.OnConnection, func(h *gosocketio.Channel, args interface{}) {
|
||||
log.Println("Connected")
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
go sendJoin(c)
|
||||
go sendJoin(c)
|
||||
go sendJoin(c)
|
||||
go sendJoin(c)
|
||||
go sendJoin(c)
|
||||
|
||||
time.Sleep(60 * time.Second)
|
||||
c.Close()
|
||||
|
||||
log.Println(" [x] Complete")
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"funstream/libs/socket.io"
|
||||
"funstream/libs/socket.io/transport"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Id int `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
server := gosocketio.NewServer(transport.GetDefaultWebsocketTransport())
|
||||
|
||||
server.On(gosocketio.OnConnection, func(c *gosocketio.Channel, args interface{}) {
|
||||
log.Println("Connected")
|
||||
|
||||
c.Emit("/message", Message{10, "main", "using emit"})
|
||||
|
||||
c.Join("test")
|
||||
c.BroadcastTo("test", "/message", Message{10, "main", "using broadcast"})
|
||||
})
|
||||
server.On(gosocketio.OnDisconnection, func(c *gosocketio.Channel, args interface{}) {
|
||||
log.Println("Disconnected")
|
||||
})
|
||||
|
||||
server.On("/join", func(c *gosocketio.Channel, channel Channel) string {
|
||||
time.Sleep(2 * time.Second)
|
||||
return "joined to " + channel.Channel
|
||||
})
|
||||
|
||||
serveMux := http.NewServeMux()
|
||||
serveMux.Handle("/socket.io/", server)
|
||||
|
||||
log.Println("Starting server...")
|
||||
log.Panic(http.ListenAndServe(":3811", serveMux))
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package gosocketio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"funstream/libs/socket.io/protocol"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
OnConnection = "connection"
|
||||
OnDisconnection = "disconnection"
|
||||
OnError = "error"
|
||||
)
|
||||
|
||||
/**
|
||||
System handler function for internal event processing
|
||||
*/
|
||||
type systemHandler func(c *Channel)
|
||||
|
||||
/**
|
||||
Contains maps of message processing functions
|
||||
*/
|
||||
type methods struct {
|
||||
messageHandlers map[string]*caller
|
||||
messageHandlersLock sync.RWMutex
|
||||
|
||||
onConnection systemHandler
|
||||
onDisconnection systemHandler
|
||||
}
|
||||
|
||||
/**
|
||||
create messageHandlers map
|
||||
*/
|
||||
func (m *methods) initMethods() {
|
||||
m.messageHandlers = make(map[string]*caller)
|
||||
}
|
||||
|
||||
/**
|
||||
Add message processing function, and bind it to given method
|
||||
*/
|
||||
func (m *methods) On(method string, f interface{}) error {
|
||||
c, err := newCaller(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.messageHandlersLock.Lock()
|
||||
defer m.messageHandlersLock.Unlock()
|
||||
m.messageHandlers[method] = c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Find message processing function associated with given method
|
||||
*/
|
||||
func (m *methods) findMethod(method string) (*caller, bool) {
|
||||
m.messageHandlersLock.RLock()
|
||||
defer m.messageHandlersLock.RUnlock()
|
||||
|
||||
f, ok := m.messageHandlers[method]
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (m *methods) callLoopEvent(c *Channel, event string) {
|
||||
if m.onConnection != nil && event == OnConnection {
|
||||
m.onConnection(c)
|
||||
}
|
||||
if m.onDisconnection != nil && event == OnDisconnection {
|
||||
m.onDisconnection(c)
|
||||
}
|
||||
|
||||
f, ok := m.findMethod(event)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
f.callFunc(c, &struct{}{})
|
||||
}
|
||||
|
||||
/**
|
||||
Check incoming message
|
||||
On ack_resp - look for waiter
|
||||
On ack_req - look for processing function and send ack_resp
|
||||
On emit - look for processing function
|
||||
*/
|
||||
func (m *methods) processIncomingMessage(c *Channel, msg *protocol.Message) {
|
||||
switch msg.Type {
|
||||
case protocol.MessageTypeEmit:
|
||||
f, ok := m.findMethod(msg.Method)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
data := f.getArgs()
|
||||
err := json.Unmarshal([]byte(msg.Args), &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.callFunc(c, data)
|
||||
|
||||
case protocol.MessageTypeAckRequest:
|
||||
f, ok := m.findMethod(msg.Method)
|
||||
if !ok || !f.Out {
|
||||
return
|
||||
}
|
||||
|
||||
data := f.getArgs()
|
||||
err := json.Unmarshal([]byte(msg.Args), &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result := f.callFunc(c, data)
|
||||
ack := &protocol.Message{
|
||||
Type: protocol.MessageTypeAckResponse,
|
||||
AckId: msg.AckId,
|
||||
}
|
||||
send(ack, c, result[0].Interface())
|
||||
|
||||
case protocol.MessageTypeAckResponse:
|
||||
waiter, err := c.ack.getWaiter(msg.AckId)
|
||||
if err == nil {
|
||||
waiter <- msg.Args
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package gosocketio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"funstream/libs/socket.io/protocol"
|
||||
"funstream/libs/socket.io/transport"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
queueBufferSize = 500
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorWrongHeader = errors.New("Wrong header")
|
||||
)
|
||||
|
||||
/**
|
||||
engine.io header to send or receive
|
||||
*/
|
||||
type Header struct {
|
||||
Sid string `json:"sid"`
|
||||
Upgrades []string `json:"upgrades"`
|
||||
PingInterval int `json:"pingInterval"`
|
||||
PingTimeout int `json:"pingTimeout"`
|
||||
}
|
||||
|
||||
/**
|
||||
socket.io connection handler
|
||||
|
||||
use IsAlive to check that handler is still working
|
||||
use Dial to connect to websocket
|
||||
use In and Out channels for message exchange
|
||||
Close message means channel is closed
|
||||
ping is automatic
|
||||
*/
|
||||
type Channel struct {
|
||||
conn transport.Connection
|
||||
|
||||
out chan string
|
||||
header Header
|
||||
|
||||
alive bool
|
||||
aliveLock sync.Mutex
|
||||
|
||||
ack ackProcessor
|
||||
|
||||
server *Server
|
||||
ip string
|
||||
}
|
||||
|
||||
/**
|
||||
create channel, map, and set active
|
||||
*/
|
||||
func (c *Channel) initChannel() {
|
||||
//TODO: queueBufferSize from constant to server or client variable
|
||||
c.out = make(chan string, queueBufferSize)
|
||||
c.ack.resultWaiters = make(map[int](chan string))
|
||||
c.alive = true
|
||||
}
|
||||
|
||||
/**
|
||||
Get id of current socket connection
|
||||
*/
|
||||
func (c *Channel) Id() string {
|
||||
return c.header.Sid
|
||||
}
|
||||
|
||||
/**
|
||||
Checks that Channel is still alive
|
||||
*/
|
||||
func (c *Channel) IsAlive() bool {
|
||||
return c.alive
|
||||
}
|
||||
|
||||
/**
|
||||
Close channel
|
||||
*/
|
||||
func CloseChannel(c *Channel, m *methods, args ...interface{}) error {
|
||||
c.aliveLock.Lock()
|
||||
defer c.aliveLock.Unlock()
|
||||
|
||||
if !c.alive {
|
||||
//already closed
|
||||
return nil
|
||||
}
|
||||
|
||||
c.conn.Close()
|
||||
c.alive = false
|
||||
|
||||
//clean outloop
|
||||
for len(c.out) > 0 {
|
||||
<-c.out
|
||||
}
|
||||
c.out <- protocol.CloseMessage
|
||||
|
||||
m.callLoopEvent(c, OnDisconnection)
|
||||
return nil
|
||||
}
|
||||
|
||||
//incoming messages loop, puts incoming messages to In channel
|
||||
func inLoop(c *Channel, m *methods) error {
|
||||
for {
|
||||
pkg, err := c.conn.GetMessage()
|
||||
if err != nil {
|
||||
return CloseChannel(c, m, err)
|
||||
}
|
||||
msg, err := protocol.Decode(pkg)
|
||||
if err != nil {
|
||||
CloseChannel(c, m, protocol.ErrorWrongPacket)
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case protocol.MessageTypeOpen:
|
||||
if err := json.Unmarshal([]byte(msg.Source[1:]), &c.header); err != nil {
|
||||
CloseChannel(c, m, ErrorWrongHeader)
|
||||
}
|
||||
m.callLoopEvent(c, OnConnection)
|
||||
case protocol.MessageTypePing:
|
||||
c.out <- protocol.PongMessage
|
||||
case protocol.MessageTypePong:
|
||||
default:
|
||||
go m.processIncomingMessage(c, msg)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var overflooded map[*Channel]struct{} = make(map[*Channel]struct{})
|
||||
var overfloodedLock sync.Mutex
|
||||
|
||||
func AmountOfOverflooded() int64 {
|
||||
overfloodedLock.Lock()
|
||||
defer overfloodedLock.Unlock()
|
||||
|
||||
return int64(len(overflooded))
|
||||
}
|
||||
|
||||
/**
|
||||
outgoing messages loop, sends messages from channel to socket
|
||||
*/
|
||||
func outLoop(c *Channel, m *methods) error {
|
||||
for {
|
||||
outBufferLen := len(c.out)
|
||||
if outBufferLen == queueBufferSize {
|
||||
return CloseChannel(c, m, ErrorSocketOverflood)
|
||||
} else if outBufferLen > int(queueBufferSize/2) {
|
||||
overfloodedLock.Lock()
|
||||
overflooded[c] = struct{}{}
|
||||
overfloodedLock.Unlock()
|
||||
} else {
|
||||
overfloodedLock.Lock()
|
||||
delete(overflooded, c)
|
||||
overfloodedLock.Unlock()
|
||||
}
|
||||
|
||||
msg := <-c.out
|
||||
if msg == protocol.CloseMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := c.conn.WriteMessage(msg)
|
||||
if err != nil {
|
||||
return CloseChannel(c, m, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Pinger sends ping messages for keeping connection alive
|
||||
*/
|
||||
func pinger(c *Channel) {
|
||||
for {
|
||||
interval, _ := c.conn.PingParams()
|
||||
time.Sleep(interval)
|
||||
if !c.IsAlive() {
|
||||
return
|
||||
}
|
||||
|
||||
c.out <- protocol.PingMessage
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package protocol
|
||||
|
||||
const (
|
||||
/**
|
||||
Message with connection options
|
||||
*/
|
||||
MessageTypeOpen = iota
|
||||
/**
|
||||
Close connection and destroy all handle routines
|
||||
*/
|
||||
MessageTypeClose = iota
|
||||
/**
|
||||
Ping request message
|
||||
*/
|
||||
MessageTypePing = iota
|
||||
/**
|
||||
Pong response message
|
||||
*/
|
||||
MessageTypePong = iota
|
||||
/**
|
||||
Empty message
|
||||
*/
|
||||
MessageTypeEmpty = iota
|
||||
/**
|
||||
Emit request, no response
|
||||
*/
|
||||
MessageTypeEmit = iota
|
||||
/**
|
||||
Emit request, wait for response (ack)
|
||||
*/
|
||||
MessageTypeAckRequest = iota
|
||||
/**
|
||||
ack response
|
||||
*/
|
||||
MessageTypeAckResponse = iota
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Type int
|
||||
AckId int
|
||||
Method string
|
||||
Args string
|
||||
Source string
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
open = "0"
|
||||
msg = "4"
|
||||
emptyMessage = "40"
|
||||
commonMessage = "42"
|
||||
ackMessage = "43"
|
||||
|
||||
CloseMessage = "1"
|
||||
PingMessage = "2"
|
||||
PongMessage = "3"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorWrongMessageType = errors.New("Wrong message type")
|
||||
ErrorWrongPacket = errors.New("Wrong packet")
|
||||
)
|
||||
|
||||
func typeToText(msgType int) (string, error) {
|
||||
switch msgType {
|
||||
case MessageTypeOpen:
|
||||
return open, nil
|
||||
case MessageTypeClose:
|
||||
return CloseMessage, nil
|
||||
case MessageTypePing:
|
||||
return PingMessage, nil
|
||||
case MessageTypePong:
|
||||
return PongMessage, nil
|
||||
case MessageTypeEmpty:
|
||||
return emptyMessage, nil
|
||||
case MessageTypeEmit, MessageTypeAckRequest:
|
||||
return commonMessage, nil
|
||||
case MessageTypeAckResponse:
|
||||
return ackMessage, nil
|
||||
}
|
||||
return "", ErrorWrongMessageType
|
||||
}
|
||||
|
||||
func Encode(msg *Message) (string, error) {
|
||||
result, err := typeToText(msg.Type)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if msg.Type == MessageTypeEmpty || msg.Type == MessageTypePing ||
|
||||
msg.Type == MessageTypePong {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if msg.Type == MessageTypeAckRequest || msg.Type == MessageTypeAckResponse {
|
||||
result += strconv.Itoa(msg.AckId)
|
||||
}
|
||||
|
||||
if msg.Type == MessageTypeOpen || msg.Type == MessageTypeClose {
|
||||
return result + msg.Args, nil
|
||||
}
|
||||
|
||||
if msg.Type == MessageTypeAckResponse {
|
||||
return result + "[" + msg.Args + "]", nil
|
||||
}
|
||||
|
||||
jsonMethod, err := json.Marshal(&msg.Method)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result + "[" + string(jsonMethod) + "," + msg.Args + "]", nil
|
||||
}
|
||||
|
||||
func MustEncode(msg *Message) string {
|
||||
result, err := Encode(msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getMessageType(data string) (int, error) {
|
||||
if len(data) == 0 {
|
||||
return 0, ErrorWrongMessageType
|
||||
}
|
||||
switch data[0:1] {
|
||||
case open:
|
||||
return MessageTypeOpen, nil
|
||||
case CloseMessage:
|
||||
return MessageTypeClose, nil
|
||||
case PingMessage:
|
||||
return MessageTypePing, nil
|
||||
case PongMessage:
|
||||
return MessageTypePong, nil
|
||||
case msg:
|
||||
if len(data) == 1 {
|
||||
return 0, ErrorWrongMessageType
|
||||
}
|
||||
switch data[0:2] {
|
||||
case emptyMessage:
|
||||
return MessageTypeEmpty, nil
|
||||
case commonMessage:
|
||||
return MessageTypeAckRequest, nil
|
||||
case ackMessage:
|
||||
return MessageTypeAckResponse, nil
|
||||
}
|
||||
}
|
||||
return 0, ErrorWrongMessageType
|
||||
}
|
||||
|
||||
/**
|
||||
Get ack id of current packet, if present
|
||||
*/
|
||||
func getAck(text string) (ackId int, restText string, err error) {
|
||||
if len(text) < 4 {
|
||||
return 0, "", ErrorWrongPacket
|
||||
}
|
||||
text = text[2:]
|
||||
|
||||
pos := strings.IndexByte(text, '[')
|
||||
if pos == -1 {
|
||||
return 0, "", ErrorWrongPacket
|
||||
}
|
||||
|
||||
ack, err := strconv.Atoi(text[0:pos])
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
return ack, text[pos:], nil
|
||||
}
|
||||
|
||||
/**
|
||||
Get message method of current packet, if present
|
||||
*/
|
||||
func getMethod(text string) (method, restText string, err error) {
|
||||
var start, end, rest, countQuote int
|
||||
|
||||
for i, c := range text {
|
||||
if c == '"' {
|
||||
switch countQuote {
|
||||
case 0:
|
||||
start = i + 1
|
||||
case 1:
|
||||
end = i
|
||||
rest = i + 1
|
||||
default:
|
||||
return "", "", ErrorWrongPacket
|
||||
}
|
||||
countQuote++
|
||||
}
|
||||
if c == ',' {
|
||||
if countQuote < 2 {
|
||||
continue
|
||||
}
|
||||
rest = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (end < start) || (rest >= len(text)) {
|
||||
return "", "", ErrorWrongPacket
|
||||
}
|
||||
|
||||
return text[start:end], text[rest : len(text)-1], nil
|
||||
}
|
||||
|
||||
func Decode(data string) (*Message, error) {
|
||||
var err error
|
||||
msg := &Message{}
|
||||
msg.Source = data
|
||||
|
||||
msg.Type, err = getMessageType(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if msg.Type == MessageTypeOpen {
|
||||
msg.Args = data[1:]
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
if msg.Type == MessageTypeClose || msg.Type == MessageTypePing ||
|
||||
msg.Type == MessageTypePong || msg.Type == MessageTypeEmpty {
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
ack, rest, err := getAck(data)
|
||||
msg.AckId = ack
|
||||
if msg.Type == MessageTypeAckResponse {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.Args = rest[1 : len(rest)-1]
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
msg.Type = MessageTypeEmit
|
||||
rest = data[2:]
|
||||
}
|
||||
|
||||
msg.Method, msg.Args, err = getMethod(rest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package gosocketio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
"funstream/libs/socket.io/protocol"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorSendTimeout = errors.New("Timeout")
|
||||
ErrorSocketOverflood = errors.New("Socket overflood")
|
||||
)
|
||||
|
||||
/**
|
||||
Send message packet to socket
|
||||
*/
|
||||
func send(msg *protocol.Message, c *Channel, args interface{}) error {
|
||||
json, err := json.Marshal(&args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.Args = string(json)
|
||||
|
||||
command, err := protocol.Encode(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(c.out) == queueBufferSize {
|
||||
return ErrorSocketOverflood
|
||||
}
|
||||
|
||||
c.out <- command
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Create packet based on given data and send it
|
||||
*/
|
||||
func (c *Channel) Emit(method string, args interface{}) error {
|
||||
msg := &protocol.Message{
|
||||
Type: protocol.MessageTypeEmit,
|
||||
Method: method,
|
||||
}
|
||||
|
||||
return send(msg, c, args)
|
||||
}
|
||||
|
||||
/**
|
||||
Create ack packet based on given data and send it and receive response
|
||||
*/
|
||||
func (c *Channel) Ack(method string, args interface{}, timeout time.Duration) (string, error) {
|
||||
msg := &protocol.Message{
|
||||
Type: protocol.MessageTypeAckRequest,
|
||||
AckId: c.ack.getNextId(),
|
||||
Method: method,
|
||||
}
|
||||
|
||||
waiter := make(chan string)
|
||||
c.ack.addWaiter(msg.AckId, waiter)
|
||||
|
||||
err := send(msg, c, args)
|
||||
if err != nil {
|
||||
c.ack.removeWaiter(msg.AckId)
|
||||
}
|
||||
|
||||
select {
|
||||
case result := <-waiter:
|
||||
return result, nil
|
||||
case <-time.After(timeout):
|
||||
c.ack.removeWaiter(msg.AckId)
|
||||
return "", ErrorSendTimeout
|
||||
}
|
||||
}
|
|
@ -0,0 +1,325 @@
|
|||
package gosocketio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"funstream/libs/socket.io/transport"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
"funstream/libs/socket.io/protocol"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorServerNotSet = errors.New("Server not set")
|
||||
ErrorConnectionNotFound = errors.New("Connection not found")
|
||||
)
|
||||
|
||||
/**
|
||||
socket.io server instance
|
||||
*/
|
||||
type Server struct {
|
||||
methods
|
||||
http.Handler
|
||||
|
||||
channels map[string]map[*Channel]struct{}
|
||||
rooms map[*Channel]map[string]struct{}
|
||||
channelsLock sync.RWMutex
|
||||
|
||||
sids map[string]*Channel
|
||||
sidsLock sync.RWMutex
|
||||
|
||||
tr transport.Transport
|
||||
}
|
||||
|
||||
/**
|
||||
Get channel by it's sid
|
||||
*/
|
||||
func (s *Server) GetChannel(sid string) (*Channel, error) {
|
||||
s.sidsLock.RLock()
|
||||
defer s.sidsLock.RUnlock()
|
||||
|
||||
c, ok := s.sids[sid]
|
||||
if !ok {
|
||||
return nil, ErrorConnectionNotFound
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
/**
|
||||
Join this channel to given room
|
||||
*/
|
||||
func (c *Channel) Join(room string) error {
|
||||
if c.server == nil {
|
||||
return ErrorServerNotSet
|
||||
}
|
||||
|
||||
c.server.channelsLock.Lock()
|
||||
defer c.server.channelsLock.Unlock()
|
||||
|
||||
cn := c.server.channels
|
||||
if _, ok := cn[room]; !ok {
|
||||
cn[room] = make(map[*Channel]struct{})
|
||||
}
|
||||
|
||||
byRoom := c.server.rooms
|
||||
if _, ok := byRoom[c]; !ok {
|
||||
byRoom[c] = make(map[string]struct{})
|
||||
}
|
||||
|
||||
cn[room][c] = struct{}{}
|
||||
byRoom[c][room] = struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Remove this channel from given room
|
||||
*/
|
||||
func (c *Channel) Leave(room string) error {
|
||||
if c.server == nil {
|
||||
return ErrorServerNotSet
|
||||
}
|
||||
|
||||
c.server.channelsLock.Lock()
|
||||
defer c.server.channelsLock.Unlock()
|
||||
|
||||
cn := c.server.channels
|
||||
if _, ok := cn[room]; ok {
|
||||
delete(cn[room], c)
|
||||
if len(cn[room]) == 0 {
|
||||
delete(cn, room)
|
||||
}
|
||||
}
|
||||
|
||||
byRoom := c.server.rooms
|
||||
if _, ok := byRoom[c]; ok {
|
||||
delete(byRoom[c], room)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Get list of channels, joined to given room, using channel
|
||||
*/
|
||||
func (c *Channel) List(room string) ([]*Channel, error) {
|
||||
if c.server == nil {
|
||||
return nil, ErrorServerNotSet
|
||||
}
|
||||
|
||||
return c.server.List(room)
|
||||
}
|
||||
|
||||
/**
|
||||
Get list of channels, joined to given room, using server
|
||||
*/
|
||||
func (s *Server) List(room string) ([]*Channel, error) {
|
||||
s.channelsLock.RLock()
|
||||
defer s.channelsLock.RUnlock()
|
||||
|
||||
roomChannels, ok := s.channels[room]
|
||||
if !ok {
|
||||
return []*Channel{}, nil
|
||||
}
|
||||
|
||||
i := 0
|
||||
roomChannelsCopy := make([]*Channel, len(roomChannels))
|
||||
for channel := range roomChannels {
|
||||
roomChannelsCopy[i] = channel
|
||||
i++
|
||||
}
|
||||
|
||||
return roomChannelsCopy, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Channel) BroadcastTo(room, method string, args interface{}) {
|
||||
if c.server == nil {
|
||||
return
|
||||
}
|
||||
c.server.BroadcastTo(room, method, args)
|
||||
}
|
||||
|
||||
/**
|
||||
Broadcast message to all room channels
|
||||
*/
|
||||
func (s *Server) BroadcastTo(room, method string, args interface{}) {
|
||||
s.channelsLock.RLock()
|
||||
defer s.channelsLock.RUnlock()
|
||||
|
||||
roomChannels, ok := s.channels[room]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for cn := range roomChannels {
|
||||
if cn.IsAlive() {
|
||||
go cn.Emit(method, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Broadcast to all clients
|
||||
*/
|
||||
func (s *Server) BroadcastToAll(method string, args interface{}) {
|
||||
s.sidsLock.RLock()
|
||||
defer s.sidsLock.RUnlock()
|
||||
|
||||
for _, cn := range s.sids {
|
||||
if cn.IsAlive() {
|
||||
go cn.Emit(method, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Generate new id for socket.io connection
|
||||
*/
|
||||
func generateNewId(custom string) string {
|
||||
hash := fmt.Sprintf("%s %s %n %n", custom, time.Now(), rand.Uint32(), rand.Uint32())
|
||||
buf := bytes.NewBuffer(nil)
|
||||
sum := md5.Sum([]byte(hash))
|
||||
encoder := base64.NewEncoder(base64.URLEncoding, buf)
|
||||
encoder.Write(sum[:])
|
||||
encoder.Close()
|
||||
return buf.String()[:20]
|
||||
}
|
||||
|
||||
/**
|
||||
On connection system handler, store sid
|
||||
*/
|
||||
func onConnectStore(c *Channel) {
|
||||
c.server.sidsLock.Lock()
|
||||
defer c.server.sidsLock.Unlock()
|
||||
|
||||
c.server.sids[c.Id()] = c
|
||||
}
|
||||
|
||||
/**
|
||||
On disconnection system handler, clean joins and sid
|
||||
*/
|
||||
func onDisconnectCleanup(c *Channel) {
|
||||
c.server.channelsLock.Lock()
|
||||
defer c.server.channelsLock.Unlock()
|
||||
|
||||
cn := c.server.channels
|
||||
byRoom, ok := c.server.rooms[c]
|
||||
if ok {
|
||||
for room := range byRoom {
|
||||
if curRoom, ok := cn[room]; ok {
|
||||
delete(curRoom, c)
|
||||
if len(curRoom) == 0 {
|
||||
delete(cn, room)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete(c.server.rooms, c)
|
||||
}
|
||||
|
||||
c.server.sidsLock.Lock()
|
||||
defer c.server.sidsLock.Unlock()
|
||||
|
||||
delete(c.server.sids, c.Id())
|
||||
}
|
||||
|
||||
func (s *Server) SendOpenSequence(c *Channel) {
|
||||
jsonHdr, err := json.Marshal(&c.header)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.out <- protocol.MustEncode(
|
||||
&protocol.Message{
|
||||
Type: protocol.MessageTypeOpen,
|
||||
Args: string(jsonHdr),
|
||||
},
|
||||
)
|
||||
|
||||
c.out <- protocol.MustEncode(&protocol.Message{Type: protocol.MessageTypeEmpty})
|
||||
}
|
||||
|
||||
/**
|
||||
Setup event loop for given connection
|
||||
*/
|
||||
func (s *Server) SetupEventLoop(conn transport.Connection, remoteAddr string) {
|
||||
interval, timeout := conn.PingParams()
|
||||
hdr := Header{
|
||||
Sid: generateNewId(remoteAddr),
|
||||
Upgrades: []string{},
|
||||
PingInterval: int(interval / time.Millisecond),
|
||||
PingTimeout: int(timeout / time.Millisecond),
|
||||
}
|
||||
|
||||
c := &Channel{}
|
||||
c.conn = conn
|
||||
c.ip = remoteAddr
|
||||
c.initChannel()
|
||||
|
||||
c.server = s
|
||||
c.header = hdr
|
||||
|
||||
s.SendOpenSequence(c)
|
||||
|
||||
go inLoop(c, &s.methods)
|
||||
go outLoop(c, &s.methods)
|
||||
|
||||
s.callLoopEvent(c, OnConnection)
|
||||
}
|
||||
|
||||
/**
|
||||
implements ServeHTTP function from http.Handler
|
||||
*/
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.tr.HandleConnection(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.SetupEventLoop(conn, r.RemoteAddr)
|
||||
s.tr.Serve(w, r)
|
||||
}
|
||||
|
||||
/**
|
||||
Get amount of current connected sids
|
||||
*/
|
||||
func (s *Server) AmountOfSids() int64 {
|
||||
s.sidsLock.RLock()
|
||||
defer s.sidsLock.RUnlock()
|
||||
|
||||
return int64(len(s.sids))
|
||||
}
|
||||
|
||||
/**
|
||||
Get amount of rooms with at least one channel(or sid) joined
|
||||
*/
|
||||
func (s *Server) AmountOfRooms() int64 {
|
||||
s.channelsLock.RLock()
|
||||
defer s.channelsLock.RUnlock()
|
||||
|
||||
return int64(len(s.channels))
|
||||
}
|
||||
|
||||
/**
|
||||
Create new socket.io server
|
||||
*/
|
||||
func NewServer(tr transport.Transport) *Server {
|
||||
s := Server{}
|
||||
s.initMethods()
|
||||
s.tr = tr
|
||||
s.channels = make(map[string]map[*Channel]struct{})
|
||||
s.rooms = make(map[*Channel]map[string]struct{})
|
||||
s.sids = make(map[string]*Channel)
|
||||
s.onConnection = onConnectStore
|
||||
s.onDisconnection = onDisconnectCleanup
|
||||
|
||||
return &s
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
/**
|
||||
End-point connection for given transport
|
||||
*/
|
||||
type Connection interface {
|
||||
/**
|
||||
Receive one more message, block until received
|
||||
*/
|
||||
GetMessage() (message string, err error)
|
||||
|
||||
/**
|
||||
Send given message, block until sent
|
||||
*/
|
||||
WriteMessage(message string) error
|
||||
|
||||
/**
|
||||
Close current connection
|
||||
*/
|
||||
Close()
|
||||
|
||||
/**
|
||||
Get ping time interval and ping request timeout
|
||||
*/
|
||||
PingParams() (interval, timeout time.Duration)
|
||||
}
|
||||
|
||||
/**
|
||||
Connection factory for given transport
|
||||
*/
|
||||
type Transport interface {
|
||||
/**
|
||||
Get client connection
|
||||
*/
|
||||
Connect(host string) (conn Connection, err error)
|
||||
|
||||
/**
|
||||
Handle one server connection
|
||||
*/
|
||||
HandleConnection(w http.ResponseWriter, r *http.Request) (conn Connection, err error)
|
||||
|
||||
/**
|
||||
Serve HTTP request after making connection and events setup
|
||||
*/
|
||||
Serve(w http.ResponseWriter, r *http.Request)
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
webSocketProtocol = "ws://"
|
||||
socketioUrl = "/socket.io/?EIO=3&transport=websocket"
|
||||
upgradeFailed = "Upgrade failed: "
|
||||
|
||||
WsDefaultPingInterval = 30 * time.Second
|
||||
WsDefaultPingTimeout = 60 * time.Second
|
||||
WsDefaultReceiveTimeout = 60 * time.Second
|
||||
WsDefaultSendTimeout = 60 * time.Second
|
||||
WsDefaultBufferSize = 1024 * 32
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorBinaryMessage = errors.New("Binary messages are not supported")
|
||||
ErrorBadBuffer = errors.New("Buffer error")
|
||||
ErrorPacketWrong = errors.New("Wrong packet type error")
|
||||
ErrorMethodNotAllowed = errors.New("Method not allowed")
|
||||
ErrorHttpUpgradeFailed = errors.New("Http upgrade failed")
|
||||
)
|
||||
|
||||
type WebsocketConnection struct {
|
||||
socket *websocket.Conn
|
||||
transport *WebsocketTransport
|
||||
}
|
||||
|
||||
func (wsc *WebsocketConnection) GetMessage() (message string, err error) {
|
||||
wsc.socket.SetReadDeadline(time.Now().Add(wsc.transport.ReceiveTimeout))
|
||||
msgType, reader, err := wsc.socket.NextReader()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
//support only text messages exchange
|
||||
if msgType != websocket.TextMessage {
|
||||
return "", ErrorBinaryMessage
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", ErrorBadBuffer
|
||||
}
|
||||
text := string(data)
|
||||
|
||||
//empty messages are not allowed
|
||||
if len(text) == 0 {
|
||||
return "", ErrorPacketWrong
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func (wsc *WebsocketConnection) WriteMessage(message string) error {
|
||||
wsc.socket.SetWriteDeadline(time.Now().Add(wsc.transport.SendTimeout))
|
||||
writer, err := wsc.socket.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := writer.Write([]byte(message)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsc *WebsocketConnection) Close() {
|
||||
wsc.socket.Close()
|
||||
}
|
||||
|
||||
func (wsc *WebsocketConnection) PingParams() (interval, timeout time.Duration) {
|
||||
return wsc.transport.PingInterval, wsc.transport.PingTimeout
|
||||
}
|
||||
|
||||
type WebsocketTransport struct {
|
||||
PingInterval time.Duration
|
||||
PingTimeout time.Duration
|
||||
ReceiveTimeout time.Duration
|
||||
SendTimeout time.Duration
|
||||
|
||||
BufferSize int
|
||||
}
|
||||
|
||||
func (wst *WebsocketTransport) Connect(host string) (conn Connection, err error) {
|
||||
dialer := websocket.Dialer{}
|
||||
socket, _, err := dialer.Dial(webSocketProtocol+host+socketioUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &WebsocketConnection{socket, wst}, nil
|
||||
}
|
||||
|
||||
func (wst *WebsocketTransport) HandleConnection(
|
||||
w http.ResponseWriter, r *http.Request) (conn Connection, err error) {
|
||||
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, upgradeFailed+ErrorMethodNotAllowed.Error(), 503)
|
||||
return nil, ErrorMethodNotAllowed
|
||||
}
|
||||
|
||||
socket, err := websocket.Upgrade(w, r, nil, wst.BufferSize, wst.BufferSize)
|
||||
if err != nil {
|
||||
http.Error(w, upgradeFailed+err.Error(), 503)
|
||||
return nil, ErrorHttpUpgradeFailed
|
||||
}
|
||||
|
||||
return &WebsocketConnection{socket, wst}, nil
|
||||
}
|
||||
|
||||
/**
|
||||
Websocket connection do not require any additional processing
|
||||
*/
|
||||
func (wst *WebsocketTransport) Serve(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
/**
|
||||
Returns websocket connection with default params
|
||||
*/
|
||||
func GetDefaultWebsocketTransport() *WebsocketTransport {
|
||||
return &WebsocketTransport{
|
||||
PingInterval: WsDefaultPingInterval,
|
||||
PingTimeout: WsDefaultPingTimeout,
|
||||
ReceiveTimeout: WsDefaultReceiveTimeout,
|
||||
SendTimeout: WsDefaultSendTimeout,
|
||||
BufferSize: WsDefaultBufferSize,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue