Skip to content

Commit df043a0

Browse files
authored
Experimental WebSocket over HTTP/2 (RFC 8441) (#519)
1 parent 1d47f1c commit df043a0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2691
-250
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ devdata/
1010
.luarc.json
1111
_examples/benchmark/bench_server/bench_server
1212
_examples/benchmark/bench_client/bench_client
13+
.claude

_examples/experimental/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
This folder contains examples which use experimental features of Centrifuge library. These examples may require additional local setup to make them work.
1+
This folder contains examples which use experimental features of Centrifuge library.
2+
3+
These examples may require additional local setup to make them work.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# HTTP/2 WebSocket Example
2+
3+
This example demonstrates WebSocket over HTTP/2 using RFC 8441 extended CONNECT protocol.
4+
5+
## Running the Example
6+
7+
First gen certs following certs/README.md instructions.
8+
9+
Then run the server with:
10+
11+
```bash
12+
GODEBUG=http2xconnect=1 go run main.go
13+
```
14+
15+
Open https://localhost:8080 in your browser.
16+
17+
## Important Notes
18+
19+
1. **GODEBUG environment variable**: The Go standard library requires `GODEBUG=http2xconnect=1` to enable HTTP/2 extended CONNECT support. See https://github.com/golang/go/issues/53208
20+
21+
2. **Browser Support**: Modern browsers (Chrome, Firefox, Safari) will automatically negotiate HTTP/2 when connecting via `wss://` (secure WebSocket). The browser handles the HTTP/2 upgrade transparently.
22+
23+
3. **Network Inspection**: To verify HTTP/2 is being used:
24+
- Open browser DevTools → Network tab
25+
- WebSocket connections over HTTP/2 will have "200" status code instead of 101.
26+
27+
## Server Options
28+
29+
```
30+
-port int
31+
Port to bind app to (default 8080)
32+
-cert string
33+
TLS certificate file (default "certs/localhost.pem")
34+
-key string
35+
TLS key file (default "certs/localhost-key.pem")
36+
```
37+
38+
## How It Works
39+
40+
The server configures the WebSocket upgrader with:
41+
42+
```go
43+
centrifuge.WebsocketConfig{
44+
EnableHTTP2ExtendedConnect: true,
45+
}
46+
```
47+
48+
This allows the same endpoint to accept:
49+
- HTTP/1.1 WebSocket upgrade (traditional `Upgrade: websocket`)
50+
- HTTP/2 extended CONNECT (RFC 8441 with `:protocol: websocket` pseudo-header)
51+
52+
The client (browser) automatically chooses the appropriate protocol based on the connection type.
53+
54+
## Emulate Latency
55+
56+
# Create pipes with 50ms delay each way (100ms RTT), apply to port 8080:
57+
58+
```
59+
sudo dnctl pipe 1 config delay 50
60+
sudo dnctl pipe 2 config delay 50
61+
sudo pfctl -e
62+
echo "
63+
dummynet in proto tcp from any port 8080 to any pipe 1
64+
dummynet out proto tcp from any to any port 8080 pipe 2
65+
" | sudo pfctl -f -
66+
```
67+
68+
Clean up:
69+
70+
```
71+
sudo dnctl -q flush
72+
sudo pfctl -d
73+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ignore certificate files
2+
*.pem
3+
*.key
4+
*.crt
5+
*.cert
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
**CERTS FOR DEVELOPMENT ONLY**
2+
3+
To generate certs install https://github.com/FiloSottile/mkcert, then:
4+
5+
```bash
6+
mkcert --install
7+
mkcert localhost
8+
```
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Centrifuge HTTP/2 WebSocket Example</title>
6+
<style type="text/css">
7+
input[type="text"] { width: 300px; }
8+
.muted {color: #CCCCCC; font-size: 10px;}
9+
</style>
10+
<script type="text/javascript" src="https://unpkg.com/centrifuge@^5/dist/centrifuge.js"></script>
11+
<script type="text/javascript">
12+
// helper functions to work with escaping html.
13+
const tagsToReplace = {'&': '&amp;', '<': '&lt;', '>': '&gt;'};
14+
function replaceTag(tag) {return tagsToReplace[tag] || tag;}
15+
function safeTagsReplace(str) {return str.replace(/[&<>]/g, replaceTag);}
16+
17+
const channel = "chat:index";
18+
19+
window.addEventListener('load', function() {
20+
const input = document.getElementById("input");
21+
const container = document.getElementById('messages');
22+
23+
const transports = [
24+
{
25+
transport: 'websocket',
26+
endpoint: `wss://${window.location.host}/connection/websocket`
27+
},
28+
{
29+
transport: 'http_stream',
30+
endpoint: `https://${window.location.host}/connection/http_stream`
31+
}
32+
];
33+
34+
const centrifuge = new Centrifuge(transports, {
35+
// Workaround to reliably use HTTP/2 Extended CONNECT in Chrome upon reconnections
36+
// (when no HTTP/2 connection in pool exists Chrome selects HTTP/1.1 for WebSocket).
37+
// So we first make some HTTP request to ensure HTTP/2 connection is established and
38+
// can be reused for WebSocket.
39+
getData: function () {
40+
return fetch('/ping', {method: 'GET'}).then(function() {
41+
return null;
42+
});
43+
}
44+
});
45+
46+
const start = performance.now();
47+
centrifuge.on('connected', function(ctx){
48+
const total = performance.now() - start;
49+
console.log(`\nTotal: ${total.toFixed(0)}ms`);
50+
drawText('Connected with client ID ' + ctx.client + ' over ' + ctx.transport + ' with data: ' + JSON.stringify(ctx.data));
51+
input.removeAttribute('disabled');
52+
});
53+
54+
centrifuge.on('connecting', function(ctx){
55+
drawText('Connecting: ' + ctx.reason);
56+
input.setAttribute('disabled', 'true');
57+
});
58+
59+
centrifuge.on('disconnected', function(ctx){
60+
drawText('Disconnected: ' + ctx.reason);
61+
input.setAttribute('disabled', 'true');
62+
});
63+
64+
centrifuge.on('error', function(ctx){
65+
drawText('Client error: ' + JSON.stringify(ctx));
66+
centrifuge.connect();
67+
});
68+
69+
centrifuge.on('message', function(data) {
70+
drawText('Message: ' + JSON.stringify(data));
71+
});
72+
73+
centrifuge.on('publication', function(ctx) {
74+
drawText('Server-side publication from channel ' + ctx.channel + ": " + JSON.stringify(ctx.data));
75+
});
76+
77+
centrifuge.on('join', function(ctx) {
78+
drawText('Server-side join from channel ' + ctx.channel + ": " + JSON.stringify(ctx.info));
79+
});
80+
81+
centrifuge.on('leave', function(ctx) {
82+
drawText('Server-side leave from channel ' + ctx.channel + ": " + JSON.stringify(ctx.info));
83+
});
84+
85+
centrifuge.on('subscribed', function(ctx) {
86+
drawText('Subscribed to server-side channel ' + ctx.channel + ' (ctx: ' + JSON.stringify(ctx) + ')');
87+
});
88+
89+
centrifuge.on('subscribing', function(ctx) {
90+
drawText('Subscribing to server-side channel ' + ctx.channel);
91+
});
92+
93+
centrifuge.on('unsubscribed', function(ctx) {
94+
drawText('Unsubscribe from server-side channel ' + ctx.channel);
95+
});
96+
97+
// show how many users currently in channel.
98+
function showPresence(sub) {
99+
sub.presence().then(function(result) {
100+
let count = 0;
101+
for (let key in result.clients){
102+
count++;
103+
}
104+
drawText('Presence: now in this room – ' + count + ' clients');
105+
}, function(err) {
106+
drawText("Presence error: " + JSON.stringify(err));
107+
});
108+
}
109+
110+
// subscribe on channel and bind various event listeners. Actual
111+
// subscription request will be sent after client connects to
112+
// a server.
113+
const sub = centrifuge.newSubscription(channel, {});
114+
115+
sub.on("publication", function(ctx) {
116+
drawText('Client-side subscription publication: ' + ctx.channel + ", " + JSON.stringify(ctx.data));
117+
});
118+
sub.on("join", function(ctx) {
119+
drawText('Client-side subscription join: ' + ctx.channel + ", " + JSON.stringify(ctx.info));
120+
});
121+
sub.on("leave", function(ctx) {
122+
drawText('Client-side subscription leave: ' + ctx.channel + ", " + JSON.stringify(ctx.info));
123+
});
124+
sub.on("subscribing", function(ctx) {
125+
drawText('Client-side subscription subscribing: ' + ctx.channel);
126+
});
127+
sub.on("subscribed", function(ctx) {
128+
drawText('Client-side subscription subscribed to channel ' + ctx.channel);
129+
showPresence(sub);
130+
});
131+
sub.on("unsubscribed", function(ctx) {
132+
drawText('Client-side subscription unsubscribed from channel ' + ctx.channel);
133+
});
134+
sub.on("error", function(ctx) {
135+
drawText('Client-side subscription error: ' + ctx.channel);
136+
});
137+
138+
sub.subscribe();
139+
140+
// Trigger actual connection request to server.
141+
centrifuge.connect();
142+
143+
// Publish to channel when user presses key in input field.
144+
input.addEventListener('keyup', function(e) {
145+
if (e.keyCode === 13) {
146+
sub.publish({"input": input.value}).then(function() {
147+
console.log("message accepted by server");
148+
}, function(err) {
149+
drawText("Error publishing message: " + JSON.stringify(err));
150+
});
151+
input.value = '';
152+
}
153+
});
154+
155+
// Draw raw data in textarea.
156+
function drawText(text) {
157+
const time = '<span class="muted">' + new Date().toLocaleTimeString() + '</span>';
158+
container.innerHTML = time + ' ' + safeTagsReplace(text) + '<br>' + container.innerHTML;
159+
}
160+
}, false);
161+
</script>
162+
</head>
163+
<body>
164+
<h3>Centrifuge WebSocket over HTTP/2 Demo</h3>
165+
<p>This example demonstrates WebSocket over HTTP/2 using RFC 8441 extended CONNECT.</p>
166+
<div id="counter"></div>
167+
<input type="text" id="input" autocomplete="off" disabled />
168+
<div id="messages"></div>
169+
</body>
170+
</html>

0 commit comments

Comments
 (0)