Skip to content

Commit fd78ac3

Browse files
committed
feat(websocket): introduce WebSocket server implementation
1 parent e5e8eb8 commit fd78ac3

10 files changed

Lines changed: 1047 additions & 4 deletions

File tree

examples/10-websocket.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* Copyright © 2024 cclilshy
5+
* Email: jingnigg@gmail.com
6+
*
7+
* This software is licensed under the MIT License.
8+
* For full license details, please visit: https://opensource.org/licenses/MIT
9+
*
10+
* By using this software, you agree to the terms of the license.
11+
* Contributions, suggestions, and feedback are always welcome!
12+
*/
13+
14+
use Ripple\Net\WebSocket;
15+
use Ripple\Net\WebSocket\Server\Connection;
16+
17+
use function Co\wait;
18+
19+
require_once __DIR__ . '/../vendor/autoload.php';
20+
21+
// 创建 WebSocket 服务器
22+
$server = WebSocket::server('ws://0.0.0.0:8001');
23+
24+
if ($server instanceof Throwable) {
25+
echo "Failed to create WebSocket server: " . $server->getMessage() . \PHP_EOL;
26+
exit(1);
27+
}
28+
29+
echo "WebSocket server started on ws://0.0.0.0:8001" . \PHP_EOL;
30+
31+
$server->onRequest = function (mixed $request) {
32+
\var_dump('on request');
33+
};
34+
35+
$server->onConnect = function (Connection $connection) {
36+
echo "New WebSocket connection established" . \PHP_EOL;
37+
38+
// 设置消息处理器
39+
$connection->onMessage = function (string $message, Connection $conn) {
40+
echo "Received message: " . $message . \PHP_EOL;
41+
$conn->sendText("Echo: " . $message);
42+
};
43+
44+
// 设置关闭处理器
45+
$connection->onClose = function (Connection $conn) {
46+
echo "WebSocket connection closed" . \PHP_EOL;
47+
};
48+
49+
$connection->sendText("Welcome to WebSocket server!");
50+
};
51+
52+
// 启动服务器
53+
$server->listen();
54+
wait();

src/Net/Http/Server.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public function listen(): void
125125
$socket = socket_import_stream($client);
126126
socket_setopt($socket, SOL_TCP, TCP_NODELAY, 1);
127127
// @socket_setopt($socket, SOL_SOCKET, SO_RCVBUF, 65536);
128-
// @socket_setopt($socket, SOL_SOCKET, SO_SNDBUF, 65536);
128+
// @socket_setopt($socket, SOL_SOCKET, SO_FINDBUGS, 65536);
129129

130130
$remoteInfo = parse_url("tcp://{$remoteAddr}");
131131

src/Net/Http/Server/Connection.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,17 +244,19 @@ private function onRequest(array $reqInfo): void
244244

245245
// 半关闭检测
246246
$connHeader = $reqInfo['server']['HTTP_CONNECTION'] ?? '';
247+
$upgradeHeader = $reqInfo['server']['HTTP_UPGRADE'] ?? '';
247248
$keepAlive = strtolower($connHeader) === 'keep-alive';
249+
$isWebSocketUpgrade = strtolower($upgradeHeader) === 'websocket' &&
250+
str_contains(strtolower($connHeader), 'upgrade');
248251

