Skip to content

Commit 2a4862d

Browse files
committed
refactor(http): move Request and Response classes from Server subdirectory to Http namespace
1 parent 439437b commit 2a4862d

12 files changed

Lines changed: 195 additions & 32 deletions

File tree

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
],
2626
"require": {
2727
"php": ">=8.1",
28-
"ext-ffi": "*",
2928
"ext-posix": "*",
3029
"ext-pcntl": "*",
3130
"ext-sockets": "*",

examples/11-http-sse.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>SSE Demo</title>
6+
</head>
7+
<body>
8+
<h1>Server-Sent Events Demo</h1>
9+
<p><strong>Connection Status:</strong> <span id="status">Connecting...</span></p>
10+
<hr>
11+
<h2>Received Events:</h2>
12+
<div id="messages"></div>
13+
14+
<script>
15+
const eventSource = new EventSource('/events');
16+
const messagesDiv = document.getElementById('messages');
17+
const statusSpan = document.getElementById('status');
18+
19+
eventSource.onopen = function() {
20+
statusSpan.textContent = 'Connected';
21+
};
22+
23+
eventSource.onmessage = function(event) {
24+
const data = JSON.parse(event.data);
25+
const p = document.createElement('p');
26+
p.textContent = `[${data.time}] ${data.message} (ID: ${data.id})`;
27+
messagesDiv.appendChild(p);
28+
};
29+
30+
eventSource.onerror = function() {
31+
statusSpan.textContent = 'Connection closed';
32+
const p = document.createElement('p');
33+
p.textContent = 'Connection closed or error occurred';
34+
messagesDiv.appendChild(p);
35+
eventSource.close();
36+
};
37+
</script>
38+
</body>
39+
</html>

examples/11-http-sse.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types=1);
2+
3+
require __DIR__ . '/../vendor/autoload.php';
4+
5+
use Ripple\Net\Http;
6+
use Ripple\Net\Http\Request;
7+
use Ripple\Runtime;
8+
use Ripple\Stream\Exception\ConnectionException;
9+
10+
function main(): int
11+
{
12+
$server = Http::server('http://0.0.0.0:8000');
13+
14+
$server->onRequest = function (Request $request) {
15+
$uri = $request->SERVER['REQUEST_URI'];
16+
17+
if ($uri === '/events') {
18+
\respondSSE($request);
19+
return;
20+
}
21+
22+
if ($uri === '/') {
23+
$htmlPath = __DIR__ . '/11-http-sse.html';
24+
if (!\is_file($htmlPath)) {
25+
$request->respond('HTML template not found', ['Content-Type' => 'text/plain'], 404);
26+
return;
27+
}
28+
29+
$html = \file_get_contents($htmlPath);
30+
$request->respond($html, ['Content-Type' => 'text/html; charset=utf-8']);
31+
return;
32+
}
33+
34+
$request->respond('Not Found', ['Content-Type' => 'text/plain'], 404);
35+
};
36+
37+
echo "SSE server started: http://127.0.0.1:8000\n";
38+
$server->listen();
39+
40+
return 0;
41+
}
42+
43+
/**
44+
* @param Request $request
45+
* @return void
46+
* @throws ConnectionException
47+
*/
48+
function respondSSE(Request $request): void
49+
{
50+
$request->response()
51+
->withHeader('Content-Type', 'text/event-stream')
52+
->withHeader('Cache-Control', 'no-cache')
53+
->withHeader('Connection', 'keep-alive')
54+
->withHeader('X-Accel-Buffering', 'no')
55+
->withBody(function () {
56+
$eventId = 0;
57+
58+
for ($i = 1; $i <= 10; $i++) {
59+
$eventId++;
60+
$time = \date('H:i:s');
61+
$data = \json_encode([
62+
'id' => $eventId,
63+
'time' => $time,
64+
'message' => "Event #$i"
65+
]);
66+
67+
yield "id: $eventId\n";
68+
yield "data: $data\n";
69+
yield "\n";
70+
71+
Co\sleep(1);
72+
}
73+
})
74+
->closeAfter()
75+
->send();
76+
}
77+
78+
Runtime::run(static fn () => \main());

src/Net/Http/Client.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
namespace Ripple\Net\Http;
1414

