Repository navigation
Expand file tree
/
Copy pathserver_socket.go
More file actions
568 lines (491 loc) · 15.8 KB
/
Copy pathserver_socket.go
File metadata and controls
568 lines (491 loc) · 15.8 KB
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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
package engineio
import (
"errors"
"slices"
"sync"
"time"
)
// Sentinel Errors.
var (
ErrSocketClosed = errors.New("socket is closed")
)
// sessionState is the lifecycle state of a server session.
type sessionState int
const (
// sessionOpening is the brief state between handshake and the open packet.
sessionOpening sessionState = iota
// sessionOpen is a live session.
sessionOpen
// sessionClosing is a session draining its final packets before closing.
sessionClosing
// sessionClosed is a torn-down session.
sessionClosed
)
// serverTransport is the server side of an Engine.IO transport. A transport
// carries packets for exactly one session.
type serverTransport interface {
// transportType reports the transport kind.
transportType() TransportType
// send writes packets to the peer. It reports whether the packets were
// written; a polling transport with no poll currently held reports false so
// the socket keeps them buffered.
send(packets []Packet) (sent bool, err error)
// close tears down the underlying HTTP or WebSocket resources. It is
// idempotent.
close()
}
// promotableTransport is a probing transport that can be promoted to the
// session's active transport once its upgrade handshake completes. Both the
// websocket and webtransport server transports implement it, so the upgrade
// machinery is transport-agnostic.
type promotableTransport interface {
serverTransport
// promote ends the probing state so subsequent packets dispatch to the session.
promote()
}
// ServerConnectionHandler is invoked once for each new session, after the open
// packet has been sent. It is where the application installs the session's
// message and close handlers and may begin sending. It must not block, since on
// the websocket transport it runs before the read loop starts.
type ServerConnectionHandler func(*ServerSocket)
// ServerMessageHandler is invoked for each message a session receives. The
// isBinary flag reports whether the peer sent the payload as binary, so the
// application can round-trip binary data without downgrading it to text.
type ServerMessageHandler func(data []byte, isBinary bool)
// ServerCloseHandler is called once when a session closes. The reason is a short
// human-readable label (e.g. "transport close", "ping timeout", "forced close").
// The cause is the underlying error when one was captured, or nil otherwise; a
// nil cause does not by itself mean a graceful close (a ping timeout also passes
// nil), so branch on the reason rather than on whether cause is nil.
type ServerCloseHandler func(reason string, cause error)
// ServerSocket is a single connected Engine.IO session, handed to the
// application through Server.OnConnection.
type ServerSocket struct {
// id, server, and options are set once at construction and never mutated.
id string
server *Server
options serverConfig
// mu guards the state machine, the active transport, the outbound buffer,
// the heartbeat and upgrade timers, and the handlers below.
mu sync.Mutex
state sessionState
transport serverTransport
upgradeTransport serverTransport
upgrading bool
writeBuffer []Packet
pingTimer *time.Timer
pongTimer *time.Timer
upgradeTimer *time.Timer
// closeTimer forces a graceful close to complete if the client never polls
// again to drain the buffered close packet, so a vanished client cannot leak
// a session stuck in the closing state.
closeTimer *time.Timer
probeNoopStop chan struct{}
onMessageHandler ServerMessageHandler
onCloseHandler ServerCloseHandler
// flushMu serializes flush so buffered packets are never written twice.
flushMu sync.Mutex
}
// newServerSocket creates a session bound to the given transport in the opening
// state.
func newServerSocket(id string, server *Server, transport serverTransport) *ServerSocket {
return &ServerSocket{
id: id,
server: server,
options: server.options,
state: sessionOpening,
transport: transport,
}
}
// ID returns the session identifier assigned at the handshake. It is stable for
// the life of the session and safe to read concurrently, since it is set once at
// construction and never mutated.
func (s *ServerSocket) ID() string {
return s.id
}
// activePollingTransport returns the active transport if it is a polling
// transport.
func (s *ServerSocket) activePollingTransport() (*serverPollingTransport, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if polling, ok := s.transport.(*serverPollingTransport); ok {
return polling, true
}
return nil, false
}
// OnMessage registers the handler invoked for each message the session receives.
// It replaces any previously registered handler; passing nil clears it. It is
// typically called from the connection handler.
func (s *ServerSocket) OnMessage(handler ServerMessageHandler) {
s.mu.Lock()
s.onMessageHandler = handler
s.mu.Unlock()
}
// OnClose registers the handler invoked once when the session closes. It
// replaces any previously registered handler; passing nil clears it.
func (s *ServerSocket) OnClose(handler ServerCloseHandler) {
s.mu.Lock()
s.onCloseHandler = handler
s.mu.Unlock()
}
// Send queues a message for delivery to the client. It is delivered on the next
// flush: immediately over WebSocket, or on the next poll for long-polling. Send
// is safe for concurrent use.
func (s *ServerSocket) Send(data []byte, isBinary bool) error {
s.mu.Lock()
if s.state == sessionClosed || s.state == sessionClosing {
s.mu.Unlock()
return ErrSocketClosed
}
s.writeBuffer = append(s.writeBuffer, Packet{Type: PacketMessage, Data: data, IsBinary: isBinary})
s.mu.Unlock()
s.flush()
return nil
}
// Close gracefully closes the session, delivering a close packet to the client
// before tearing down.
func (s *ServerSocket) Close() error {
if !s.beginClose() {
return nil
}
// Deliver everything still buffered followed by the close packet, then tear
// down. flush completes the teardown once the transport accepts the buffer:
// immediately over WebSocket, or on the client's next poll for long-polling,
// so a message sent just before Close is not dropped.
s.flush()
// If a long-poll transport had no poll held, the buffer is still pending and
// the next poll will drain it and close the session. Arm a fallback so a
// client that never polls again cannot leak the session in the closing state.
s.mu.Lock()
if s.state == sessionClosing && s.closeTimer == nil {
s.closeTimer = time.AfterFunc(s.options.pingTimeout, func() {
s.closeWithReason("forced close", nil)
})
}
s.mu.Unlock()
return nil
}
// beginClose claims the closing transition under a single lock, stops the
// heartbeat, and queues the close packet, so Close can flush and tear down
// without touching the mutex. The heartbeat is stopped now so a ping or
// pong-timeout cannot fire during the drain wait and tear the session down early
// (with the wrong reason and before the buffered data is delivered). It reports
// false when the session is not in a state that can be closed.
func (s *ServerSocket) beginClose() bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != sessionOpening && s.state != sessionOpen {
return false
}
s.state = sessionClosing
s.stopTimersLocked()
s.writeBuffer = append(s.writeBuffer, Packet{Type: PacketClose})
return true
}
// open marks the session open and arms the heartbeat once the handshake values
// are known.
func (s *ServerSocket) open() {
s.mu.Lock()
if s.state != sessionOpening {
s.mu.Unlock()
return
}
s.state = sessionOpen
s.armPingLocked()
s.mu.Unlock()
}
// flush writes any buffered packets to the active transport. It must be called
// without holding s.mu. flushMu serializes it so packets are never written
// twice, and the actual transport write happens outside s.mu so a slow client
// cannot block the heartbeat.
func (s *ServerSocket) flush() {
s.flushMu.Lock()
defer s.flushMu.Unlock()
for {
s.mu.Lock()
if s.state == sessionClosed || s.transport == nil || len(s.writeBuffer) == 0 {
s.mu.Unlock()
return
}
packets := slices.Clone(s.writeBuffer)
transport := s.transport
s.mu.Unlock()
sent, err := transport.send(packets)
switch {
case err != nil:
s.closeWithReason("transport error", err)
return
case !sent:
// A polling transport with no poll held keeps the packets buffered.
return
}
s.mu.Lock()
// Drop the packets handed off, tolerating a concurrent close that may
// have already emptied the buffer.
if count := len(packets); count <= len(s.writeBuffer) {
s.writeBuffer = s.writeBuffer[count:]
} else {
s.writeBuffer = nil
}
drained := len(s.writeBuffer) == 0
closing := s.state == sessionClosing
closed := s.state == sessionClosed
s.mu.Unlock()
if drained && closing {
// The graceful close packet, and everything queued before it, has been
// handed to the transport; complete the teardown now.
s.closeWithReason("forced close", nil)
return
}
if drained || closed {
return
}
}
}
// handlePacket dispatches an inbound packet received on the active transport.
func (s *ServerSocket) handlePacket(p Packet) {
switch p.Type {
case PacketPong:
s.onPong()
case PacketMessage:
s.deliverMessage(p.Data, p.IsBinary)
case PacketClose:
s.closeWithReason("transport close", nil)
case PacketPing, PacketUpgrade, PacketOpen, PacketNoop:
// A v4 client never originates these on an active transport; ignore them.
}
}
// deliverMessage invokes the message handler outside the lock.
func (s *ServerSocket) deliverMessage(data []byte, isBinary bool) {
s.mu.Lock()
handler := s.onMessageHandler
s.mu.Unlock()
if handler != nil {
handler(data, isBinary)
}
}
// onPong clears the pong deadline and schedules the next ping.
func (s *ServerSocket) onPong() {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != sessionOpen {
return
}
if s.pongTimer != nil {
s.pongTimer.Stop()
s.pongTimer = nil
}
s.armPingLocked()
}
// armPingLocked schedules the next ping. The caller must hold s.mu.
func (s *ServerSocket) armPingLocked() {
if s.pingTimer != nil {
s.pingTimer.Stop()
}
s.pingTimer = time.AfterFunc(s.options.pingInterval, s.sendPing)
}
// sendPing emits a ping and arms the pong deadline.
func (s *ServerSocket) sendPing() {
s.mu.Lock()
if s.state != sessionOpen {
s.mu.Unlock()
return
}
s.writeBuffer = append(s.writeBuffer, Packet{Type: PacketPing})
if s.pongTimer != nil {
s.pongTimer.Stop()
}
s.pongTimer = time.AfterFunc(s.options.pingTimeout, func() {
s.closeWithReason("ping timeout", nil)
})
s.mu.Unlock()
s.flush()
}
// closeWithReason tears the session down, reporting the human-readable reason
// and the underlying cause (which may be nil) to the close handler. It is
// idempotent.
func (s *ServerSocket) closeWithReason(reason string, cause error) {
transport, upgrade, probeNoopStop, handler, ok := s.beginTeardown()
if !ok {
return
}
if probeNoopStop != nil {
close(probeNoopStop)
}
if transport != nil {
transport.close()
}
if upgrade != nil {
upgrade.close()
}
s.server.sessions.delete(s.id)
if handler != nil {
handler(reason, cause)
}
}
// beginTeardown claims the closed transition, stops the timers, and snapshots the
// resources to release under a single lock, so closeWithReason can close the
// channels and transports and notify the handler without touching the mutex. It
// reports false when the session is already closed, making the teardown
// idempotent.
func (s *ServerSocket) beginTeardown() (transport, upgrade serverTransport, probeNoopStop chan struct{}, handler ServerCloseHandler, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
if s.state == sessionClosed {
return nil, nil, nil, nil, false
}
s.state = sessionClosed
s.stopTimersLocked()
transport = s.transport
upgrade = s.upgradeTransport
probeNoopStop = s.probeNoopStop
handler = s.onCloseHandler
s.transport = nil
s.upgradeTransport = nil
s.upgrading = false
s.probeNoopStop = nil
s.writeBuffer = nil
return transport, upgrade, probeNoopStop, handler, true
}
// stopTimersLocked stops and clears the heartbeat and upgrade timers. The caller
// must hold s.mu.
func (s *ServerSocket) stopTimersLocked() {
if s.pingTimer != nil {
s.pingTimer.Stop()
s.pingTimer = nil
}
if s.pongTimer != nil {
s.pongTimer.Stop()
s.pongTimer = nil
}
if s.upgradeTimer != nil {
s.upgradeTimer.Stop()
s.upgradeTimer = nil
}
if s.closeTimer != nil {
s.closeTimer.Stop()
s.closeTimer = nil
}
}
// startUpgrade begins probing an upgrade to the given transport while the polling
// transport keeps running. It reports whether the probe may proceed.
func (s *ServerSocket) startUpgrade(transport serverTransport) bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != sessionOpen || s.upgrading || s.transport == nil || s.transport.transportType() != TransportTypePolling {
return false
}
s.upgrading = true
s.upgradeTransport = transport
s.upgradeTimer = time.AfterFunc(s.options.upgradeTimeout, func() {
s.abortUpgrade(transport)
})
return true
}
// onProbe responds to a probe ping by flushing the held poll with noop packets
// until the upgrade resolves, so the client can finish pausing the polling
// transport.
func (s *ServerSocket) onProbe() {
s.mu.Lock()
if !s.upgrading || s.probeNoopStop != nil {
s.mu.Unlock()
return
}
stop := make(chan struct{})
s.probeNoopStop = stop
s.mu.Unlock()
go func() {
var ticker = time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
s.flushProbeNoop()
for {
select {
case <-stop:
return
case <-ticker.C:
s.flushProbeNoop()
}
}
}()
}
// flushProbeNoop writes a noop to the held poll, if the active transport is
// still polling.
func (s *ServerSocket) flushProbeNoop() {
s.mu.Lock()
transport := s.transport
s.mu.Unlock()
if polling, ok := transport.(*serverPollingTransport); ok {
polling.writeNoop()
}
}
// completeUpgrade promotes the probing transport to the active transport and
// discards the polling transport.
func (s *ServerSocket) completeUpgrade(transport promotableTransport) {
old, stop, ok := s.beginCompleteUpgrade(transport)
if !ok {
return
}
transport.promote()
if stop != nil {
close(stop)
}
if old != nil {
old.close()
}
s.flush()
}
// beginCompleteUpgrade swaps the probing transport in as the active transport and
// snapshots the polling transport to discard and the probe-noop stop channel
// under a single lock, so completeUpgrade can promote, close, and flush without
// touching the mutex. It reports false when the probe no longer matches the
// in-flight upgrade.
func (s *ServerSocket) beginCompleteUpgrade(transport promotableTransport) (old serverTransport, stop chan struct{}, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.upgrading || s.upgradeTransport != transport {
return nil, nil, false
}
s.upgrading = false
old = s.transport
s.transport = transport
s.upgradeTransport = nil
if s.upgradeTimer != nil {
s.upgradeTimer.Stop()
s.upgradeTimer = nil
}
stop = s.probeNoopStop
s.probeNoopStop = nil
return old, stop, true
}
// abortUpgrade abandons a stalled or failed probe and keeps the session running
// on the polling transport.
func (s *ServerSocket) abortUpgrade(transport serverTransport) {
stop, ok := s.beginAbortUpgrade(transport)
if !ok {
return
}
if stop != nil {
close(stop)
}
transport.close()
}
// beginAbortUpgrade clears the in-flight upgrade and snapshots the probe-noop
// stop channel under a single lock, so abortUpgrade can close the channel and the
// probing transport without touching the mutex. It reports false when the probe
// no longer matches the in-flight upgrade.
func (s *ServerSocket) beginAbortUpgrade(transport serverTransport) (stop chan struct{}, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.upgrading || s.upgradeTransport != transport {
return nil, false
}
s.upgrading = false
s.upgradeTransport = nil
if s.upgradeTimer != nil {
s.upgradeTimer.Stop()
s.upgradeTimer = nil
}
stop = s.probeNoopStop
s.probeNoopStop = nil
return stop, true
}