@@ -8,6 +8,8 @@ class WebSocketServer implements WebSocketInterface
8
8
private $ port ;
9
9
private $ socket ;
10
10
private $ clients = [];
11
+ private $ running = true ;
12
+ private $ socketClosed = false ;
11
13
12
14
public function __construct ($ host = '0.0.0.0 ' , $ port = 8081 )
13
15
{
@@ -17,6 +19,10 @@ public function __construct($host = '0.0.0.0', $port = 8081)
17
19
18
20
public function start ()
19
21
{
22
+ // Register signal handlers for SIGINT and SIGTERM
23
+ pcntl_signal (SIGINT , [$ this , 'stop ' ]); // Handle Ctrl+C
24
+ pcntl_signal (SIGTERM , [$ this , 'stop ' ]); // Handle termination signals
25
+
20
26
// Create a TCP/IP socket
21
27
$ this ->socket = socket_create (AF_INET , SOCK_STREAM , SOL_TCP );
22
28
if ($ this ->socket === false ) {
@@ -39,14 +45,32 @@ public function start()
39
45
echo "WebSocket server started on ws:// {$ this ->host }: {$ this ->port }\n" ;
40
46
41
47
// Main server loop
42
- while (true ) {
48
+ while ($ this ->running ) {
49
+ // Dispatch any pending signals
50
+ pcntl_signal_dispatch ();
51
+
43
52
// Prepare the read array with the server socket and all client sockets
44
53
$ read = array_merge ([$ this ->socket ], $ this ->clients );
45
54
$ write = $ except = null ;
46
55
47
56
// Use socket_select to monitor sockets for activity
48
- if (socket_select ($ read , $ write , $ except , null ) === false ) {
49
- die ("socket_select failed: " . socket_strerror (socket_last_error ()) . "\n" );
57
+ $ result = @socket_select ($ read , $ write , $ except , 1 ); // 1-second timeout
58
+
59
+ if ($ result === false ) {
60
+ $ error = socket_last_error ();
61
+ if ($ error === SOCKET_EINTR ) {
62
+ // Interrupted by a signal, continue the loop
63
+ continue ;
64
+ }
65
+ die ("socket_select failed: " . socket_strerror ($ error ) . "\n" );
66
+ }
67
+
68
+ if ($ result === 0 ) {
69
+ // Timeout, check if we should stop
70
+ if (!$ this ->running ) {
71
+ break ;
72
+ }
73
+ continue ;
50
74
}
51
75
52
76
// Handle new connections
@@ -103,27 +127,36 @@ public function start()
103
127
$ this ->broadcast ($ decodedFrame ['payload ' ]);
104
128
}
105
129
}
130
+
131
+ // Clean up resources
132
+ $ this ->stop ();
106
133
}
107
134
108
135
public function handleConnection ($ socket )
109
136
{
110
137
$ this ->clients [] = $ socket ;
111
138
112
- while (true ) {
113
-
114
- $ data = socket_read ($ socket , 8192 , PHP_BINARY_READ );
115
- if ($ data === false ) {
116
- echo "socket_read failed: " . socket_strerror (socket_last_error ()) . "\n" ;
117
- $ this ->removeClient ($ socket );
139
+ foreach ($ this ->clients as $ client ) {
140
+ $ data = @socket_read ($ client , 8192 , PHP_BINARY_READ );
141
+ if ($ data === false || $ data === '' ) {
142
+ // Client disconnected
143
+ $ this ->removeClient ($ client );
118
144
continue ;
119
145
}
120
146
121
147
// Decode the WebSocket frame
122
148
$ decodedFrame = $ this ->decodeWebSocketFrame ($ data );
123
149
if ($ decodedFrame === null ) {
124
150
echo "Invalid WebSocket frame received. \n" ;
125
- $ this ->removeClient ($ socket );
126
- break ;
151
+ $ this ->removeClient ($ client );
152
+ continue ;
153
+ }
154
+
155
+ // Handle close frame from the client
156
+ if ($ decodedFrame ['opcode ' ] === 0x8 ) { // 0x8 = close frame
157
+ echo "Client sent a close frame. Closing connection. \n" ;
158
+ $ this ->removeClient ($ client );
159
+ continue ;
127
160
}
128
161
129
162
// Log the decoded frame
@@ -133,7 +166,16 @@ public function handleConnection($socket)
133
166
134
167
// Send a response back to the client
135
168
$ responseFrame = $ this ->encodeWebSocketFrame ("Server received: " . $ decodedFrame ['payload ' ]);
136
- @socket_write ($ socket , $ responseFrame , strlen ($ responseFrame )); // Suppress warnings
169
+ if (!@socket_write ($ client , $ responseFrame , strlen ($ responseFrame ))) {
170
+ $ error = socket_last_error ($ client );
171
+ if ($ error === SOCKET_EPIPE ) {
172
+ echo "Socket is already closed may be by client. Skipping response. \n" ;
173
+ } else {
174
+ echo "socket_write failed: " . socket_strerror ($ error ) . "\n" ;
175
+ }
176
+ $ this ->removeClient ($ client );
177
+ continue ;
178
+ }
137
179
echo "Sent response to client. \n" ;
138
180
139
181
// Broadcast the message to all clients
@@ -150,10 +192,6 @@ public function handshake($socket)
150
192
return false ;
151
193
}
152
194
153
- // Debug: Output the handshake request
154
- echo "Handshake request received: \n" ;
155
- echo $ request . "\n" ;
156
-
157
195
// Extract the WebSocket key from the request headers
158
196
if (preg_match ('/Sec-WebSocket-Key: (.*)\r\n/ ' , $ request , $ matches )) {
159
197
$ key = trim ($ matches [1 ]);
@@ -171,10 +209,6 @@ public function handshake($socket)
171
209
$ response .= "Connection: Upgrade \r\n" ;
172
210
$ response .= "Sec-WebSocket-Accept: $ acceptKey \r\n\r\n" ;
173
211
174
- // Debug: Output the handshake response
175
- echo "Handshake response: \n" ;
176
- echo $ response . "\n" ;
177
-
178
212
// Send the handshake response
179
213
if (!socket_write ($ socket , $ response , strlen ($ response ))) {
180
214
echo "Failed to send handshake response. \n" ;
@@ -189,8 +223,14 @@ public function removeClient($socket)
189
223
{
190
224
$ index = array_search ($ socket , $ this ->clients );
191
225
if ($ index !== false ) {
192
- unset($ this ->clients [$ index ]);
226
+ // Send a close frame before closing the connection
227
+ $ this ->sendCloseFrame ($ socket , 1000 , 'Server closing connection ' );
228
+
229
+ // Close the socket
193
230
socket_close ($ socket );
231
+
232
+ // Remove the client from the clients array
233
+ unset($ this ->clients [$ index ]);
194
234
echo "Client removed. Total clients: " . count ($ this ->clients ) . "\n" ;
195
235
}
196
236
}
@@ -255,13 +295,13 @@ public function unmaskPayload($payload, $maskingKey)
255
295
return $ unmaskedPayload ;
256
296
}
257
297
258
- public function encodeWebSocketFrame ($ payload )
298
+ public function encodeWebSocketFrame ($ payload, $ opcode = 0x81 )
259
299
{
260
300
$ frame = '' ;
261
301
$ payloadLength = strlen ($ payload );
262
302
263
- // Set the first byte (opcode and flags )
264
- $ frame .= chr (0x81 ); // 0x81 = text frame ( FIN bit set)
303
+ // Set the first byte (FIN bit and opcode )
304
+ $ frame .= chr (0x80 | $ opcode ); // FIN bit set (0x80) + opcode
265
305
266
306
// Set the second byte (mask and payload length)
267
307
if ($ payloadLength <= 125 ) {
@@ -278,11 +318,57 @@ public function encodeWebSocketFrame($payload)
278
318
return $ frame ;
279
319
}
280
320
321
+ public function sendCloseFrame ($ socket , $ statusCode = 1000 , $ reason = '' )
322
+ {
323
+ // Check if the socket is still writable
324
+ if (socket_last_error ($ socket ) === SOCKET_EPIPE ) {
325
+ echo "Socket is already closed. Skipping close frame. \n" ;
326
+ return ;
327
+ }
328
+
329
+ // Pack the status code into 2 bytes
330
+ $ statusCode = pack ('n ' , $ statusCode );
331
+
332
+ // Combine the status code and reason
333
+ $ payload = $ statusCode . $ reason ;
334
+
335
+ // Create the close frame
336
+ $ closeFrame = $ this ->encodeWebSocketFrame ($ payload , 0x88 ); // 0x88 = close frame
337
+
338
+ // Send the close frame
339
+ if (!@socket_write ($ socket , $ closeFrame , strlen ($ closeFrame ))) {
340
+ $ error = socket_last_error ($ socket );
341
+ if ($ error === SOCKET_EPIPE ) {
342
+ echo "Socket is already closed may be by client. Skipping close frame. \n" ;
343
+ } else {
344
+ echo "Failed to send close frame: " . socket_strerror ($ error ) . "\n" ;
345
+ }
346
+ }
347
+ }
348
+
281
349
public function stop ()
282
350
{
283
- if ($ this ->socket ) {
351
+ if (!$ this ->running ) {
352
+ return ;
353
+ }
354
+
355
+ $ this ->running = false ;
356
+
357
+ // Send close frames to all clients
358
+ foreach ($ this ->clients as $ client ) {
359
+ $ this ->sendCloseFrame ($ client , 1000 , 'Server shutting down ' );
360
+ socket_close ($ client );
361
+ }
362
+ echo "Clients connections closed. \n" ;
363
+
364
+ // Close the server socket only if it hasn't been closed already
365
+ if ($ this ->socket && !$ this ->socketClosed ) {
284
366
socket_close ($ this ->socket );
285
- echo "WebSocket server stopped. \n" ;
367
+ $ this ->socketClosed = true ; // Mark the socket as closed
368
+ echo "WebSocket server socket closed. \n" ;
286
369
}
370
+
371
+ // Exit the script
372
+ exit (0 );
287
373
}
288
374
}
0 commit comments