0、前言

在实际项目中,长连接我们会经常用到,有时候为了后台的信息即时性,我们也会使用到长连接进行消息推送等。
同时,在一些例如即时通讯工具、游戏开发、行程APP开发中。长连接都是基础的、必须掌握的技能点。

下面我们就来学习下,如何使用Swoole,搭建一个基本的WebSocket架构。

1、注意事项

(以下标记可以理解为一个class的实例,或一个请求实例。)
1、上下线状态管理,由于长连接是基于TCP握手协议构建的,所以当我们握手时需要标记对应实例的上线状态,当该标记挥手离开时,我们需要标记其下线状态,又或者直接杀掉该进程。

2、消息广播过滤,前面说到,标记会存在下线情况,当标记下线的时候,如果你还向该标记push消息,就会出现警告错误,所以需要利用上下线状态,避开这些可能已经挂掉的请求。

3、消息广播管理,由于WebSocket只有一个动作接收所有请求消息,所以不管是上线、下线、聊天消息等内容都是发送到这个动作中,所以我们需要合理的设计广播内容数据格式,至少都需要有一个code请求状态跟user_id身份标记。

2、服务端server.php代码

  1. <?php
  2. // +----------------------------------------------------------------------
  3. // 小黄牛blog - Swoole 即时通讯交互处理
  4. // +----------------------------------------------------------------------
  5. // Copyright (c) 2018 https://xiuxian.junphp.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // Author: 小黄牛 <1731223728@qq.com>
  10. // +----------------------------------------------------------------------
  11. class Server{
  12. /**
  13. * 客户端身份存储器
  14. */
  15. private $_data = [];
  16. /**
  17. * WS的启动实例
  18. */
  19. private $_ws;
  20. /**
  21. * host-IP,0.0.0.0表示允许接收所有请求
  22. */
  23. private $_host = '0.0.0.0';
  24. /**
  25. * 端口号
  26. */
  27. private $_port = '9502';
  28. /**
  29. * 这是启动服务端的入口
  30. */
  31. public function run() {
  32. $this->start_service();
  33. $this->start_handshake();
  34. $this->start_message();
  35. $this->end();
  36. }
  37. /**
  38. * ①启动websocker服务
  39. */
  40. private function start_service() {
  41. # 创建websocket服务器对象,监听0.0.0.0:9502端口
  42. $this->_ws = new swoole_websocket_server($this->_host, $this->_port);
  43. }
  44. /**
  45. * ②监听WebSocket握手申请
  46. */
  47. private function start_handshake() {
  48. # 监听WebSocket连接打开事件
  49. $this->_ws->on('open', function ($ws, $request){
  50. # 加入身份存储器
  51. $this->_data[$request->fd]['fd'] = $request->fd;
  52. # 默认离线状态
  53. $this->_data[$request->fd]['status'] = 0;
  54. });
  55. }
  56. /**
  57. * ③监听客户端消息发送请求
  58. */
  59. private function start_message() {
  60. # 监听WebSocket消息事件
  61. $this->_ws->on('message', function ($ws, $frame) {
  62. $data = json_decode($frame->data, true);
  63. # 登录广播处理
  64. if ($data['code'] == 1) {
  65. # 修改fd对应的身份
  66. $this->_data[$frame->fd]['user_id'] = $data['user_id'];
  67. # 设置上线状态
  68. $this->_data[$frame->fd]['status'] = 1;
  69. # 发送广播上线消息
  70. $data['content'] = '【'.$data['user_id'].'】骑着小黄牛上线啦~!';
  71. $this->broadcast($ws, $this->json($data), $frame->fd);
  72. # 其他请求
  73. } else {
  74. # 广播消息
  75. $this->broadcast($ws, $frame->data, $frame->fd);
  76. }
  77. });
  78. }
  79. /**
  80. * ④监听客户端退出事件
  81. */
  82. private function end() {
  83. # 监听WebSocket连接关闭事件
  84. $this->_ws->on('close', function ($ws, $fd) {
  85. # 获取用户ID
  86. $user_id = $this->_data[$fd]['user_id'];
  87. # 设置离线状态
  88. $this->_data[$fd]['status'] = 0;
  89. $data = [
  90. 'code' => 2,
  91. 'user_id' => $user_id,
  92. 'content' => '【'.$user_id.'】骑着小扫帚灰溜溜的走了~~!'
  93. ];
  94. # 广播消息
  95. $this->broadcast($ws, $this->json($data));
  96. });
  97. $this->_ws->start();
  98. }
  99. /**
  100. * 广播消息
  101. * @todo 无
  102. * @author 小黄牛
  103. * @version v1.0.0.1 + 2018.11.12
  104. * @deprecated 暂不弃用
  105. * @global 无
  106. * @param object $wx 实例
  107. * @param string $content 广播内容
  108. * @param string $id 自己的身份凭证 如果为空则也向自己广播
  109. * @return void
  110. */
  111. private function broadcast($ws, $content, $id=null) {
  112. # 向所有人广播
  113. foreach ($this->_data as $k=>$v) {
  114. # 不向自己广播,并且要在线的
  115. # 注意,这里一定要有上线状态的限制,否则假设用户已经退出,但你的进程还开着,实际上已经关闭,这时候push就会报错
  116. if ($k != $id && $v['status'] == 1) {
  117. $ws->push($v['fd'], $content);
  118. }
  119. }
  120. }
  121. /**
  122. * 数组转json
  123. * @todo 无
  124. * @author 小黄牛
  125. * @version v1.0.0.1 + 2018.11.08
  126. * @deprecated 暂不弃用
  127. * @global 无
  128. * @param array $array 数组
  129. * @return json
  130. */
  131. private function json($array) {
  132. return json_encode($array, JSON_UNESCAPED_UNICODE);
  133. }
  134. }
  135. $socketServer = new Server();
  136. $socketServer->run();