1515
use Ripple\Net\Http\Exception\TimeoutException;
16-
use Ripple\Net\Http\Server\Request;
17-
use Ripple\Net\Http\Server\Response;
1816
use Ripple\Net\Http\Trait\ClientRequest;
1917
use Ripple\Runtime\Scheduler;
2018
use Ripple\Stream;
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
* Contributions, suggestions, and feedback are always welcome!
1111
*/
1212

13-
namespace Ripple\Net\Http\Server;
13+
namespace Ripple\Net\Http;
1414

15+
use Ripple\Net\Http\Server\Connection;
1516
use Ripple\Net\Http\Trait\ClientRequest;
1617
use Ripple\Stream\Exception\ConnectionException;
1718

@@ -78,7 +79,7 @@ public function respond(mixed $content = null, array $withHeaders = [], int $sta
7879
$response->withHeader($name, $value);
7980
}
8081

81-
$response($this->conn->stream);
82+
$response->send();
8283
}
8384

8485
/**
@@ -139,7 +140,7 @@ public function respondHtml(string $content, array $withHeaders = [], int $statu
139140
public function response(): Response
140141
{
141142
if (!isset($this->response)) {
142-
$this->response = new Response();
143+
$this->response = (new Response())->withStream($this->conn->stream);
143144
}
144145

145146
return $this->response;
Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* Contributions, suggestions, and feedback are always welcome!
1111
*/
1212

13-
namespace Ripple\Net\Http\Server;
13+
namespace Ripple\Net\Http;
1414

1515
use Closure;
1616
use Generator;
@@ -31,6 +31,7 @@
3131
use function is_int;
3232
use function is_file;
3333
use function strtolower;
34+
use function is_callable;
3435

3536
/**
3637
* response entity
@@ -39,7 +40,7 @@ class Response
3940
{
4041
use ClientResponse;
4142

42-
/*** @var mixed|Stream */
43+
/*** @var mixed|Stream|Generator|string */
4344
protected mixed $body;
4445

4546
/*** @var array */
@@ -54,18 +55,34 @@ class Response
5455
/*** @var string */
5556
protected string $statusText = 'OK';
5657

58+
/*** @var bool 响应体发送完成后是否关闭连接 */
59+
protected bool $closeAfterBody = false;
60+
61+
/**
62+
* @var Stream|null
63+
*/
64+
protected ?Stream $stream = null;
65+
5766
/**
5867
*/
5968
public function __construct()
6069
{
6170
}
6271

6372
/**
64-
* @param Stream $stream
6573
* @return void
6674
* @throws ConnectionException
6775
*/
68-
public function __invoke(Stream $stream): void
76+
public function __invoke(): void
77+
{
78+
$this->send();
79+
}
80+
81+
/**
82+
* @return void
83+
* @throws ConnectionException
84+
*/
85+
public function send(): void
6986
{
7087
// respond header
7188
$header = "HTTP/1.1 {$this->statusCode()} {$this->statusText}\r\n";
@@ -82,14 +99,19 @@ public function __invoke(Stream $stream): void
8299
$header .= 'Set-Cookie: ' . $cookieLine . "\r\n";
83100
}
84101

102+
// respond body
103+
if (is_callable($this->body)) {
104+
$this->body = ($this->body)($this->stream);
105+
}
106+
85107
if (is_string($this->body)) {
86-
$stream->writeAll("{$header}\r\n{$this->body}");
108+
$this->stream->writeAll("{$header}\r\n{$this->body}");
87109
} else {
88-
$stream->writeAll("{$header}\r\n");
110+
$this->stream->writeAll("{$header}\r\n");
89111

90112
// 普通文本
91113
if (is_string($this->body)) {
92-
$stream->writeAll($this->body);
114+
$this->stream->writeAll($this->body);
93115
}
94116

95117
// 可控流式传输方式
@@ -98,41 +120,46 @@ public function __invoke(Stream $stream): void
98120
if (!is_string($content) || $content === '') {
99121
continue;
100122
}
101-
$stream->writeAll($content);
123+
124+
try {
125+
$this->stream->writeAll($content);
126+
} catch (Throwable) {
127+
break;
128+
}
102129
}
103130
}
104131

