fayec/transport/websocket/websocket.go
2018-09-11 13:30:07 +02:00

378 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package websocket
import (
"fmt"
"github.com/gorilla/websocket"
"github.com/thesyncim/faye/message"
"github.com/thesyncim/faye/subscription"
"github.com/thesyncim/faye/transport"
"log"
"strconv"
"sync"
"sync/atomic"
)
const transportName = "websocket"
func init() {
transport.RegisterTransport(&Websocket{})
}
//Websocket represents an websocket transport for the faye protocol
type Websocket struct {
topts *transport.Options
connMu sync.Mutex
conn *websocket.Conn
clientID string
msgID *uint64
once sync.Once
advice atomic.Value //type message.Advise
stopCh chan error
//subsMu sync.Mutex //todo sync.Map
//subs map[string]chan *message.Message
subsMu2 sync.Mutex //todo sync.Map
subs2 map[string][]*subscription.Subscription
onPubResponseMu sync.Mutex //todo sync.Map
onPublishResponse map[string]func(message *message.Message)
}
var _ transport.Transport = (*Websocket)(nil)
//Init initializes the transport with the provided options
func (w *Websocket) Init(endpoint string, options *transport.Options) error {
var (
err error
msgID uint64
)
w.topts = options
w.msgID = &msgID
//w.subs = map[string]chan *message.Message{}
w.subs2 = map[string][]*subscription.Subscription{}
w.onPublishResponse = map[string]func(message *message.Message){}
w.stopCh = make(chan error)
w.conn, _, err = websocket.DefaultDialer.Dial(endpoint, options.Headers)
if err != nil {
return err
}
return nil
}
func (w *Websocket) readWorker() error {
for {
select {
case err := <-w.stopCh:
return err
default:
}
var payload []message.Message
err := w.conn.ReadJSON(&payload)
if err != nil {
return err
}
//dispatch
msg := &payload[0]
w.topts.Extensions.ApplyInExtensions(msg)
if msg.Advice != nil {
w.handleAdvise(msg.Advice)
}
if transport.IsMetaMessage(msg) {
//handle it
switch msg.Channel {
case transport.MetaSubscribe:
//handle MetaSubscribe resp
w.subsMu2.Lock()
subscriptions, ok := w.subs2[msg.Subscription]
if !ok {
panic("BUG: subscription not registered `" + msg.Subscription + "`")
}
if !msg.Successful {
if msg.GetError() == nil {
//inject the error if the server returns unsuccessful without error
msg.Error = fmt.Sprintf("susbscription `%s` failed", msg.Subscription)
}
var si = -1
for i := range subscriptions {
if subscriptions[i].ID() == msg.Id {
si = i
select {
case subscriptions[i].SubscriptionResult() <- msg.GetError():
close(subscriptions[i].MsgChannel())
default:
log.Println("subscription has no listeners") //todo remove*/
}
}
}
//remove subscription
if si > -1 {
subscriptions = subscriptions[:si+copy(subscriptions[si:], subscriptions[si+1:])]
}
w.subs2[msg.Subscription] = subscriptions
//v2
} else {
for i := range subscriptions {
if subscriptions[i].ID() == msg.Id {
select {
case subscriptions[i].SubscriptionResult() <- nil:
default:
log.Println("subscription has no listeners") //todo remove*/
}
}
}
}
w.subsMu2.Unlock()
}
continue
}
//is Event Message
//there are 2 types of Event Message
// 1. Publish
// 2. Delivery
if transport.IsEventDelivery(msg) {
w.subsMu2.Lock()
subscriptions, ok := w.subs2[msg.Channel]
if ok {
//send to all listeners
for i := range subscriptions {
if subscriptions[i].MsgChannel() != nil {
select {
case subscriptions[i].MsgChannel() <- msg:
default:
log.Println("subscription has no listeners") //todo remove
}
}
}
}
w.subsMu2.Unlock()
continue
}
if transport.IsEventPublish(msg) {
w.onPubResponseMu.Lock()
onPublish, ok := w.onPublishResponse[msg.Channel]
w.onPubResponseMu.Unlock()
if ok {
onPublish(msg)
}
}
}
}
//Name returns the transport name (websocket)
func (w *Websocket) Name() string {
return transportName
}
func (w *Websocket) sendMessage(m *message.Message) error {
w.connMu.Lock()
defer w.connMu.Unlock()
w.topts.Extensions.ApplyOutExtensions(m)
var payload []message.Message
payload = append(payload, *m)
again:
err := w.conn.WriteJSON(payload)
if websocket.IsUnexpectedCloseError(err) {
advise := w.advice.Load().(*message.Advise)
if advise.Reconnect == message.ReconnectNone {
return err
}
//reconnect
//we should re-register again subscriptions again
goto again
}
return nil
}
func (w *Websocket) nextMsgID() string {
return strconv.Itoa(int(atomic.AddUint64(w.msgID, 1)))
}
//Options return the transport Options
func (w *Websocket) Options() *transport.Options {
return w.topts
}
//Handshake initiates a connection negotiation by sending a message to the /meta/handshake channel.
func (w *Websocket) Handshake() (err error) {
m := message.Message{
Channel: transport.MetaHandshake,
Version: "1.0", //todo const
SupportedConnectionTypes: []string{transportName},
}
err = w.sendMessage(&m)
if err != nil {
return err
}
var hsResps []message.Message
if err = w.conn.ReadJSON(&hsResps); err != nil {
return err
}
resp := &hsResps[0]
w.topts.Extensions.ApplyInExtensions(resp)
if resp.GetError() != nil {
return err
}
w.clientID = resp.ClientId
return nil
}
//Connect is called after a client has discovered the servers capabilities with a handshake exchange,
//a connection is established by sending a message to the /meta/connect channel
func (w *Websocket) Connect() error {
m := message.Message{
Channel: transport.MetaConnect,
ClientId: w.clientID,
ConnectionType: transportName,
Id: w.nextMsgID(),
}
go w.readWorker()
return w.sendMessage(&m)
}
//Disconnect closes all subscriptions and inform the server to remove any client-related state.
//any subsequent method call to the client object will result in undefined behaviour.
func (w *Websocket) Disconnect() error {
m := message.Message{
Channel: transport.MetaDisconnect,
ClientId: w.clientID,
Id: w.nextMsgID(),
}
w.stopCh <- nil
close(w.stopCh)
w.subsMu2.Lock()
for i := range w.subs2 {
//close all listeners
for j := range w.subs2[i] {
close(w.subs2[i][j].MsgChannel())
}
delete(w.subs2, i)
}
w.subsMu2.Unlock()
return w.sendMessage(&m)
}
//Subscribe informs the server that messages published to that channel are delivered to itself.
func (w *Websocket) Subscribe(channel string) (*subscription.Subscription, error) {
id := w.nextMsgID()
m := &message.Message{
Channel: transport.MetaSubscribe,
ClientId: w.clientID,
Subscription: channel,
Id: id,
}
if err := w.sendMessage(m); err != nil {
return nil, err
}
inMsgCh := make(chan *message.Message, 0)
subRes := make(chan error)
var pub = func(data message.Data) (string, error) {
return w.Publish(channel, data)
}
sub := subscription.NewSubscription(id, channel, w.Unsubscribe, pub, inMsgCh, subRes)
w.subsMu2.Lock()
w.subs2[channel] = append(w.subs2[channel], sub)
w.subsMu2.Unlock()
//todo timeout here
err := <-subRes
if err != nil {
log.Println(err)
return nil, err
}
log.Println(sub)
return sub, nil
}
//Unsubscribe informs the server that the client will no longer listen to incoming event messages on
//the specified channel/subscription
func (w *Websocket) Unsubscribe(subscription *subscription.Subscription) error {
//https://docs.cometd.org/current/reference/#_bayeux_meta_unsubscribe
w.subsMu2.Lock()
defer w.subsMu2.Unlock()
subs, ok := w.subs2[subscription.Channel()]
if ok {
var si = -1
for i := range subs {
if subs[i] == subscription {
close(subs[i].MsgChannel())
si = i
}
}
if si > -1 {
//remove the subscription
subs = subs[:si+copy(subs[si:], subs[si+1:])]
}
w.subs2[subscription.Channel()] = subs
//if no more listeners to this subscription send unsubscribe to server
if len(subs) == 0 {
delete(w.subs2, subscription.Channel())
//remove onPublishResponse handler
w.onPubResponseMu.Lock()
delete(w.onPublishResponse, subscription.Channel())
w.onPubResponseMu.Unlock()
m := &message.Message{
Channel: transport.MetaUnsubscribe,
Subscription: subscription.Channel(),
ClientId: w.clientID,
Id: w.nextMsgID(),
}
return w.sendMessage(m)
}
}
return nil
}
//Publish publishes events on a channel by sending event messages, the server MAY respond to a publish event
//if this feature is supported by the server use the OnPublishResponse to get the publish status.
func (w *Websocket) Publish(subscription string, data message.Data) (id string, err error) {
id = w.nextMsgID()
m := &message.Message{
Channel: subscription,
Data: data,
ClientId: w.clientID,
Id: id,
}
if err = w.sendMessage(m); err != nil {
return "", err
}
return id, nil
}
//OnPublishResponse sets the handler to be triggered if the server replies to the publish request
//according to the spec the server MAY reply to the publish request, so its not guaranteed that this handler will
//ever be triggered
//can be used to identify the status of the published request and for example retry failed published requests
func (w *Websocket) OnPublishResponse(subscription string, onMsg func(message *message.Message)) {
w.onPubResponseMu.Lock()
w.onPublishResponse[subscription] = onMsg
w.onPubResponseMu.Unlock()
}
func (w *Websocket) handleAdvise(m *message.Advise) {
//todo actually handle the advice
w.advice.Store(m)
}