-
-
Notifications
You must be signed in to change notification settings - Fork 52
/
server.go
498 lines (420 loc) · 16.2 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
package neffos
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
uuid "github.com/iris-contrib/go.uuid"
)
// Upgrader is the definition type of a protocol upgrader, gorilla or gobwas or custom.
// It is the first parameter of the `New` function which constructs a neffos server.
type Upgrader func(w http.ResponseWriter, r *http.Request) (Socket, error)
// IDGenerator is the type of function that it is used
// to generate unique identifiers for new connections.
//
// See `Server.IDGenerator`.
type IDGenerator func(w http.ResponseWriter, r *http.Request) string
// DefaultIDGenerator returns a universal unique identifier for a new connection.
// It's the default `IDGenerator` for `Server`.
var DefaultIDGenerator IDGenerator = func(http.ResponseWriter, *http.Request) string {
id, err := uuid.NewV4()
if err != nil {
return strconv.FormatInt(time.Now().Unix(), 10)
}
return id.String()
}
// Server is the neffos server.
// Keeps the `IDGenerator` which can be customized, by default it's the `DefaultIDGenerator` which
// generates connections unique identifiers using the uuid/v4.
//
// Callers can optionally register callbacks for connection, disconnection and errored.
// Its most important methods are `ServeHTTP` which is used to register the server on a specific endpoint
// and `Broadcast` and `Close`.
// Use the `New` function to create a new server, server starts automatically, no further action is required.
type Server struct {
uuid string
upgrader Upgrader
IDGenerator IDGenerator
StackExchange StackExchange
mu sync.RWMutex
namespaces Namespaces
// connection read/write timeouts.
readTimeout time.Duration
writeTimeout time.Duration
count uint64
connections map[*Conn]struct{}
connect chan *Conn
disconnect chan *Conn
actions chan action
broadcaster *broadcaster
closed uint32
// OnUpgradeError can be optionally registered to catch upgrade errors.
OnUpgradeError func(err error)
// OnConnect can be optionally registered to be notified for any new neffos client connection,
// it can be used to force-connect a client to a specific namespace(s) or to send data immediately or
// even to cancel a client connection and dissalow its connection when its return error value is not nil.
// Don't confuse it with the `OnNamespaceConnect`, this callback is for the entire client side connection.
OnConnect func(c *Conn) error
// OnDisconnect can be optionally registered to notify about a connection's disconnect.
// Don't confuse it with the `OnNamespaceDisconnect`, this callback is for the entire client side connection.
OnDisconnect func(c *Conn)
}
// New constructs and returns a new neffos server.
// Listens to incoming connections automatically, no further action is required from the caller.
// The second parameter is the "connHandler", it can be
// filled as `Namespaces`, `Events` or `WithTimeout`, same namespaces and events can be used on the client-side as well,
// Use the `Conn#IsClient` on any event callback to determinate if it's a client-side connection or a server-side one.
//
// See examples for more.
func New(upgrader Upgrader, connHandler ConnHandler) *Server {
readTimeout, writeTimeout := getTimeouts(connHandler)
namespaces := connHandler.GetNamespaces()
s := &Server{
uuid: uuid.Must(uuid.NewV4()).String(),
upgrader: upgrader,
namespaces: namespaces,
readTimeout: readTimeout,
writeTimeout: writeTimeout,
connections: make(map[*Conn]struct{}),
connect: make(chan *Conn, 1),
disconnect: make(chan *Conn),
actions: make(chan action),
broadcaster: newBroadcaster(),
IDGenerator: DefaultIDGenerator,
}
// s.broadcastCond = sync.NewCond(&s.broadcastMu)
go s.start()
return s
}
func (s *Server) start() {
atomic.StoreUint32(&s.closed, 0)
for {
select {
case c := <-s.connect:
s.connections[c] = struct{}{}
atomic.AddUint64(&s.count, 1)
case c := <-s.disconnect:
if _, ok := s.connections[c]; ok {
// close(c.out)
delete(s.connections, c)
atomic.AddUint64(&s.count, ^uint64(0))
// println("disconnect...")
if s.OnDisconnect != nil {
// don't fire disconnect if was immediately closed on the `OnConnect` server event.
if !c.readiness.isReady() || (c.readiness.err != nil) {
continue
}
s.OnDisconnect(c)
}
if s.StackExchange != nil {
s.StackExchange.OnDisconnect(c)
}
}
case act := <-s.actions:
for c := range s.connections {
act.call(c)
}
if act.done != nil {
act.done <- struct{}{}
}
}
}
}
// Close terminates the server and all of its connections, client connections are getting notified.
func (s *Server) Close() {
if atomic.CompareAndSwapUint32(&s.closed, 0, 1) {
s.Do(func(c *Conn) {
c.Close()
}, false)
}
}
var (
errServerClosed = errors.New("server closed")
errInvalidMethod = errors.New("no valid request method")
)
// URLParamAsHeaderPrefix is the prefix that server parses the url parameters as request headers.
// The client's `URLParamAsHeaderPrefix` must match.
// Note that this is mostly useful for javascript browser-side clients, nodejs and go client support custom headers by default.
// No action required from end-developer, exported only for chance to a custom parsing.
const URLParamAsHeaderPrefix = "X-Websocket-Header-"
func tryParseURLParamsToHeaders(r *http.Request) {
q := r.URL.Query()
for k, values := range q {
if len(k) <= len(URLParamAsHeaderPrefix) {
continue
}
k = http.CanonicalHeaderKey(k) // canonical, so no X-WebSocket thing.
idx := strings.Index(k, URLParamAsHeaderPrefix)
if idx != 0 { // must be prefix.
continue
}
if r.Header == nil {
r.Header = make(http.Header)
}
k = k[len(URLParamAsHeaderPrefix):]
for _, v := range values {
r.Header.Add(k, v)
}
}
}
var errUpgradeOnRetry = errors.New("check status")
// IsTryingToReconnect reports whether the returning "err" from the `Server#Upgrade`
// is from a client that was trying to reconnect to the websocket server.
//
// Look the `Conn#WasReconnected` and `Conn#ReconnectTries` too.
func IsTryingToReconnect(err error) (ok bool) {
return err != nil && err == errUpgradeOnRetry
}
// This header key should match with that browser-client's `whenResourceOnline->re-dial` uses.
const websocketReconectHeaderKey = "X-Websocket-Reconnect"
func isServerConnID(s string) bool {
return strings.HasPrefix(s, "neffos(0x")
}
func genServerConnID(s *Server, c *Conn) string {
return fmt.Sprintf("neffos(0x%s(%s%p))", s.uuid, c.id, c)
}
// Upgrade handles the connection, same as `ServeHTTP` but it can accept
// a socket wrapper and a "customID" that overrides the server's IDGenerator
// and it does return the connection or any errors.
func (s *Server) Upgrade(
w http.ResponseWriter,
r *http.Request,
socketWrapper func(Socket) Socket,
customID string,
) (*Conn, error) {
if atomic.LoadUint32(&s.closed) > 0 {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil, errServerClosed
}
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusFound)
return nil, errUpgradeOnRetry
}
if r.Method != http.MethodGet {
// RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
// The response MUST include an Allow header containing a list of valid methods for the requested resource.
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow#Examples
w.Header().Set("Allow", http.MethodGet)
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintln(w, http.StatusText(http.StatusMethodNotAllowed))
return nil, errInvalidMethod
}
tryParseURLParamsToHeaders(r)
socket, err := s.upgrader(w, r)
if err != nil {
if s.OnUpgradeError != nil {
s.OnUpgradeError(err)
}
return nil, err
}
if socketWrapper != nil {
socket = socketWrapper(socket)
}
c := newConn(socket, s.namespaces)
if customID != "" {
c.id = customID
} else {
c.id = s.IDGenerator(w, r)
}
c.serverConnID = genServerConnID(s, c)
c.readTimeout = s.readTimeout
c.writeTimeout = s.writeTimeout
c.server = s
retriesHeaderValue := r.Header.Get(websocketReconectHeaderKey)
if retriesHeaderValue != "" {
c.ReconnectTries, _ = strconv.Atoi(retriesHeaderValue)
}
go func(c *Conn) {
for s.waitMessage(c) {
}
}(c)
s.connect <- c
go c.startReader()
// Start the reader before `OnConnect`, remember clients may remotely connect to namespace before `Server#OnConnect`
// therefore any `Server:NSConn#OnNamespaceConnected` can write immediately to the client too.
// Note also that the `Server#OnConnect` itself can do that as well but if the written Message's Namespace is not locally connected
// it, correctly, can't pass the write checks. Also, and most important, the `OnConnect` is ready to connect a client to a namespace (locally and remotely).
//
// This has a downside:
// We need a way to check if the `OnConnect` returns an non-nil error which means that the connection should terminate before namespace connect or anything.
// The solution is to still accept reading messages but add them to the queue(like we already do for any case messages came before ack),
// the problem to that is that the queue handler is fired when ack is done but `OnConnect` may not even return yet, so we introduce a `mark ready` atomic scope
// and a channel which will wait for that `mark ready` if handle queue is called before ready.
// Also make the same check before emit the connection's disconnect event (if defined),
// which will be always ready to be called because we added the connections via the connect channel;
// we still need the connection to be available for any broadcasting on connected events.
// ^ All these only when server-side connection in order to correctly handle the end-developer's `OnConnect`.
//
// Look `Conn.serverReadyWaiter#startReader##handleQueue.serverReadyWaiter.unwait`(to hold the events until no error returned or)
// `#Write:serverReadyWaiter.unwait` (for things like server connect).
// All cases tested & worked perfectly.
if s.OnConnect != nil {
if err = s.OnConnect(c); err != nil {
// TODO: Do something with that error.
// The most suitable thing we can do is to somehow send this to the client's `Dial` return statement.
// This can be done if client waits for "OK" signal or a failure with an error before return the websocket connection,
// as for today we have the ack process which does NOT block and end-developer can send messages and server will handle them when both sides are ready.
// So, maybe it's a better solution to transform that process into a blocking state which can handle any `Server#OnConnect` error and return it at client's `Dial`.
// Think more later today.
// Done but with a lot of code.... will try to cleanup some things.
//println("OnConnect error: " + err.Error())
c.readiness.unwait(err)
// c.Close()
return nil, err
}
}
if s.StackExchange != nil {
if err := s.StackExchange.OnConnect(c); err != nil {
c.readiness.unwait(err)
return nil, err
}
}
//println("OnConnect does not exist or no error, fire unwait")
c.readiness.unwait(nil)
return c, nil
}
// ServeHTTP completes the `http.Handler` interface, it should be passed on a http server's router
// to serve this neffos server on a specific endpoint.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.Upgrade(w, r, nil, "")
}
func (s *Server) waitMessage(c *Conn) bool {
s.broadcaster.mu.Lock()
defer s.broadcaster.mu.Unlock()
msg, ok := s.broadcaster.waitUntilClosed(c.closeCh)
if !ok {
return false
}
if msg.from == c.ID() {
// if the message is not supposed to return back to any connection with this ID.
return true
}
// if "To" field is given then send to a specific connection.
if msg.To != "" && msg.To != c.ID() {
return true
}
// c.Write may fail if the message is not supposed to end to this client
// but the connection should be still open in order to continue.
if !c.Write(msg) && c.IsClosed() {
return false
}
return true
}
// GetTotalConnections returns the total amount of the connected connections to the server, it's fast
// and can be used as frequently as needed.
func (s *Server) GetTotalConnections() uint64 {
return atomic.LoadUint64(&s.count)
}
type action struct {
call func(*Conn)
done chan struct{}
}
// Do loops through all connected connections and fires the "fn", with this method
// callers can do whatever they want on a connection outside of a event's callback,
// but make sure that these operations are not taking long time to complete because it delays the
// new incoming connections.
// If "async" is true then this method does not block the flow of the program.
func (s *Server) Do(fn func(*Conn), async bool) {
act := action{call: fn}
if !async {
act.done = make(chan struct{})
// go func() { s.actions <- act }()
// <-act.done
}
s.actions <- act
if !async {
<-act.done
}
}
type stringerValue struct{ v string }
func (s stringerValue) String() string { return s.v }
// Exclude can be passed on `Server#Broadcast` when
// caller does not have access to the `Conn`, `NSConn` or a `Room` value but
// has access to a string variable which is a connection's ID instead.
//
// Example Code:
// nsConn.Conn.Server().Broadcast(
// neffos.Exclude("connection_id_here"),
// neffos.Message{Namespace: "default", Room: "roomName or empty", Event: "chat", Body: [...]})
func Exclude(connID string) fmt.Stringer { return stringerValue{connID} }
// Broadcast method is fast and does not block any new incoming connection,
// it can be used as frequently as needed. Use the "msg"'s Namespace, or/and Event or/and Room to broadcast
// to a specific type of connection collectives.
//
// If first "exceptSender" parameter is not nil then the message "msg" will be
// broadcasted to all connected clients except the given connection's ID,
// any value that completes the `fmt.Stringer` interface is valid. Keep note that
// `Conn`, `NSConn`, `Room` and `Exclude(connID) global function` are valid values.
//
// Example Code:
// nsConn.Conn.Server().Broadcast(
// nsConn OR nil,
// neffos.Message{Namespace: "default", Room: "roomName or empty", Event: "chat", Body: [...]})
func (s *Server) Broadcast(exceptSender fmt.Stringer, msg Message) {
if exceptSender != nil {
switch c := exceptSender.(type) {
case *Conn:
msg.FromExplicit = c.serverConnID
case *NSConn:
msg.FromExplicit = c.Conn.serverConnID
default:
msg.from = exceptSender.String()
}
// msg.from = exceptSender.String()
}
// s.broadcast <- msg
// s.broadcastMu.Lock()
// s.broadcastMessage = msg
// s.broadcastMu.Unlock()
// s.broadcastCond.Broadcast()
if s.StackExchange != nil {
s.StackExchange.Publish(msg)
return
}
s.broadcaster.broadcast(msg)
}
// GetConnectionsByNamespace can be used as an alternative way to retrieve
// all connected connections to a specific "namespace" on a specific time point.
// Do not use this function frequently, it is not designed to be fast or cheap, use it for debugging or logging every 'x' time.
// Users should work with the event's callbacks alone, the usability is enough for all type of operations. See `Do` too.
//
// Not thread safe.
func (s *Server) GetConnectionsByNamespace(namespace string) map[string]*NSConn {
conns := make(map[string]*NSConn)
s.mu.RLock()
for c := range s.connections {
if ns := c.Namespace(namespace); ns != nil {
conns[ns.Conn.ID()] = ns
}
}
s.mu.RUnlock()
return conns
}
// GetConnections can be used as an alternative way to retrieve
// all connected connections to the server on a specific time point.
// Do not use this function frequently, it is not designed to be fast or cheap, use it for debugging or logging every 'x' time.
//
// Not thread safe.
func (s *Server) GetConnections() map[string]*Conn {
conns := make(map[string]*Conn)
s.mu.RLock()
for c := range s.connections {
conns[c.ID()] = c
}
s.mu.RUnlock()
return conns
}
var (
// ErrBadNamespace may return from a `Conn#Connect` method when the remote side does not declare the given namespace.
ErrBadNamespace = errors.New("bad namespace")
// ErrBadRoom may return from a `Room#Leave` method when trying to leave from a not joined room.
ErrBadRoom = errors.New("bad room")
// ErrWrite may return from any connection's method when the underline connection is closed (unexpectedly).
ErrWrite = errors.New("write closed")
)