249-
if ($keepAlive) {
250-
$response->withHeader('Connection', 'keep-alive');
252+
if ($keepAlive || $isWebSocketUpgrade) {
253+
$response->withHeader('Connection', $keepAlive ? 'keep-alive' : 'Upgrade');
251254
} else {
252255
$response->withHeader('Connection', 'close');
253256
$this->stream->shutdownRead();
254257
}
255258

256259
try {
257-
// call_user_func($this->httpServer->onRequest, $req);
258260
Scheduler::resume($this->httpServer->acquireCoroutine(), $req)->rethrow();
259261
} catch (ConnectionException $exception) {
260262
throw $exception;

src/Net/WebSocket.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* Copyright © 2024 cclilshy
4+
* Email: jingnigg@gmail.com
5+
*
6+
* This software is licensed under the MIT License.
7+
* For full license details, please visit: https://opensource.org/licenses/MIT
8+
*
9+
* By using this software, you agree to the terms of the license.
10+
* Contributions, suggestions, and feedback are always welcome!
11+
*/
12+
13+
namespace Ripple\Net;
14+
15+
use Ripple\Net\WebSocket\Server\Server;
16+
use Ripple\Stream\Exception\ConnectionException;
17+
use Throwable;
18+
use RuntimeException;
19+
20+
use function str_replace;
21+
22+
class WebSocket
23+
{
24+
/**
25+
* 创建 WebSocket 服务器
26+
* @param string $address 监听地址, 格式:ws://host:port 或 wss://host:port
27+
* @param mixed|null $streamContext 流上下文选项
28+
* @return Server|Throwable
29+
*/
30+
public static function server(string $address, mixed $streamContext = null): Server|Throwable
31+
{
32+
try {
33+
$httpAddress = self::convertToHttpAddress($address);
34+
return new Server($httpAddress, $streamContext);
35+
} catch (ConnectionException $e) {
36+
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
37+
}
38+
}
39+
40+
/**
41+
* @param string $wsAddress
42+
* @return string
43+
*/
44+
private static function convertToHttpAddress(string $wsAddress): string
45+
{
46+
$wsAddress = str_replace('ws://', 'http://', $wsAddress);
47+
$wsAddress = str_replace('wss://', 'https://', $wsAddress);
48+
return $wsAddress;
49+
}
50+
}

src/Net/WebSocket/Enum/Opcode.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* Copyright © 2024 cclilshy
4+
* Email: jingnigg@gmail.com
5+
*
6+
* This software is licensed under the MIT License.
7+
* For full license details, please visit: https://opensource.org/licenses/MIT
8+
*
9+
* By using this software, you agree to the terms of the license.
10+
* Contributions, suggestions, and feedback are always welcome!
11+
*/
12+
13+
namespace Ripple\Net\WebSocket\Enum;
14+
15+
use InvalidArgumentException;
16+
17+
enum Opcode: int
18+
{
19+
case CONTINUATION = 0x0; // 继续帧
20+
case TEXT = 0x1; // 文本帧
21+
case BINARY = 0x2; // 二进制帧
22+
case CLOSE = 0x8; // 关闭帧
23+
case PING = 0x9; // ping 帧
24+
case PONG = 0xA; // pong 帧
25+
26+
/**
27+
* 从整数值创建枚举
28+
* @param int $value
29+
* @return self
30+
* @throws InvalidArgumentException
31+
*/
32+
public static function fromValue(int $value): self
33+
{
34+
return match ($value) {
35+
0x0 => self::CONTINUATION,
36+
0x1 => self::TEXT,
37+
0x2 => self::BINARY,
38+
0x8 => self::CLOSE,
39+
0x9 => self::PING,
40+
0xA => self::PONG,
41+
default => throw new InvalidArgumentException("Invalid opcode: {$value}")
42+
};
43+
}
44+
45+
/**
46+
* 是否为控制帧
47+
* @return bool
48+
*/
49+
public function isControlFrame(): bool
50+
{
51+
return match ($this) {
52+
self::CLOSE, self::PING, self::PONG => true,
53+
default => false
54+
};
55+
}
56+
57+
/**
58+
* 是否为数据帧
59+
* @return bool
60+
*/
61+
public function isDataFrame(): bool
62+
{
63+
return match ($this) {
64+
self::TEXT, self::BINARY => true,
65+
default => false
66+
};
67+
}
68+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* Copyright © 2024 cclilshy
4+
* Email: jingnigg@gmail.com
5+
*
6+
* This software is licensed under the MIT License.
7+
* For full license details, please visit: https://opensource.org/licenses/MIT
8+
*
9+
* By using this software, you agree to the terms of the license.
10+
* Contributions, suggestions, and feedback are always welcome!
11+
*/
12+
13+
namespace Ripple\Net\WebSocket\Exception;
14+
15+
use RuntimeException;
16+
17+
class FrameException extends RuntimeException
18+
{
19+
}

0 commit comments

Comments
 (0)