实战 swoole【聊天室】

2019-08-01 15:21:48   PHP

  PHP   Swoole  

前言:了解概念之后就应该练练手啦,不然就是巨婴

有收获的话请加颗小星星,没有收获的话可以 反对 没有帮助 举报三连

准备工作

  • 需要先看初识swoole【上】,了解基本的服务端WebSocket使用
  • js WebSocket客户端简单使用

使用

  1. # 命令行1
  2. php src/websocket/run.php
  3. # 命令行2
  4. cd public && php -S localhost:8000
  5. # 客户端,多开几个查看效果
  6. 访问http://localhost:8000/

WebSocket

官方示例

  1. $server = new swoole_websocket_server("0.0.0.0", 9501);
  2. $server->on('open', function (swoole_websocket_server $server, $request) {
  3. echo "server: handshake success with fd{$request->fd}\n";
  4. });
  5. $server->on('message', function (swoole_websocket_server $server, $frame) {
  6. echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
  7. $server->push($frame->fd, "this is server");
  8. });
  9. $server->on('close', function ($ser, $fd) {
  10. echo "client {$fd} closed\n";
  11. });
  12. $server->on('request', function (swoole_http_request $request, swoole_http_response $response) {
  13. global $server;//调用外部的server
  14. // $server->connections 遍历所有websocket连接用户的fd,给所有用户推送
  15. foreach ($server->connections as $fd) {
  16. $server->push($fd, $request->get['message']);
  17. }
  18. });
  19. $server->start();

详解:

  • swoole_websocket_server 继承自 swoole_http_server

    • 设置了onRequest回调,websocket服务器也可以同时作为http服务器
    • 未设置onRequest回调,websocket服务器收到http请求后会返回http 400错误页面
    • 如果想通过接收http触发所有websocket的推送,需要注意作用域的问题,面向过程请使用global对swoole_websocket_server进行引用,面向对象可以把swoole_websocket_server设置成一个成员属性
  • function onOpen(swoole_websocket_server $svr, swoole_http_request $req);

    • 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数。
    • $req 是一个Http请求对象,包含了客户端发来的握手请求信息
    • onOpen事件函数中可以调用push向客户端发送数据或者调用close关闭连接
    • onOpen事件回调是可选的
  • function onMessage(swoole_websocket_server $server, swoole_websocket_frame $frame)

    • 当服务器收到来自客户端的数据帧时会回调此函数。
    • $frame 是swoole_websocket_frame对象,包含了客户端发来的数据帧信息
    • onMessage回调必须被设置,未设置服务器将无法启动
    • 客户端发送的ping帧不会触发onMessage,底层会自动回复pong包
  • swoole_websocket_frame 属性
    • $frame->fd,客户端的socket id,使用$server->push推送数据时需要用到
    • $frame->data,数据内容,可以是文本内容也可以是二进制数据,可以通过opcode的值来判断
    • $frame->opcode,WebSocket的OpCode类型,可以参考WebSocket协议标准文档
    • $frame->finish, 表示数据帧是否完整,一个WebSocket请求可能会分成多个数据帧进行发送(底层已经实现了自动合并数据帧,现在不用担心接收到的数据帧不完整)

聊天室服务端示例

目录结构:

  • config
    • socket.php
  • src
    • websocket
      • Config.php
      • run.php
      • WebSocketServer.php 内存表版本
      • WsRedisServer.php redis版本

