Skip to content

Commit 6ce121f

Browse files
mbonneaudavidwdan
authored andcommitted
Add keepalive and some minor fixes with disposables (#14)
* Add keepalive and some minor fixes with disposables * Shorter callables * Correct ping to actually send the frame instead of nothing * Remove old method that should have been removed a long time ago * Mask ping if client
1 parent 06680bb commit 6ce121f

11 files changed

+305
-70
lines changed

composer.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@
2929
},
3030
"autoload-dev": {
3131
"psr-4": {
32-
"Rx\\Websocket\\Test\\": "tests/"
33-
}
32+
"Rx\\Websocket\\Test\\": "test/",
33+
"Rx\\": "vendor/reactivex/rxphp/test/Rx"
34+
},
35+
"files": [
36+
"vendor/reactivex/rxphp/test/helper-functions.php"
37+
]
3438
},
3539
"require": {
3640
"react/http": "^0.7.3",

src/Client.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ class Client extends Observable
2525
private $subProtocols;
2626
private $loop;
2727
private $connector;
28+
private $keepAlive;
2829

29-
public function __construct(string $url, bool $useMessageObject = false, array $subProtocols = [], LoopInterface $loop = null, ConnectorInterface $connector = null)
30+
public function __construct(string $url, bool $useMessageObject = false, array $subProtocols = [], LoopInterface $loop = null, ConnectorInterface $connector = null, int $keepAlive = 60000)
3031
{
3132
$parsedUrl = parse_url($url);
3233
if (!isset($parsedUrl['scheme']) || !in_array($parsedUrl['scheme'], ['wss', 'ws'])) {
@@ -46,6 +47,7 @@ public function __construct(string $url, bool $useMessageObject = false, array $
4647
$this->subProtocols = $subProtocols;
4748
$this->loop = $loop ?: \EventLoop\getLoop();
4849
$this->connector = $connector;
50+
$this->keepAlive = $keepAlive;
4951
}
5052

5153
public function _subscribe(ObserverInterface $clientObserver): DisposableInterface
@@ -137,7 +139,8 @@ function () use ($request) {
137139
$this->useMessageObject,
138140
$subprotoHeader,
139141
$nRequest,
140-
$psr7Response
142+
$psr7Response,
143+
$this->keepAlive
141144
));
142145
});
143146

src/MessageSubject.php

