Socket.io是一款非常流行的应用层实时通讯协议和框架,可以轻松实现应答、分组、广播。hyperf/socketio-server支持了Socket.io的WebSocket传输协议。

安装

  1. composer require hyperf/socketio-server

hyperf/socketio-server 组件是基于 WebSocket 实现的,请确保服务端已经添加了 WebSocket 服务 的配置。

  1. // config/autoload/server.php
  2. [
  3. 'name' => 'socket-io',
  4. 'type' => Server::SERVER_WEBSOCKET,
  5. 'host' => '0.0.0.0',
  6. 'port' => 9502,
  7. 'sock_type' => SWOOLE_SOCK_TCP,
  8. 'callbacks' => [
  9. SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
  10. SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
  11. SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
  12. ],
  13. ],

快速开始

服务端

  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use Hyperf\SocketIOServer\Annotation\Event;
  5. use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
  6. use Hyperf\SocketIOServer\BaseNamespace;
  7. use Hyperf\SocketIOServer\Socket;
  8. use Hyperf\Utils\Codec\Json;
  9. /**
  10. * @SocketIONamespace("/")
  11. */
  12. class WebSocketController extends BaseNamespace
  13. {
  14. /**
  15. * @Event("event")
  16. * @param string $data
  17. */
  18. public function onEvent(Socket $socket, $data)
  19. {
  20. // 应答
  21. return 'Event Received: ' . $data;
  22. }
  23. /**
  24. * @Event("join-room")
  25. * @param string $data
  26. */
  27. public function onJoinRoom(Socket $socket, $data)
  28. {
  29. // 将当前用户加入房间
  30. $socket->join($data);
  31. // 向房间内其他用户推送(不含当前用户)
  32. $socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
  33. // 向房间内所有人广播(含当前用户)
  34. $this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
  35. }
  36. /**
  37. * @Event("say")
  38. * @param string $data
  39. */
  40. public function onSay(Socket $socket, $data)
  41. {
  42. $data = Json::decode($data);
  43. $socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
  44. }
  45. }

每个 socket 会自动加入以自己 sid 命名的房间($socket->getSid()),发送私聊信息就推送到对应 sid 即可。

框架会自动触发 connectdisconnect 两个事件。

客户端

由于服务端只实现了 WebSocket 通讯,所以客户端要加上 {transports:["websocket"]}

  1. <script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
  2. <script>
  3. var socket = io('ws://127.0.0.1:9502', { transports: ["websocket"] });
  4. socket.on('connect', data => {
  5. socket.emit('event', 'hello, hyperf', console.log);
  6. socket.emit('join-room', 'room1', console.log);
  7. setInterval(function () {
  8. socket.emit('say', '{"room":"room1", "message":"Hello Hyperf."}');
  9. }, 1000);
  10. });
  11. socket.on('event', console.log);
  12. </script>

API 清单

Socket API

通过 SocketAPI 对目标 Socket 进行推送,或以目标 Socket 的身份在房间内发言。需要在事件回调中使用。

  1. <?php
  2. /**
  3. * @Event("SomeEvent")
  4. */
  5. function onSomeEvent(\Hyperf\SocketIOServer\Socket $socket){
  6. // sending to the client
  7. // 向连接推送 hello 事件
  8. $socket->emit('hello', 'can you hear me?', 1, 2, 'abc');
  9. // sending to all clients except sender
  10. // 向所有连接推送 broadcast 事件,但是不包括当前连接。
  11. $socket->broadcast->emit('broadcast', 'hello friends!');
  12. // sending to all clients in 'game' room except sender
  13. // 向 game 房间内所有连接推送 nice game 事件,但是不包括当前连接。
  14. $socket->to('game')->emit('nice game', "let's play a game");
  15. // sending to all clients in 'game1' and/or in 'game2' room, except sender
  16. // 向 game1 房间 和 game2 房间内所有连接取并集推送 nice game 事件,但是不包括当前连接。
  17. $socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
  18. // WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
  19. // named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
  20. // 注意:自己给自己推送的时候不要加to,因为$socket->to()总是排除自己。直接$socket->emit()就好了。
  21. // sending with acknowledgement
  22. // 发送信息,并且等待并接收客户端响应。
  23. $reply = $socket->emit('question', 'do you think so?')->reply();
  24. // sending without compression
  25. // 无压缩推送
  26. $socket->compress(false)->emit('uncompressed', "that's rough");
  27. }

全局API

直接从容器中获取SocketIO单例。这个单例可向全局广播或指定房间、个人通讯。未指定命名空间时,默认使用’/‘空间。

  1. <?php
  2. $io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
  3. // sending to all clients in 'game' room, including sender
  4. // 向 game 房间内的所有连接推送 bigger-announcement 事件。
  5. $io->in('game')->emit('big-announcement', 'the game will start soon');
  6. // sending to all clients in namespace 'myNamespace', including sender
  7. // 向 /myNamespace 命名空间下的所有连接推送 bigger-announcement 事件
  8. $io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
  9. // sending to a specific room in a specific namespace, including sender
  10. // 向 /myNamespace 命名空间下的 room 房间所有连接推送 event 事件
  11. $io->of('/myNamespace')->to('room')->emit('event', 'message');
  12. // sending to individual socketid (private message)
  13. // 向 socketId 单点推送
  14. $io->to('socketId')->emit('hey', 'I just met you');
  15. // sending to all clients on this node (when using multiple nodes)
  16. // 向本机所有连接推送
  17. $io->local->emit('hi', 'my lovely babies');
  18. // sending to all connected clients
  19. // 向所有连接推送
  20. $io->emit('an event sent to all connected clients');