WebSocketServer.php 内存表版本

  1. <?php
  2. namespace App\WebSocket;
  3. class WebSocketServer
  4. {
  5. private $config;
  6. private $table;
  7. private $server;
  8. public function __construct()
  9. {
  10. // 内存表 实现进程间共享数据,也可以使用redis替代
  11. $this->createTable();
  12. // 实例化配置
  13. $this->config = Config::getInstance();
  14. }
  15. public function run()
  16. {
  17. $this->server = new \swoole_websocket_server(
  18. $this->config['socket']['host'],
  19. $this->config['socket']['port']
  20. );
  21. $this->server->on('open', [$this, 'open']);
  22. $this->server->on('message', [$this, 'message']);
  23. $this->server->on('close', [$this, 'close']);
  24. $this->server->start();
  25. }
  26. public function open(\swoole_websocket_server $server, \swoole_http_request $request)
  27. {
  28. $user = [
  29. 'fd' => $request->fd,
  30. 'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd,
  31. 'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])]
  32. ];
  33. // 放入内存表
  34. $this->table->set($request->fd, $user);
  35. $server->push($request->fd, json_encode(
  36. array_merge(['user' => $user], ['all' => $this->allUser()], ['type' => 'openSuccess'])
  37. )
  38. );
  39. }
  40. private function allUser()
  41. {
  42. $users = [];
  43. foreach ($this->table as $row) {
  44. $users[] = $row;
  45. }
  46. return $users;
  47. }
  48. public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
  49. {
  50. $this->pushMessage($server, $frame->data, 'message', $frame->fd);
  51. }
  52. /**
  53. * 推送消息
  54. *
  55. * @param \swoole_websocket_server $server
  56. * @param string $message
  57. * @param string $type
  58. * @param int $fd
  59. */
  60. private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd)
  61. {
  62. $message = htmlspecialchars($message);
  63. $datetime = date('Y-m-d H:i:s', time());
  64. $user = $this->table->get($fd);
  65. foreach ($this->table as $item) {
  66. // 自己不用发送
  67. if ($item['fd'] == $fd) {
  68. continue;
  69. }
  70. $server->push($item['fd'], json_encode([
  71. 'type' => $type,
  72. 'message' => $message,
  73. 'datetime' => $datetime,
  74. 'user' => $user
  75. ]));
  76. }
  77. }
  78. /**
  79. * 客户端关闭的时候
  80. *
  81. * @param \swoole_websocket_server $server
  82. * @param int $fd
  83. */
  84. public function close(\swoole_websocket_server $server, int $fd)
  85. {
  86. $user = $this->table->get($fd);
  87. $this->pushMessage($server, "{$user['name']}离开聊天室", 'close', $fd);
  88. $this->table->del($fd);
  89. }
  90. /**
  91. * 创建内存表
  92. */
  93. private function createTable()
  94. {
  95. $this->table = new \swoole_table(1024);
  96. $this->table->column('fd', \swoole_table::TYPE_INT);
  97. $this->table->column('name', \swoole_table::TYPE_STRING, 255);
  98. $this->table->column('avatar', \swoole_table::TYPE_STRING, 255);
  99. $this->table->create();
  100. }
  101. }

WsRedisServer.php redis版本

  1. <?php
  2. namespace App\WebSocket;
  3. use Predis\Client;
  4. /**
  5. * 使用redis代替table,并存储历史聊天记录
  6. *
  7. * Class WsRedisServer
  8. * @package App\WebSocket
  9. */
  10. class WsRedisServer
  11. {
  12. private $config;
  13. private $server;
  14. private $client;
  15. private $key = "socket:user";
  16. public function __construct()
  17. {
  18. // 实例化配置
  19. $this->config = Config::getInstance();
  20. // redis
  21. $this->initRedis();
  22. // 初始化,主要是服务端自己关闭不会清空redis
  23. foreach ($this->allUser() as $item) {
  24. $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']);
  25. }
  26. }
  27. public function run()
  28. {
  29. $this->server = new \swoole_websocket_server(
  30. $this->config['socket']['host'],
  31. $this->config['socket']['port']
  32. );
  33. $this->server->on('open', [$this, 'open']);
  34. $this->server->on('message', [$this, 'message']);
  35. $this->server->on('close', [$this, 'close']);
  36. $this->server->start();
  37. }
  38. public function open(\swoole_websocket_server $server, \swoole_http_request $request)
  39. {
  40. $user = [
  41. 'fd' => $request->fd,
  42. 'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd,
  43. 'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])]
  44. ];
  45. // 放入redis
  46. $this->client->hmset("{$this->key}:{$user['fd']}", $user);
  47. // 给每个人推送,包括自己
  48. foreach ($this->allUser() as $item) {
  49. $server->push($item['fd'], json_encode([
  50. 'user' => $user,
  51. 'all' => $this->allUser(),
  52. 'type' => 'openSuccess'
  53. ]));
  54. }
  55. }
  56. private function allUser()
  57. {
  58. $users = [];
  59. $keys = $this->client->keys("{$this->key}:*");
  60. // 所有的key
  61. foreach ($keys as $k => $item) {
  62. $users[$k]['fd'] = $this->client->hget($item, 'fd');
  63. $users[$k]['name'] = $this->client->hget($item, 'name');
  64. $users[$k]['avatar'] = $this->client->hget($item, 'avatar');
  65. }
  66. return $users;
  67. }
  68. public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
  69. {
  70. $this->pushMessage($server, $frame->data, 'message', $frame->fd);
  71. }
  72. /**
  73. * 推送消息
  74. *
  75. * @param \swoole_websocket_server $server
  76. * @param string $message
  77. * @param string $type
  78. * @param int $fd
  79. */
  80. private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd)
  81. {
  82. $message = htmlspecialchars($message);
  83. $datetime = date('Y-m-d H:i:s', time());
  84. $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd');
  85. $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name');
  86. $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar');
  87. foreach ($this->allUser() as $item) {
  88. // 自己不用发送
  89. if ($item['fd'] == $fd) {
  90. continue;
  91. }
  92. $is_push = $server->push($item['fd'], json_encode([
  93. 'type' => $type,
  94. 'message' => $message,
  95. 'datetime' => $datetime,
  96. 'user' => $user
  97. ]));
  98. // 删除失败的推送
  99. if (!$is_push) {
  100. $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']);
  101. }
  102. }
  103. }
  104. /**
  105. * 客户端关闭的时候
  106. *
  107. * @param \swoole_websocket_server $server
  108. * @param int $fd
  109. */
  110. public function close(\swoole_websocket_server $server, int $fd)
  111. {
  112. $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd');
  113. $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name');
  114. $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar');
  115. $this->pushMessage($server, "{$user['name']}离开聊天室", 'close', $fd);
  116. $this->client->hdel("{$this->key}:{$fd}", ['fd', 'name', 'avatar']);
  117. }
  118. /**
  119. * 初始化redis
  120. */
  121. private function initRedis()
  122. {
  123. $this->client = new Client([
  124. 'scheme' => $this->config['socket']['redis']['scheme'],
  125. 'host' => $this->config['socket']['redis']['host'],
  126. 'port' => $this->config['socket']['redis']['port'],
  127. ]);
  128. }
  129. }

