Skip to content

Commit 111007d

Browse files
authored
Add optional server-side player keepalive to reap dead connections (#912)
1 parent a516a78 commit 111007d

4 files changed

Lines changed: 44 additions & 2 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@epicgames-ps/lib-pixelstreamingsignalling-ue5.7': patch
3+
'@epicgames-ps/wilbur': patch
4+
---
5+
6+
Add an optional server-side keepalive that disconnects players whose connection has died without a clean close. Previously `KeepaliveMonitor` was only used on the client, so a player whose socket dropped silently (sleeping laptop, lost Wi-Fi, killed tab) stayed subscribed until the OS TCP keepalive reaped it, which could hold a `maxSubscribers` slot in the meantime. The new `IServerConfig.playerKeepaliveTimeout` controls this; the signalling server exposes it as `--player_keepalive_timeout <milliseconds>` (default 30000, 0 disables).

Signalling/src/SignallingServer.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { SFUConnection } from './SFUConnection';
88
import { Logger } from './Logger';
99
import { StreamerRegistry } from './StreamerRegistry';
1010
import { PlayerRegistry } from './PlayerRegistry';
11-
import { Messages, MessageHelpers, SignallingProtocol } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.7';
11+
import {
12+
Messages,
13+
MessageHelpers,
14+
SignallingProtocol,
15+
KeepaliveMonitor
16+
} from '@epicgames-ps/lib-pixelstreamingcommon-ue5.7';
1217
import { stringify } from './Utils';
1318

1419
/**
@@ -45,6 +50,10 @@ export interface IServerConfig {
4550

4651
// Max number of players per streamer.
4752
maxSubscribers?: number;
53+
54+
// Idle timeout in milliseconds after which a player that has stopped responding to keepalive
55+
// pings is forcibly disconnected. 0 (the default) disables the check.
56+
playerKeepaliveTimeout?: number;
4857
}
4958

5059
export type ProtocolConfig = {
@@ -159,6 +168,25 @@ export class SignallingServer {
159168
Logger.info(`Player %s (%s) disconnected.`, newPlayer.playerId, request.socket.remoteAddress);
160169
});
161170

171+
// Optionally monitor the player connection for liveness. A player whose socket dies without
172+
// a clean close frame (sleeping laptop, dropped Wi-Fi, killed tab) is otherwise only removed
173+
// once the OS TCP keepalive eventually reaps it, leaving it subscribed in the meantime. When
174+
// maxSubscribers is set this can hold a slot that no live player is using. We use
175+
// ws.terminate() rather than a graceful close because a dead peer never completes the close
176+
// handshake. The monitor stops itself on transport 'close', so no manual teardown is needed.
177+
const keepaliveTimeout = this.config.playerKeepaliveTimeout || 0;
178+
if (keepaliveTimeout > 0) {
179+
const keepalive = new KeepaliveMonitor(newPlayer.protocol, keepaliveTimeout);
180+
keepalive.onTimeout = () => {
181+
Logger.info(
182+
`Player %s (%s) failed keepalive - terminating dead connection.`,
183+
newPlayer.playerId,
184+
request.socket.remoteAddress
185+
);
186+
ws.terminate();
187+
};
188+
}
189+
162190
this.sendConfigMessage(newPlayer);
163191
}
164192

SignallingWebServer/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Options:
5050
--player_port <port> Sets the listening port for player connections. (default: "80")
5151
--sfu_port <port> Sets the listening port for SFU connections. (default: "8889")
5252
--max_players <number> Sets the maximum number of subscribers per streamer. 0 = unlimited (default: "0")
53+
--player_keepalive_timeout <milliseconds>
54+
Disconnect a player after this many milliseconds without a keepalive response. 0 = disabled (default: "30000")
5355
--serve Enables the webserver on player_port. (default: true)
5456
--http_root <path> Sets the path for the webserver root. (default: "D:\\PixelStreamingInfrastructure\\SignallingWebServer\\www")
5557
--homepage <filename> The default html file to serve on the web server. (default: "player.html")

SignallingWebServer/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ program
108108
'Sets the maximum number of subscribers per streamer. 0 = unlimited',
109109
config_file.max_players || '0'
110110
)
111+
.option(
112+
'--player_keepalive_timeout <milliseconds>',
113+
'Disconnect a player after this many milliseconds without a keepalive response. 0 = disabled',
114+
config_file.player_keepalive_timeout || '30000'
115+
)
111116
.option('--serve', 'Enables the webserver on player_port.', config_file.serve || false)
112117
.option(
113118
'--http_root <path>',
@@ -270,7 +275,8 @@ const serverOpts: IServerConfig = {
270275
playerPort: options.player_port,
271276
sfuPort: options.sfu_port,
272277
peerOptions: options.peer_options,
273-
maxSubscribers: options.max_players
278+
maxSubscribers: options.max_players,
279+
playerKeepaliveTimeout: Number(options.player_keepalive_timeout)
274280
};
275281

276282
const shouldServerStart = options.serve || options.rest_api;

0 commit comments

Comments
 (0)