命名空间API

和全局API一样,只不过已经限制了命名空间。

  1. // 以下伪码等价
  2. $foo->emit();
  3. $io->of('/foo')->emit();
  4. /**
  5. * class内使用也等价
  6. * @SocketIONamespace("/foo")
  7. */
  8. class FooNamespace extends BaseNamespace {
  9. public function onEvent(){
  10. $this->emit();
  11. $this->io->of('/foo')->emit();
  12. }
  13. }

进阶教程

设置 Socket.io 命名空间

Socket.io 通过自定义命名空间实现多路复用。(注意:不是 PHP 的命名空间)

  1. 可以通过 @SocketIONamespace("/xxx") 将控制器映射为 xxx 的命名空间,

  2. 也可通过

  1. <?php
  2. use Hyperf\SocketIOServer\Collector\SocketIORouter;
  3. use App\Controller\WebSocketController;
  4. SocketIORouter::addNamespace('/xxx' , WebSocketController::class);

在路由中添加。

开启 Session

安装并配置好 hyperf/session 组件及其对应中间件,再通过 SessionAspect 切入 SocketIO 来使用 Session 。

  1. <?php
  2. // config/autoload/aspect.php
  3. return [
  4. \Hyperf\SocketIOServer\Aspect\SessionAspect::class,
  5. ];

Swoole 4.4.17 及以下版本只能读取 HTTP 创建好的 Cookie,Swoole 4.4.18 及以上版本可以在 WebSocket 握手时创建 Cookie

调整房间适配器

默认的房间功能通过 Redis 适配器实现,可以适应多进程乃至分布式场景。

  1. 可以替换为内存适配器,只适用于单 worker 场景。
  1. <?php
  2. // config/autoload/dependencies.php
  3. return [
  4. \Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
  5. ];
  1. 可以替换为空适配器,不需要房间功能时可以降低消耗。
  1. <?php
  2. // config/autoload/dependencies.php
  3. return [
  4. \Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\NullAdapter::class,
  5. ];

调整 SocketID (sid)

默认 SocketID 使用 ServerID#FD 的格式,可以适应分布式场景。

  1. 可以替换为直接使用 Fd 。
  1. <?php
  2. // config/autoload/dependencies.php
  3. return [
  4. \Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
  5. ];
  1. 也可以替换为 SessionID 。
  1. <?php
  2. // config/autoload/dependencies.php
  3. return [
  4. \Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
  5. ];

其他事件分发方法

  1. 可以手动注册事件,不使用注解。
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use Hyperf\SocketIOServer\BaseNamespace;
  5. use Hyperf\SocketIOServer\SidProvider\SidProviderInterface;
  6. use Hyperf\SocketIOServer\Socket;
  7. use Hyperf\WebSocketServer\Sender;
  8. class WebSocketController extends BaseNamespace
  9. {
  10. public function __construct(Sender $sender, SidProviderInterface $sidProvider) {
  11. parent::__construct($sender,$sidProvider);
  12. $this->on('event', [$this, 'echo']);
  13. }
  14. public function echo(Socket $socket, $data)
  15. {
  16. $socket->emit('event', $data);
  17. }
  18. }
  1. 可以在控制器上添加 @Event() 注解,以方法名作为事件名来分发。此时应注意其他公有方法可能会和事件名冲突。
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
  5. use Hyperf\SocketIOServer\Annotation\Event;
  6. use Hyperf\SocketIOServer\BaseNamespace;
  7. use Hyperf\SocketIOServer\Socket;
  8. /**
  9. * @SocketIONamespace("/")
  10. * @Event()
  11. */
  12. class WebSocketController extends BaseNamespace
  13. {
  14. public function echo(Socket $socket, $data)
  15. {
  16. $socket->emit('event', $data);
  17. }
  18. }

Auth 鉴权

您可以通过使用中间件来拦截 WebSocket 握手,实现鉴权功能,如下:

  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Middleware;
  4. use Psr\Container\ContainerInterface;
  5. use Psr\Http\Message\ResponseInterface;
  6. use Psr\Http\Server\MiddlewareInterface;
  7. use Psr\Http\Message\ServerRequestInterface;
  8. use Psr\Http\Server\RequestHandlerInterface;
  9. class WebSocketAuthMiddleware implements MiddlewareInterface
  10. {
  11. /**
  12. * @var ContainerInterface
  13. */
  14. protected $container;
  15. public function __construct(ContainerInterface $container)
  16. {
  17. $this->container = $container;
  18. }
  19. public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
  20. {
  21. // 伪代码,通过 isAuth 方法拦截握手请求并实现权限检查
  22. if (! $this->isAuth($request)) {
  23. return $this->container->get(\Hyperf\HttpServer\Contract\ResponseInterface::class)->raw('Forbidden');
  24. }
  25. return $handler->handle($request);
  26. }
  27. }

并将上面的中间件配置到对应的 WebSocket Server 中去即可。