2020-05-19 11:26:54 +08:00
|
|
|
|
Socket.io是一款非常流行的應用層實時通訊協議和框架,可以輕鬆實現應答、分組、廣播。hyperf/socketio-server支持了Socket.io的WebSocket傳輸協議。
|
|
|
|
|
|
|
|
|
|
## 安裝
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
composer require hyperf/socketio-server
|
|
|
|
|
```
|
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
hyperf/socketio-server 組件是基於 WebSocket 實現的,請確保服務端已經添加了 `WebSocket 服務` 的配置。
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
|
|
|
|
```php
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// config/autoload/server.php
|
|
|
|
|
[
|
|
|
|
|
'name' => 'socket-io',
|
|
|
|
|
'type' => Server::SERVER_WEBSOCKET,
|
|
|
|
|
'host' => '0.0.0.0',
|
|
|
|
|
'port' => 9502,
|
|
|
|
|
'sock_type' => SWOOLE_SOCK_TCP,
|
|
|
|
|
'callbacks' => [
|
|
|
|
|
SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
|
|
|
|
|
SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
|
|
|
|
|
SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
|
|
|
|
|
],
|
|
|
|
|
],
|
2020-05-19 11:26:54 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 快速開始
|
|
|
|
|
|
|
|
|
|
### 服務端
|
2020-06-21 21:36:55 +08:00
|
|
|
|
|
2020-05-19 11:26:54 +08:00
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Controller;
|
|
|
|
|
|
|
|
|
|
use Hyperf\SocketIOServer\Annotation\Event;
|
|
|
|
|
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
|
|
|
|
|
use Hyperf\SocketIOServer\BaseNamespace;
|
|
|
|
|
use Hyperf\SocketIOServer\Socket;
|
|
|
|
|
use Hyperf\Utils\Codec\Json;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @SocketIONamespace("/")
|
|
|
|
|
*/
|
|
|
|
|
class WebSocketController extends BaseNamespace
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @Event("event")
|
|
|
|
|
* @param string $data
|
|
|
|
|
*/
|
|
|
|
|
public function onEvent(Socket $socket, $data)
|
|
|
|
|
{
|
|
|
|
|
// 應答
|
|
|
|
|
return 'Event Received: ' . $data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @Event("join-room")
|
|
|
|
|
* @param string $data
|
|
|
|
|
*/
|
|
|
|
|
public function onJoinRoom(Socket $socket, $data)
|
|
|
|
|
{
|
|
|
|
|
// 將當前用户加入房間
|
|
|
|
|
$socket->join($data);
|
|
|
|
|
// 向房間內其他用户推送(不含當前用户)
|
|
|
|
|
$socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
|
|
|
|
|
// 向房間內所有人廣播(含當前用户)
|
|
|
|
|
$this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @Event("say")
|
|
|
|
|
* @param string $data
|
|
|
|
|
*/
|
|
|
|
|
public function onSay(Socket $socket, $data)
|
|
|
|
|
{
|
|
|
|
|
$data = Json::decode($data);
|
|
|
|
|
$socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> 每個 socket 會自動加入以自己 `sid` 命名的房間(`$socket->getSid()`),發送私聊信息就推送到對應 `sid` 即可。
|
|
|
|
|
|
|
|
|
|
> 框架會自動觸發 `connect` 和 `disconnect` 兩個事件。
|
|
|
|
|
|
|
|
|
|
### 客户端
|
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
由於服務端只實現了 WebSocket 通訊,所以客户端要加上 `{transports:["websocket"]}` 。
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
|
|
|
|
|
<script>
|
|
|
|
|
var socket = io('ws://127.0.0.1:9502', { transports: ["websocket"] });
|
|
|
|
|
socket.on('connect', data => {
|
|
|
|
|
socket.emit('event', 'hello, hyperf', console.log);
|
|
|
|
|
socket.emit('join-room', 'room1', console.log);
|
|
|
|
|
setInterval(function () {
|
|
|
|
|
socket.emit('say', '{"room":"room1", "message":"Hello Hyperf."}');
|
|
|
|
|
}, 1000);
|
|
|
|
|
});
|
|
|
|
|
socket.on('event', console.log);
|
|
|
|
|
</script>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## API 清單
|
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
### Socket API
|
|
|
|
|
|
|
|
|
|
通過 SocketAPI 對目標 Socket 進行推送,或以目標 Socket 的身份在房間內發言。需要在事件回調中使用。
|
|
|
|
|
|
2020-05-19 11:26:54 +08:00
|
|
|
|
```php
|
|
|
|
|
<?php
|
2020-06-21 21:36:55 +08:00
|
|
|
|
/**
|
|
|
|
|
* @Event("SomeEvent")
|
|
|
|
|
*/
|
|
|
|
|
function onSomeEvent(\Hyperf\SocketIOServer\Socket $socket){
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
|
|
|
|
// sending to the client
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// 向連接推送 hello 事件
|
2020-05-19 11:26:54 +08:00
|
|
|
|
$socket->emit('hello', 'can you hear me?', 1, 2, 'abc');
|
|
|
|
|
|
|
|
|
|
// sending to all clients except sender
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// 向所有連接推送 broadcast 事件,但是不包括當前連接。
|
2020-05-19 11:26:54 +08:00
|
|
|
|
$socket->broadcast->emit('broadcast', 'hello friends!');
|
|
|
|
|
|
|
|
|
|
// sending to all clients in 'game' room except sender
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// 向 game 房間內所有連接推送 nice game 事件,但是不包括當前連接。
|
2020-05-19 11:26:54 +08:00
|
|
|
|
$socket->to('game')->emit('nice game', "let's play a game");
|
|
|
|
|
|
|
|
|
|
// sending to all clients in 'game1' and/or in 'game2' room, except sender
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// 向 game1 房間 和 game2 房間內所有連接取並集推送 nice game 事件,但是不包括當前連接。
|
2020-05-19 11:26:54 +08:00
|
|
|
|
$socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
|
|
|
|
|
|
|
|
|
|
// WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
|
|
|
|
|
// named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// 注意:自己給自己推送的時候不要加to,因為$socket->to()總是排除自己。直接$socket->emit()就好了。
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
|
|
|
|
// sending with acknowledgement
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// 發送信息,並且等待並接收客户端響應。
|
2020-05-19 11:26:54 +08:00
|
|
|
|
$reply = $socket->emit('question', 'do you think so?')->reply();
|
|
|
|
|
|
|
|
|
|
// sending without compression
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// 無壓縮推送
|
2020-05-19 11:26:54 +08:00
|
|
|
|
$socket->compress(false)->emit('uncompressed', "that's rough");
|
2020-06-21 21:36:55 +08:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
### 全局API
|
|
|
|
|
|
|
|
|
|
直接從容器中獲取SocketIO單例。這個單例可向全局廣播或指定房間、個人通訊。未指定命名空間時,默認使用'/'空間。
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
$io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// sending to all clients in 'game' room, including sender
|
|
|
|
|
// 向 game 房間內的所有連接推送 bigger-announcement 事件。
|
|
|
|
|
$io->in('game')->emit('big-announcement', 'the game will start soon');
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// sending to all clients in namespace 'myNamespace', including sender
|
|
|
|
|
// 向 /myNamespace 命名空間下的所有連接推送 bigger-announcement 事件
|
|
|
|
|
$io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// sending to a specific room in a specific namespace, including sender
|
|
|
|
|
// 向 /myNamespace 命名空間下的 room 房間所有連接推送 event 事件
|
|
|
|
|
$io->of('/myNamespace')->to('room')->emit('event', 'message');
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// sending to individual socketid (private message)
|
|
|
|
|
// 向 socketId 單點推送
|
|
|
|
|
$io->to('socketId')->emit('hey', 'I just met you');
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// sending to all clients on this node (when using multiple nodes)
|
|
|
|
|
// 向本機所有連接推送
|
|
|
|
|
$io->local->emit('hi', 'my lovely babies');
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
// sending to all connected clients
|
|
|
|
|
// 向所有連接推送
|
|
|
|
|
$io->emit('an event sent to all connected clients');
|
|
|
|
|
```
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
### 命名空間API
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
和全局API一樣,只不過已經限制了命名空間。
|
|
|
|
|
```php
|
|
|
|
|
// 以下偽碼等價
|
|
|
|
|
$foo->emit();
|
|
|
|
|
$io->of('/foo')->emit();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* class內使用也等價
|
|
|
|
|
* @SocketIONamespace("/foo")
|
|
|
|
|
*/
|
|
|
|
|
class FooNamespace extends BaseNamespace {
|
|
|
|
|
public function onEvent(){
|
|
|
|
|
$this->emit();
|
|
|
|
|
$this->io->of('/foo')->emit();
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-19 11:26:54 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 進階教程
|
|
|
|
|
|
|
|
|
|
### 設置 Socket.io 命名空間
|
|
|
|
|
|
|
|
|
|
Socket.io 通過自定義命名空間實現多路複用。(注意:不是 PHP 的命名空間)
|
|
|
|
|
|
|
|
|
|
1. 可以通過 `@SocketIONamespace("/xxx")` 將控制器映射為 xxx 的命名空間,
|
|
|
|
|
|
|
|
|
|
2. 也可通過
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
use Hyperf\SocketIOServer\Collector\SocketIORouter;
|
|
|
|
|
use App\Controller\WebSocketController;
|
|
|
|
|
SocketIORouter::addNamespace('/xxx' , WebSocketController::class);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在路由中添加。
|
|
|
|
|
|
|
|
|
|
### 開啟 Session
|
|
|
|
|
|
|
|
|
|
安裝並配置好 hyperf/session 組件及其對應中間件,再通過 `SessionAspect` 切入 SocketIO 來使用 Session 。
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
// config/autoload/aspect.php
|
|
|
|
|
return [
|
|
|
|
|
\Hyperf\SocketIOServer\Aspect\SessionAspect::class,
|
|
|
|
|
];
|
|
|
|
|
```
|
|
|
|
|
|
2020-06-21 21:36:55 +08:00
|
|
|
|
> Swoole 4.4.17 及以下版本只能讀取 HTTP 創建好的 Cookie,Swoole 4.4.18 及以上版本可以在 WebSocket 握手時創建 Cookie
|
2020-05-19 11:26:54 +08:00
|
|
|
|
|
|
|
|
|
### 調整房間適配器
|
|
|
|
|
|
|
|
|
|
默認的房間功能通過 Redis 適配器實現,可以適應多進程乃至分佈式場景。
|
|
|
|
|
|
|
|
|
|
1. 可以替換為內存適配器,只適用於單 worker 場景。
|
2020-06-21 21:36:55 +08:00
|
|
|
|
|
2020-05-19 11:26:54 +08:00
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
// config/autoload/dependencies.php
|
|
|
|
|
return [
|
|
|
|
|
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
|
|
|
|
|
];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. 可以替換為空適配器,不需要房間功能時可以降低消耗。
|
2020-06-21 21:36:55 +08:00
|
|
|
|
|
2020-05-19 11:26:54 +08:00
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
// config/autoload/dependencies.php
|
|
|
|
|
return [
|
|
|
|
|
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\NullAdapter::class,
|
|
|
|
|
];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 調整 SocketID (`sid`)
|
|
|
|
|
|
|
|
|
|
默認 SocketID 使用 `ServerID#FD` 的格式,可以適應分佈式場景。
|
|
|
|
|
|
|
|
|
|
1. 可以替換為直接使用 Fd 。
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
// config/autoload/dependencies.php
|
|
|
|
|
return [
|
|
|
|
|
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
|
|
|
|
|
];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. 也可以替換為 SessionID 。
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
// config/autoload/dependencies.php
|
|
|
|
|
return [
|
|
|
|
|
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
|
|
|
|
|
];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 其他事件分發方法
|
|
|
|
|
|
|
|
|
|
1. 可以手動註冊事件,不使用註解。
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Controller;
|
|
|
|
|
|
|
|
|
|
use Hyperf\SocketIOServer\BaseNamespace;
|
|
|
|
|
use Hyperf\SocketIOServer\SidProvider\SidProviderInterface;
|
|
|
|
|
use Hyperf\SocketIOServer\Socket;
|
|
|
|
|
use Hyperf\WebSocketServer\Sender;
|
|
|
|
|
|
|
|
|
|
class WebSocketController extends BaseNamespace
|
|
|
|
|
{
|
|
|
|
|
public function __construct(Sender $sender, SidProviderInterface $sidProvider) {
|
|
|
|
|
parent::__construct($sender,$sidProvider);
|
|
|
|
|
$this->on('event', [$this, 'echo']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function echo(Socket $socket, $data)
|
|
|
|
|
{
|
|
|
|
|
$socket->emit('event', $data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. 可以在控制器上添加 `@Event()` 註解,以方法名作為事件名來分發。此時應注意其他公有方法可能會和事件名衝突。
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Controller;
|
|
|
|
|
|
|
|
|
|
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
|
|
|
|
|
use Hyperf\SocketIOServer\Annotation\Event;
|
|
|
|
|
use Hyperf\SocketIOServer\BaseNamespace;
|
|
|
|
|
use Hyperf\SocketIOServer\Socket;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @SocketIONamespace("/")
|
|
|
|
|
* @Event()
|
|
|
|
|
*/
|
|
|
|
|
class WebSocketController extends BaseNamespace
|
|
|
|
|
{
|
|
|
|
|
public function echo(Socket $socket, $data)
|
|
|
|
|
{
|
|
|
|
|
$socket->emit('event', $data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
2020-06-16 01:43:41 +08:00
|
|
|
|
|
|
|
|
|
## Auth 鑑權
|
|
|
|
|
|
|
|
|
|
您可以通過使用中間件來攔截 WebSocket 握手,實現鑑權功能,如下:
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Middleware;
|
|
|
|
|
|
|
|
|
|
use Psr\Container\ContainerInterface;
|
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
|
use Psr\Http\Server\MiddlewareInterface;
|
|
|
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
|
|
|
use Psr\Http\Server\RequestHandlerInterface;
|
|
|
|
|
|
|
|
|
|
class WebSocketAuthMiddleware implements MiddlewareInterface
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @var ContainerInterface
|
|
|
|
|
*/
|
|
|
|
|
protected $container;
|
|
|
|
|
|
|
|
|
|
public function __construct(ContainerInterface $container)
|
|
|
|
|
{
|
|
|
|
|
$this->container = $container;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
|
|
|
|
{
|
|
|
|
|
// 偽代碼,通過 isAuth 方法攔截握手請求並實現權限檢查
|
|
|
|
|
if (! $this->isAuth($request)) {
|
|
|
|
|
return $this->container->get(\Hyperf\HttpServer\Contract\ResponseInterface::class)->raw('Forbidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $handler->handle($request);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
並將上面的中間件配置到對應的 WebSocket Server 中去即可。
|