Completed
Push — master ( 05544f...34252d )
by Maxime
02:24
created

WebSocketServer::setLoop()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
 * This file is a part of Woketo package.
5
 *
6
 * (c) Nekland <[email protected]>
7
 *
8
 * For the full license, take a look to the LICENSE file
9
 * on the root directory of this project
10
 */
11
12
namespace Nekland\Woketo\Server;
13
14
use Nekland\Woketo\Exception\ConfigException;
15
use Nekland\Woketo\Exception\RuntimeException;
16
use Nekland\Woketo\Message\MessageHandlerInterface;
17
use Nekland\Woketo\Rfc6455\FrameFactory;
18
use Nekland\Woketo\Rfc6455\MessageFactory;
19
use Nekland\Woketo\Rfc6455\MessageHandler\CloseFrameHandler;
20
use Nekland\Woketo\Rfc6455\MessageHandler\RsvCheckFrameHandler;
21
use Nekland\Woketo\Rfc6455\MessageHandler\WrongOpcodeHandler;
22
use Nekland\Woketo\Rfc6455\MessageHandler\PingFrameHandler;
23
use Nekland\Woketo\Rfc6455\MessageProcessor;
24
use Nekland\Woketo\Rfc6455\ServerHandshake;
25
use Nekland\Woketo\Utils\SimpleLogger;
26
use Psr\Log\LoggerInterface;
27
use Psr\Log\LogLevel;
28
use React\EventLoop\LoopInterface;
29
use React\Socket\ConnectionInterface;
30
use React\Socket\ServerInterface;
31
32
class WebSocketServer
33
{
34
    /**
35
     * @var int
36
     */
37
    private $port;
38
39
    /**
40
     * @var string
41
     */
42
    private $host;
43
44
    /**
45
     * @var ServerHandshake
46
     */
47
    private $handshake;
48
49
    /**
50
     * @var MessageHandlerInterface[]
51
     */
52
    private $messageHandlers;
53
54
    /**
55
     * @var array
56
     */
57
    private $connections;
58
59
    /**
60
     * @var LoopInterface
61
     */
62
    private $loop;
63
64
    /**
65
     * @var ServerInterface
66
     */
67
    private $server;
68
69
    /**
70
     * @var MessageProcessor
71
     */
72
    private $messageProcessor;
73
74
    /**
75
     * @var array
76
     */
77
    private $config;
78
79
    /**
80
     * @var LoggerInterface
81
     */
82
    private $logger;
83
84
    /**
85
     * @param int    $port    The number of the port to bind
86
     * @param string $host    The host to listen on (by default 127.0.0.1)
87
     * @param array  $config
88
     */
89 7
    public function __construct($port, $host = '127.0.0.1', $config = [])
90
    {
91 7
        $this->setConfig($config);
92 6
        $this->host = $host;
93 6
        $this->port = $port;
94 6
        $this->handshake = new ServerHandshake();
95 6
        $this->connections = [];
96 6
        $this->buildMessageProcessor();
97
98
        // Some optimization
99 5
        \gc_enable();       // As the process never stops, the garbage collector will be usefull, you may need to call it manually sometimes for performance purpose
100 5
        \set_time_limit(0); // It's by default on most server for cli apps but better be sure of that fact
101 5
    }
102
103
    /**
104
     * @param MessageHandlerInterface|string $messageHandler An instance of a class as string
105
     * @param string                         $uri            The URI you want to bind on
106
     */
107 3
    public function setMessageHandler($messageHandler, $uri = '*')
108
    {
109 3
        if (!$messageHandler instanceof MessageHandlerInterface &&  !\is_string($messageHandler)) {
110
            throw new \InvalidArgumentException('The message handler must be an instance of MessageHandlerInterface or a string.');
111
        }
112 3
        if (\is_string($messageHandler)) {
113
            try {
114
                $reflection = new \ReflectionClass($messageHandler);
115
                if(!$reflection->implementsInterface('Nekland\Woketo\Message\MessageHandlerInterface')) {
116
                    throw new \InvalidArgumentException('The messageHandler must implement MessageHandlerInterface');
117
                }
118
            } catch (\ReflectionException $e) {
119
                throw new \InvalidArgumentException('The messageHandler must be a string representing a class.');
120
            }
121
        }
122 3
        $this->messageHandlers[$uri] = $messageHandler;
123 3
    }
124
125
    /**
126
     * Launch the WebSocket server and an infinite loop that act on event.
127
     *
128
     * @throws \Exception
129
     */
130 3
    public function start()
131
    {
132 3
        if ($this->config['prod'] && \extension_loaded('xdebug')) {
133
            throw new \Exception('xdebug is enabled, it\'s a performance issue. Disable that extension or specify "prod" option to false.');
134
        }
135
136 3
        $this->loop = $this->loop ?? \React\EventLoop\Factory::create();
137 3
        $this->server = $this->server ?? new \React\Socket\Server($this->loop);
138
139 3
        if ($this->config['ssl']) {
140
            $this->server = new \React\Socket\SecureServer($this->server, $this->loop, array_merge([
0 ignored issues
show
Compatibility introduced by
$this->server of type object<React\Socket\ServerInterface> is not a sub-type of object<React\Socket\Server>. It seems like you assume a concrete implementation of the interface React\Socket\ServerInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
141
                'local_cert' => $this->config['certFile'],
142
                'passphrase' => $this->config['passphrase'],
143
            ], $this->config['sslContextOptions']));
144
            $this->getLogger()->info('Enabled ssl');
145
        }
146
147
        $this->server->on('connection', function ($socketStream) {
148 3
            $this->onNewConnection($socketStream);
149 3
        });
150 3
        $this->server->listen($this->port);
151
152 3
        $this->getLogger()->info('Listening on ' . $this->host . ':' . $this->port);
153
154 3
        $this->loop->run();
155 3
    }
156
157
    /**
158
     * @param ConnectionInterface $socketStream
159
     */
160
    private function onNewConnection(ConnectionInterface $socketStream)
161
    {
162 3
        $connection = new Connection($socketStream, function ($uri, Connection $connection) {
163 3
            return $this->getMessageHandler($uri, $connection);
164 3
        }, $this->loop, $this->messageProcessor);
165
166 3
        $connection->setLogger($this->getLogger());
167 3
        $this->connections[] = $connection;
168 3
    }
169
170
    /**
171
     * @param string $uri
172
     * @param Connection $connection
173
     * @return MessageHandlerInterface|null
174
     */
175 3
    private function getMessageHandler(string $uri, Connection $connection)
0 ignored issues
show
Unused Code introduced by
The parameter $connection is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
176
    {
177 3
        $handler = null;
178
179 3
        if (!empty($this->messageHandlers[$uri])) {
180 2
            $handler = $this->messageHandlers[$uri];
181
        }
182
183 3
        if (null === $handler && !empty($this->messageHandlers['*'])) {
184
            $handler = $this->messageHandlers['*'];
185
        }
186
187 3
        if (null !== $handler) {
188 2
            if (\is_string($handler)) {
189
                $handler = new $handler;
190
            }
191
192 2
            return $handler;
193
        }
194
195 1
        return null;
196
    }
197
198
    /**
199
     * Build the message processor with configuration
200
     */
201 6
    private function buildMessageProcessor()
202
    {
203 6
        $this->messageProcessor = new MessageProcessor(
204 6
            new FrameFactory($this->config['frame']),
205 6
            new MessageFactory($this->config['message'])
206
        );
207 6
        $this->messageProcessor->addHandler(new PingFrameHandler());
208 6
        $this->messageProcessor->addHandler(new CloseFrameHandler());
209 6
        $this->messageProcessor->addHandler(new WrongOpcodeHandler());
210 6
        $this->messageProcessor->addHandler(new RsvCheckFrameHandler());
211
212 6
        foreach ($this->config['messageHandlers'] as $handler) {
213 1
            if (!$handler instanceof MessageHandlerInterface) {
214 1
                throw new RuntimeException(sprintf('%s is not an instance of MessageHandlerInterface but must be !', get_class($handler)));
215
            }
216
        }
217 5
    }
218
219
    /**
220
     * Sets the configuration
221
     *
222
     * @param array $config
223
     * @throws ConfigException
224
     */
225 7
    private function setConfig(array $config)
226
    {
227 7
        $this->config = \array_merge([
228 7
            'frame' => [],
229
            'message' => [],
230
            'messageHandlers' => [],
231
            'prod' => true,
232
            'ssl' => false,
233
            'certFile' => '',
234
            'passphrase' => '',
235
            'sslContextOptions' => [],
236
        ], $config);
237
238 7
        if ($this->config['ssl'] && !is_file($this->config['certFile'])) {
239 1
            throw new ConfigException('With ssl configuration, you need to specify a certificate file.');
240
        }
241 6
    }
242
243
    /**
244
     * @return SimpleLogger|LoggerInterface
245
     */
246 3
    public function getLogger()
247
    {
248 3
        if (null === $this->logger) {
249 3
            return $this->logger = new SimpleLogger(!$this->config['prod']);
250
        }
251
252 3
        return $this->logger;
253
    }
254
255
    /**
256
     * Allows you to set a custom logger
257
     *
258
     * @param LoggerInterface $logger
259
     * @return WebSocketServer
260
     */
261
    public function setLogger(LoggerInterface $logger)
262
    {
263
        $this->logger = $logger;
264
265
        return $this;
266
    }
267
268
    /**
269
     * Allows to specify a loop that will be used instead of the reactphp generated loop.
270
     *
271
     * @param LoopInterface $loop
272
     * @return WebSocketServer
273
     */
274 3
    public function setLoop(LoopInterface $loop)
275
    {
276 3
        $this->loop = $loop;
277
278 3
        return $this;
279
    }
280
281
    /**
282
     * @param ServerInterface $server
283
     * @return WebSocketServer
284
     */
285 3
    public function setSocketServer(ServerInterface $server)
286
    {
287 3
        $this->server = $server;
288
289 3
        return $this;
290
    }
291
}
292