105132
// 固定流传输方式
106133
elseif ($this->body instanceof Stream) {
107134
try {
108135
$owner = \Co\current();
109-
$stream->setWriteBufferMax(10485760);
110-
$stream->watchWrite(function () use ($owner, $stream) {
136+
$this->stream->setWriteBufferMax(10485760);
137+
$this->stream->watchWrite(function () use ($owner) {
111138
try {
112-
$bufferLen = $stream->writeBuffer()->length();
139+
$bufferLen = $this->stream->writeBuffer()->length();
113140

114141
// 阈值检查
115142
if ($bufferLen > 10405760) {
116-
$stream->flushOnce();
143+
$this->stream->flushOnce();
117144
return;
118145
}
119146

120147
// 优先处理body数据
121148
if (!$this->body->isClosed()) {
122149
$buf = $this->body->read(8192);
123150
if ($buf) {
124-
$stream->writeAsync($buf);
151+
$this->stream->writeAsync($buf);
125152
}
126153

127154
if ($this->body->eof()) {
128155
$this->body->close();
129156
}
130157
}
131158

132-
$stream->flushOnce();
159+
$this->stream->flushOnce();
133160

134161
// 文件末尾 && 缓冲区空
135-
if ($this->body->eof() && $stream->writeBuffer()->length() === 0) {
162+
if ($this->body->eof() && $this->stream->writeBuffer()->length() === 0) {
136163
Scheduler::tryResume($owner);
137164
}
138165
} catch (Throwable) {
@@ -147,7 +174,7 @@ public function __invoke(Stream $stream): void
147174
throw new ConnectionException($exception->getMessage(), $exception->getCode(), $exception);
148175
} finally {
149176
$this->body->close();
150-
$stream->unwatchWrite();
177+
$this->stream->unwatchWrite();
151178
}
152179
} else {
153180
throw new ConnectionException('The response content is illegal.');
@@ -157,8 +184,8 @@ public function __invoke(Stream $stream): void
157184
$connectionHeader = $this->headers['Connection'] ?? '';
158185
$connectionValue = strtolower($connectionHeader);
159186

160-
if ($connectionValue === 'close') {
161-
$stream->close();
187+
if ($connectionValue === 'close' || $this->closeAfterBody) {
188+
$this->stream->close();
162189
}
163190
}
164191

@@ -272,6 +299,16 @@ public function withBody(mixed $content): static
272299
return $this;
273300
}
274301

302+
/**
303+
* @param Stream $stream
304+
* @return $this
305+
*/
306+
public function withStream(Stream $stream): static
307+
{
308+
$this->stream = $stream;
309+
return $this;
310+
}
311+
275312
/**
276313
* @param string $name
277314
* @return $this
@@ -282,6 +319,16 @@ public function removeHeader(string $name): static
282319
return $this;
283320
}
284321

322+
/**
323+
* 响应体发送完成后关闭连接
324+
* @return $this
325+
*/
326+
public function closeAfter(): static
327+
{
328+
$this->closeAfterBody = true;
329+
return $this;
330+
}
331+
285332
/**
286333
* 获取响应体内容
287334
* @return mixed

src/Net/Http/Server/Connection.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace Ripple\Net\Http\Server;
1414

15+
use Ripple\Net\Http\Request;
1516
use Ripple\Runtime\Scheduler;
1617
use RuntimeException;
1718
use Ripple\Net\Http\Enum\Method;
@@ -188,9 +189,9 @@ public function start(Server $server): void
188189
foreach ($this->processData($content) as $reqInfo) {
189190
$this->onRequest($reqInfo);
190191
}
191-
} catch (Throwable $err) {
192-
if (!$err instanceof ConnectionException) {
193-
Stdin::println($err->getMessage());
192+
} catch (Throwable $e) {
193+
if (!$e instanceof ConnectionException) {
194+
Stdin::println($e->getMessage());
194195
}
195196

196197
$this->stream->close();

src/Net/Http/Trait/ClientRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace Ripple\Net\Http\Trait;
1414

1515
use Ripple\Coroutine;
16-
use Ripple\Net\Http\Server\Response;
16+
use Ripple\Net\Http\Response;
1717
use Ripple\Runtime\Scheduler;
1818
use Ripple\Stream;
1919
use Ripple\Stream\Exception\ConnectionException;

src/Net/WebSocket/Server/Connection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace Ripple\Net\WebSocket\Server;
1414

1515
use Closure;
16-
use Ripple\Net\Http\Server\Request;
16+
use Ripple\Net\Http\Request;
1717
use Ripple\Net\WebSocket\Enum\Opcode;
1818
use Ripple\Net\WebSocket\Frame;
1919
use Ripple\Stream;

0 commit comments

Comments
 (0)