+54-22
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
use Ratchet\RFC6455\Messaging\Message;
1111
use Ratchet\RFC6455\Messaging\MessageBuffer;
1212
use Ratchet\RFC6455\Messaging\MessageInterface;
13+
use Rx\Disposable\CallbackDisposable;
14+
use Rx\Disposable\CompositeDisposable;
15+
use Rx\DisposableInterface;
16+
use Rx\Exception\TimeoutException;
1317
use Rx\Observable;
1418
use Rx\ObserverInterface;
1519
use Rx\Subject\Subject;
@@ -32,11 +36,12 @@ public function __construct(
3236
bool $useMessageObject = false,
3337
$subProtocol = "",
3438
RequestInterface $request,
35-
ResponseInterface $response
39+
ResponseInterface $response,
40+
int $keepAlive = 60000
3641
) {
3742
$this->request = $request;
3843
$this->response = $response;
39-
$this->rawDataIn = $rawDataIn;
44+
$this->rawDataIn = $rawDataIn->share();
4045
$this->rawDataOut = $rawDataOut;
4146
$this->mask = $mask;
4247
$this->subProtocol = $subProtocol;
@@ -46,7 +51,7 @@ public function __construct(
4651
function (MessageInterface $msg) use ($useMessageObject) {
4752
parent::onNext($useMessageObject ? $msg : $msg->getPayload());
4853
},
49-
function (FrameInterface $frame) use ($rawDataOut) {
54+
function (FrameInterface $frame) {
5055
switch ($frame->getOpcode()) {
5156
case Frame::OP_PING:
5257
$this->sendFrame(new Frame($frame->getPayload(), true, Frame::OP_PONG));
@@ -63,31 +68,63 @@ function (FrameInterface $frame) use ($rawDataOut) {
6368
parent::onError($exception);
6469
}
6570

66-
// complete output stream
67-
$rawDataOut->onCompleted();
71+
$this->rawDataOut->onCompleted();
6872

69-
// signal subscribers that we are done here
70-
//parent::onCompleted();
73+
parent::onCompleted();
74+
75+
$this->rawDataDisp->dispose();
7176
return;
7277
}
7378
},
7479
!$this->mask
7580
);
7681

77-
$this->rawDataDisp = $this->rawDataIn->subscribe(
78-
function ($data) use ($messageBuffer) {
79-
$messageBuffer->onData($data);
80-
},
81-
function (\Exception $exception) {
82-
parent::onError($exception);
83-
},
84-
function () {
85-
parent::onCompleted();
86-
});
82+
// keepAlive
83+
$keepAliveObs = Observable::empty();
84+
if ($keepAlive > 0) {
85+
$keepAliveObs = $this->rawDataIn
86+
->startWith(0)
87+
->throttle($keepAlive / 2)
88+
->map(function () use ($keepAlive, $rawDataOut) {
89+
return Observable::timer($keepAlive)
90+
->do(function () use ($rawDataOut) {
91+
$frame = new Frame('', true, Frame::OP_PING);
92+
if ($this->mask) {
93+
$frame->maskPayload();
94+
}
95+
$rawDataOut->onNext($frame->getContents());
96+
})
97+
->delay($keepAlive)
98+
->do(function () use ($rawDataOut) {
99+
$rawDataOut->onError(new TimeoutException());
100+
});
101+
})
102+
->switch()
103+
->flatMapTo(Observable::never());
104+
}
105+
106+
$this->rawDataDisp = $this->rawDataIn
107+
->merge($keepAliveObs)
108+
->subscribe(
109+
[$messageBuffer, 'onData'],
110+
[$this, 'parent::onError'],
111+
[$this, 'parent::onCompleted']
112+
);
87113

88114
$this->subProtocol = $subProtocol;
89115
}
90116

117+
protected function _subscribe(ObserverInterface $observer): DisposableInterface
118+
{
119+
$disposable = new CompositeDisposable([
120+
parent::_subscribe($observer),
121+
$this->rawDataDisp,
122+
new CallbackDisposable([$this->rawDataOut, 'onCompleted'])
123+
]);
124+
125+
return $disposable;
126+
}
127+
91128
private function createCloseFrame(int $closeCode = Frame::CLOSE_NORMAL): Frame
92129
{
93130
$frame = new Frame(pack('n', $closeCode), true, Frame::OP_CLOSE);
@@ -112,11 +149,6 @@ public function sendFrame(Frame $frame)
112149
$this->rawDataOut->onNext($frame->getContents());
113150
}
114151

115-
public function getControlFrames(): Observable
116-
{
117-
return $this->controlFrames;
118-
}
119-
120152
// The ObserverInterface is commandeered by this class. We will use the parent:: stuff ourselves for notifying
121153
// subscribers
122154
public function onNext($value)

src/Server.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ class Server extends Observable
2727
private $useMessageObject;
2828
private $subProtocols;
2929
private $loop;
30+
private $keepAlive;
3031

31-
public function __construct(string $bindAddressOrPort, bool $useMessageObject = false, array $subProtocols = [], LoopInterface $loop = null)
32+
public function __construct(string $bindAddressOrPort, bool $useMessageObject = false, array $subProtocols = [], LoopInterface $loop = null, int $keepAlive = 60000)
3233
{
3334
$this->bindAddress = $bindAddressOrPort;
3435
$this->useMessageObject = $useMessageObject;
3536
$this->subProtocols = $subProtocols;
3637
$this->loop = $loop ?: \EventLoop\getLoop();
38+
$this->keepAlive = $keepAlive;
3739
}
3840

3941
public function _subscribe(ObserverInterface $observer): DisposableInterface
@@ -124,7 +126,8 @@ function () use ($responseStream) {
124126
$this->useMessageObject,
125127
$subProtocol,
126128
$psrRequest,
127-
$negotiatorResponse
129+
$negotiatorResponse,
130+
$this->keepAlive
128131
);
129132

130133
$observer->onNext($messageSubject);

test/ClientTest.php

-35
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
namespace Rx\Websocket\Test;
44

55
use React\EventLoop\Factory;
6-
use Rx\Websocket\Client;
7-
use Rx\Websocket\MessageSubject;
8-
use Rx\Websocket\Server;
96

107
class ClientTest extends \PHPUnit_Framework_TestCase
118
{
@@ -29,36 +26,4 @@ function ($err) use (&$errored) {
2926

3027
$this->assertTrue($errored);
3128
}
32-
33-
public function testRequestEndOnDispose()
34-
{
35-
$this->markTestSkipped();
36-
$loop = Factory::create();
37-
38-
$server = new Server('tcp://127.0.0.1:1234', false, [], $loop);
39-
$serverDisp = $server->subscribe(function (MessageSubject $ms) {
40-
$ms->map('strrev')->subscribe($ms);
41-
});
42-
43-
$value = null;
44-
45-
$client = new Client('ws://127.0.0.1:1234/', false, [], $loop);
46-
$client
47-
->subscribe(function (MessageSubject $ms) use ($serverDisp) {
48-
$ms->onNext('Hello');
49-
$ms
50-
->finally(function () use ($serverDisp) {
51-
$serverDisp->dispose();
52-
})
53-
->take(1)
54-
->subscribe(function ($x) use (&$value) {
55-
$this->assertNull($value);
56-
$value = $x;
57-
});
58-
});
59-
60-
$loop->run();
61-
62-
$this->assertEquals('olleH', $value);
63-
}
6429
}

test/MessageSubjectTest.php

+134-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
use GuzzleHttp\Psr7\Request;
66
use GuzzleHttp\Psr7\Response;
77
use Ratchet\RFC6455\Messaging\Frame;
8+
use Rx\Exception\TimeoutException;
89
use Rx\Observer\CallbackObserver;
910
use Rx\Subject\Subject;
11+
use Rx\Testing\MockObserver;
1012
use Rx\Websocket\MessageSubject;
1113
use Rx\Websocket\WebsocketErrorException;
1214

13-
class MessageSubjectTest extends \PHPUnit_Framework_TestCase
15+
class MessageSubjectTest extends TestCase
1416
{
1517
public function testCloseCodeSentToOnError()
1618
{
@@ -50,4 +52,135 @@ function () use (&$closeCode) {
5052

5153
$this->assertEquals(4000, $closeCode);
5254
}
55+
56+
public function testPingPongTimeout()
57+
{
58+
$dataIn = $this->createHotObservable([
59+
onNext(200, (new Frame('', true, Frame::OP_TEXT))->getContents()),
60+
onNext(205, (new Frame('', true, Frame::OP_TEXT))->getContents()),
61+
]);
62+
63+
$dataOut = new Subject();
64+
65+
$ms = new MessageSubject(
66+
$dataIn,
67+
$dataOut,
68+
true,
69+
false,
70+
'',
71+
new Request('GET', '/ws'),
72+
new Response(),
73+
300
74+
);
75+
76+
$result = $this->scheduler->startWithCreate(function () use ($dataOut) {
77+
return $dataOut;
78+
});
79+
80+
$this->assertMessages([
81+
onNext(650, (new Frame('', true, Frame::OP_PING))->getContents()),
82+
onError(950, new TimeoutException())
83+
], $result->getMessages());
84+
}
85+
86+
public function testPingPong()
87+
{
88+
$dataIn = $this->createHotObservable([
89+
onNext(200, (new Frame('', true, Frame::OP_TEXT))->getContents()),
90+
onNext(205, (new Frame('', true, Frame::OP_TEXT))->getContents()),
91+
onNext(651, (new Frame('', true, Frame::OP_PONG))->getContents())
92+
]);
93+
94+
$dataOut = new Subject();
95+
96+
$ms = new MessageSubject(
97+
$dataIn,
98+
$dataOut,
99+
true,
100+
false,
101+
'',
102+
new Request('GET', '/ws'),
103+
new Response(),
104+
300
105+
);
106+
107+
$result = $this->scheduler->startWithDispose(function () use ($dataOut) {
108+
return $dataOut;
109+
}, 2000);
110+
111+
$this->assertMessages([
112+
onNext(650, (new Frame('', true, Frame::OP_PING))->getContents()),
113+
onNext(951, (new Frame('', true, Frame::OP_PING))->getContents()),
114+
onError(1251, new TimeoutException())
115+
], $result->getMessages());
116+
}
117+
118+
public function testPingPongDataSuppressesPing()
119+
{
120+
$dataIn = $this->createHotObservable([
121+
onNext(201, (new Frame('', true, Frame::OP_TEXT))->getContents()),
122+
onNext(205, (new Frame('', true, Frame::OP_TEXT))->getContents()),
123+
onNext(649, (new Frame('', true, Frame::OP_TEXT))->getContents())
124+
]);
125+
126+
$dataOut = new Subject();
127+
128+
$ms = new MessageSubject(
129+
$dataIn,
130+
$dataOut,
131+
true,
132+
false,
133+
'',
134+
new Request('GET', '/ws'),
135+
new Response(),
136+
300
137+
);
138+
139+
$result = $this->scheduler->startWithDispose(function () use ($dataOut) {
140+
return $dataOut;
141+
}, 2000);
142+
143+
$this->assertMessages([
144+
onNext(949, (new Frame('', true, Frame::OP_PING))->getContents()),
145+
onError(1249, new TimeoutException())
146+
], $result->getMessages());
147+
}
148+
149+
public function testDisposeOnMessageSubjectClosesConnection()
150+
{
151+
$dataIn = $this->createHotObservable([
152+
onNext(201, (new Frame('', true, Frame::OP_TEXT))->getContents()),
153+
onNext(205, (new Frame('', true, Frame::OP_TEXT))->getContents()),
154+
]);
155+
156+
$dataOut = new MockObserver($this->scheduler);
157+
158+
$ms = new MessageSubject(
159+
$dataIn,
160+
$dataOut,
161+
true,
162+
false,
163+
'',
164+
new Request('GET', '/ws'),
165+
new Response(),
166+
300
167+
);
168+
169+
$result = $this->scheduler->startWithDispose(function () use ($ms) {
170+
return $ms;
171+
}, 300);
172+
173+
$this->assertMessages([
174+
onNext(201, ''),
175+
onNext(205, ''),
176+
], $result->getMessages());
177+
178+
$this->assertSubscriptions([
179+
subscribe(0,300)
180+
], $dataIn->getSubscriptions());
181+
182+
$this->assertMessages([
183+
onCompleted(300)
184+
], $dataOut->getMessages());
185+
}
53186
}

0 commit comments

Comments
 (0)