config.php

  1. <?php
  2. namespace App\WebSocket;
  3. class Config implements \ArrayAccess
  4. {
  5. private $path;
  6. private $config;
  7. private static $instance;
  8. public function __construct()
  9. {
  10. $this->path = __DIR__ . '/../../config/';
  11. }
  12. // 单例模式
  13. public static function getInstance()
  14. {
  15. if (!self::$instance) {
  16. self::$instance = new self();
  17. }
  18. return self::$instance;
  19. }
  20. public function offsetSet($offset, $value)
  21. {
  22. // 阉割
  23. }
  24. public function offsetGet($offset)
  25. {
  26. if (empty($this->config)) {
  27. $this->config[$offset] = require $this->path . $offset . ".php";
  28. }
  29. return $this->config[$offset];
  30. }
  31. public function offsetExists($offset)
  32. {
  33. return isset($this->config[$offset]);
  34. }
  35. public function offsetUnset($offset)
  36. {
  37. // 阉割
  38. }
  39. // 禁止克隆
  40. final private function __clone(){}
  41. }

config/socket.php

  1. <?php
  2. return [
  3. 'host' => '0.0.0.0',
  4. 'port' => 9501,
  5. 'redis' => [
  6. 'scheme' => 'tcp',
  7. 'host' => '0.0.0.0',
  8. 'port' => 6380
  9. ],
  10. 'avatar' => [
  11. './images/avatar/1.jpg',
  12. './images/avatar/2.jpg',
  13. './images/avatar/3.jpg',
  14. './images/avatar/4.jpg',
  15. './images/avatar/5.jpg',
  16. './images/avatar/6.jpg'
  17. ],
  18. 'name' => [
  19. '科比',
  20. '库里',
  21. 'KD',
  22. 'KG',
  23. '乔丹',
  24. '邓肯',
  25. '格林',
  26. '汤普森',
  27. '伊戈达拉',
  28. '麦迪',
  29. '艾弗森',
  30. '卡哇伊',
  31. '保罗'
  32. ]
  33. ];

run.php

  1. <?php
  2. require __DIR__ . '/../bootstrap.php';
  3. $server = new App\WebSocket\WebSocketServer();
  4. $server->run();

总结

完整示例:聊天室

学完后发现生活中所谓的聊天室其实也不过如此,当然这只是简单的demo,很多功能都没有实现,想进一步学习的话可以去github上找完整的项目进行深入学习

参考