3、前端WebSocket代码

  1. <?php
  2. // +----------------------------------------------------------------------
  3. // 小黄牛blog - websocket
  4. // +----------------------------------------------------------------------
  5. // Copyright (c) 2018 https://xiuxian.junphp.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // Author: 小黄牛 <1731223728@qq.com>
  10. // +----------------------------------------------------------------------
  11. ?>
  12. <!DOCTYPE html>
  13. <html>
  14. <head>
  15. <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  16. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  17. <title>Swoole+Websocket案例 - 小黄牛</title>
  18. <script src="./js/jquery.min.js"></script>
  19. <style>
  20. html,body{margin:0;padding:0;font-size:13px}
  21. .left{width: 20%;height: 600px;border: 1px solid #ddd;float: left;}
  22. .right{width: 79.7%;height: 400px;border: 1px solid #ddd;border-left: 0px;float: left;overflow: auto;}
  23. .bottom{width: 79.7%;height: 199px;border: 1px solid #ddd;border-left: 0px;border-top: 0px;float: left;}
  24. #content{width: 99.5%;height: 165px;}
  25. .blue{color:blue}
  26. .red{color:red}
  27. .div_left{width:100%;float:left}
  28. .div_right{width:100%;float:left;text-align: right;}
  29. .div_centent{width:100%;float:left;text-align: center;}
  30. </style>
  31. </head>
  32. <body>
  33. <div id="USER"></div>
  34. <div class="left">
  35. <ul>
  36. </ul>
  37. </div>
  38. <div class="right">
  39. </div>
  40. <div class="bottom">
  41. <textarea id="content"></textarea>
  42. <button type="button" id="submit">发送消息</button>
  43. </div>
  44. <h3>使用方法:</h3>
  45. <p>①:CD进您的server.php文件目录</p>
  46. <p>②:如果您是调试阶段,可以直接php server.php,激活程序,这样的话在运行过程中出错,能在cmd界面查看报错内容</p>
  47. <p>③:如果您是部署阶段,可以使用nohup server.php >>/dev/null 2>&1 &命令,后台守护进程运行。</p>
  48. </body>
  49. </html>
  50. <script>
  51. var wsServer = 'ws://47.106.187.208:9502';
  52. var websocket = new WebSocket(wsServer);
  53. // 生成一个唯一ID,假设这是userID
  54. var USER_ID = "static" + Math.round(Math.random() * 10000);
  55. $('#USER').html('您的USER_ID为:'+USER_ID);
  56. // 打开连接成功
  57. websocket.onopen = function (evt) {
  58. console.log("WebSocker - 链接成功.");
  59. var data = {
  60. 'code':1, // 我们假设code为1时,是绑定登录请求
  61. 'user_id':USER_ID
  62. };
  63. // 前端发送json前,必须先转义成字符串
  64. data = JSON.stringify(data);
  65. websocket.send(data);
  66. };
  67. // 接收服务端断开时的消息通知
  68. websocket.onclose = function (evt) {
  69. alert("链接断开了");
  70. };
  71. // 接收到服务端的消息通知
  72. websocket.onmessage = function (evt) {
  73. console.log('Retrieved data from server: ' + evt.data);
  74. var obj = JSON.parse(evt.data);
  75. // 登录广播
  76. if (obj.code == 1) {
  77. // 存在修改上线状态
  78. if ($("#"+obj.user_id).length>0) {
  79. $("#"+obj.user_id+' span').removeClass('blue');
  80. $("#"+obj.user_id+' span').addClass('red');
  81. $("#"+obj.user_id+' span').html('离线');
  82. // 不存在,添加用户列表
  83. } else {
  84. $('.left ul').append('<li id="'+obj.user_id+'">'+obj.user_id+' <span class="blue">(在线)</span></li>');
  85. }
  86. $('.right').append('<div class="div_centent">'+obj.content+'</div>');
  87. // 下线广播
  88. } else if (obj.code == 2) {
  89. $("#"+obj.user_id+' span').removeClass('blue');
  90. $("#"+obj.user_id+' span').addClass('red');
  91. $("#"+obj.user_id+' span').html('离线');
  92. $('.right').append('<div class="div_centent">'+obj.content+'</div>');
  93. // 聊天消息广播
  94. } else if (obj.code == 3) {
  95. $('.right').append('<div class="div_left">'+obj.user_id+':'+obj.content+'</div>');
  96. // 聊天界面默认自动底部
  97. $('.right').scrollTop( $('.right')[0].scrollHeight );
  98. }
  99. };
  100. // 接收Socket连接失败时的异常通知
  101. websocket.onerror = function (evt, e) {
  102. console.log('Error occured: ' + evt.data);
  103. };
  104. // 点击发送消息按钮
  105. $('#submit').click(function(){
  106. var content = $('#content').val();
  107. $('.right').append('<div class="div_right">'+content+':'+USER_ID+'</div>');
  108. var data = {
  109. 'code':3, // 我们假设code为3时,既为聊天消息广播请求
  110. 'user_id':USER_ID,
  111. 'content':content
  112. };
  113. // 前端发送json前,必须先转义成字符串
  114. data = JSON.stringify(data);
  115. websocket.send(data);
  116. // 输入表单清空
  117. $('#content').val('');
  118. // 聊天界面默认自动底部
  119. $('.right').scrollTop( $('.right')[0].scrollHeight );
  120. });
  121. </script>

4、使用方法

①:使用CD命令进您的server.php文件目录
②:如果您是调试阶段,可以直接php server.php命令激活程序,这样的话在运行过程中出错,能在cmd界面查看报错内容
③:如果您是部署阶段,可以使用nohup php server.php >>/dev/null 2>&1 &命令,后台守护进程运行。

完整的案例DEMO,可以直接到我的开源栏目中进行下载:Swoole聊天室Demo之一