-
-
Notifications
You must be signed in to change notification settings - Fork 121
Expand file tree
/
Copy pathdictionary_compression.go
More file actions
176 lines (164 loc) · 9.08 KB
/
Copy pathdictionary_compression.go
File metadata and controls
176 lines (164 loc) · 9.08 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
package centrifuge
import (
"github.com/centrifugal/protocol"
)
// ConnectionFlagDictionaryCompression is advertised by a client in
// ConnectRequest.flag to say it can decode dictionary-compressed frames, and
// echoed in ConnectResult.flag when the server enabled it.
//
// A client must not assume a feature it advertised was accepted: the server may
// have it switched off, or may decline for this particular connection.
const ConnectionFlagDictionaryCompression int64 = 1 << 0
// DictionaryCompression compresses outgoing frames against a shared dictionary.
//
// This package provides no implementation. It defines the shape of one and the
// points where a connection hands work to it, so compression can be supplied
// from outside - which is where the decisions live: which dictionary a
// connection gets, where dictionaries come from, and what may go in one.
//
// A nil DictionaryCompression means the feature is absent. Nothing is allocated
// and no code path here is entered.
type DictionaryCompression interface {
// NewDictionaryConnection is called once per connection, after the client has been
// authenticated and before the connect reply is written.
//
// Returning nil leaves the connection uncompressed, which is the right answer
// for a client that cannot decode, a profile with nothing to offer, or any
// case the implementation would rather sit out.
NewDictionaryConnection(params DictionaryConnectionParams) DictionaryConnection
}
// DictionaryConnectionParams describes the client a DictionaryConnection is
// being made for. It is a struct rather than an argument list so more can be
// negotiated later without breaking implementations outside this package.
type DictionaryConnectionParams struct {
// ProtocolType is the connection's protocol. A dictionary built from JSON
// frames is useless on Protobuf, so implementations must keep them apart.
ProtocolType ProtocolType
// ClientFlags is the capability bitmask the client advertised.
ClientFlags int64
// Profile is the application context this connection belongs to, resolved by
// the server. Empty means unclassified.
Profile string
// UserID is the authenticated user, or empty for an anonymous connection.
//
// It is here because it is the only identity that survives a reconnect, and
// a staged rollout needs one: a cohort re-drawn on every reconnect is not a
// cohort, and a client that fell out of one would pay to be sent a
// dictionary it already had. Implementations that split traffic should
// derive the split from this rather than from anything per-connection.
UserID string
// HeldDictionaryID is the dictionary the client says it already has from an
// earlier connection. An id identifies dictionary content, so a match means
// both sides hold the same bytes and the dictionary can be named rather than
// sent. An unrecognised id is simply ignored.
HeldDictionaryID string
}
// DictionaryConnection compresses one connection's outgoing frames against a
// dictionary. It is named for the dictionary rather than for compression in
// general: a connection may also be using permessage-deflate, delta compression
// or channel compaction, and none of those go through here.
type DictionaryConnection interface {
// Dictionary returns what to put in ConnectResult.dict, or nil to send
// nothing.
//
// It is called once, before the connect reply is written.
//
// Compression always begins on the frame AFTER the connect reply, whether
// this returns bytes or only an id. The reply itself goes out in the plain
// protocol - text JSON, length-prefixed Protobuf - so a client never has to
// work out whether the frame that establishes compression was itself
// compressed.
//
// A reply could be compressed when the client already holds the dictionary,
// and briefly was. Doing so requires the client to decide how to decode the
// reply before it can read it, and the two encodings are not reliably
// distinguishable: a Protobuf ping is an empty reply, a single 0x00 byte,
// which is exactly what a raw frame marker looks like. Every scheme that
// resolves that costs either a byte on every frame or an invariant about
// minimum frame sizes that nobody would think to preserve - to save one
// small frame, once per connection.
//
// Returning only an id means the client presented that id and already holds
// the bytes, so nothing is transferred - which is where the saving on a
// returning connection actually is, and it is unaffected by any of this.
//
// Set only the id, with no bytes, when this engine RECOGNISES the id the
// client advertised - meaning it holds that dictionary and will compress
// against it. Matching the advertised id is necessary but not sufficient:
// echoing back an id merely because the client sent it is a security bug,
// not just a correctness one.
//
// A client's stored dictionary can be tampered with - browser storage is
// writable by anything running on the origin - and a client cannot fully
// detect it, since bytes and id rewritten together verify locally. What
// stops that is the server refusing to recognise an id it never issued, so
// the tampered bytes are replaced rather than used. An engine that echoes
// the advertised id removes exactly that defence: the client then decodes
// real server frames against attacker-chosen content, because DEFLATE back
// references resolve into whatever dictionary is installed.
//
// Centrifuge cannot check this for you. It does not know which dictionaries
// an engine holds, so it can only reject an id the client never mentioned.
Dictionary() *protocol.Dictionary
// Encode is called for every frame this connection compresses, on the
// connection's write goroutine - including the connect reply when the
// client already held the dictionary.
//
// It returns the bytes to write and whether they must go out as a binary
// message, which compressed payloads need even on a JSON connection.
//
// One publication reaches every subscriber of a channel as the same bytes,
// so implementations are called with an identical frame once per
// subscriber, from that many goroutines, at nearly the same instant. What
// an implementation does about that decides what the feature costs: a
// dictionary compression is dominated by loading the dictionary rather than
// by the frame, so compressing a fan-out once instead of once per
// subscriber is the difference between the feature being viable and not.
//
// A cache alone does not achieve it. The subscribers arrive together and
// all miss, because the first has not finished compressing yet - measured
// on a four-subscriber channel, the same frame was compressed three or four
// times over while the cache reported a plausible hit rate. Collapsing the
// duplicates as well removed half the compressions and a third of the CPU.
//
// This package deliberately provides neither, so that engines keep control
// of their own memory and lifetime. It is what an implementation should
// build first.
Encode(frame []byte) (out []byte, binary bool)
// Close is called once when the connection goes away, on the same goroutine
// as the final Encode and never concurrently with one.
//
// Implementations that batch their accounting need this, and need it to be
// exact rather than best-effort. Without it, everything a connection did
// since its last flush is lost - and what is lost is not a random sample:
// short connections are the ones that end before a flush, and they are also
// the ones that most often pay to be sent a dictionary. Dropping them makes
// compression look better than it is, in a number an operator uses to decide
// whether the feature is worth keeping on.
Close()
}
// DictionaryAwareTransport is implemented by transports that can carry
// dictionary-compressed frames. A transport that does not implement it is left
// alone: no dictionary is offered on it, and the connect reply never claims
// compression that was not installed. That is what makes it safe to enable the
// feature for a mixed fleet, where a client may arrive over a fallback
// transport that cannot carry a compressed frame.
//
// It is exported so transports defined outside this package can opt in, and
// only for that. Centrifuge calls these methods itself, as part of the connect
// handshake and connection teardown; application code should not. Installing a
// codec outside the handshake compresses frames against a dictionary the client
// was never sent, and it has no way to report that it cannot read them.
// Whether a transport can is a question about framing rather than effort: a
// compressed frame is arbitrary bytes, so a transport that delimits messages
// with a newline cannot carry one without re-encoding it and giving the saving
// straight back.
type DictionaryAwareTransport interface {
// SetDictionaryCompression installs a codec that must not encode the next
// frame written, because that frame is the connect reply carrying the
// dictionary this codec uses.
SetDictionaryCompression(cc DictionaryConnection)
// CloseDictionaryCompression is called once the writer has stopped, so an
// implementation's Close cannot race the last Encode.
CloseDictionaryCompression()
}