Web Socket

EasySwoole的Web Socket 其实是由 swoole_websocket_server实现。若想使用WebSocket,请修改/Conf/Config.php,改变服务模式。

  1. "SERVER_TYPE"=>\Core\Swoole\Config::SERVER_TYPE_WEB_SOCKET

开启WebSocket模式之后,需要注册onMessage事件。

相关事件注册

EasySwoole的beforeWorkerStart事件,可以对Server做一些列的补充操作。

  1. $server->on("message",function (\swoole_websocket_server $server, \swoole_websocket_frame $frame){
  2. Logger::getInstance()->console("receive data ".$frame->data);
  3. $json = json_decode($frame->data,1);
  4. if(is_array($json)){
  5. if($json['action'] == 'who'){
  6. //可以获取bind后的uid
  7. //var_dump($server->connection_info($frame->fd));
  8. $server->push($frame->fd,"your fd is ".$frame->fd);
  9. }else{
  10. $server->push($frame->fd,"this is server and you say :".$json['content']);
  11. }
  12. }else{
  13. $server->push($frame->fd,"command error");
  14. }
  15. }
  16. );

注册message事件后,客户端发送过来的全部数据包都会被该函数处理。

  1. $server->on("handshake",function (\swoole_http_request $request, \swoole_http_response $response){
  2. //自定定握手规则,没有设置则用系统内置的(只支持version:13的)
  3. if (!isset($request->header['sec-websocket-key']))
  4. {
  5. //'Bad protocol implementation: it is not RFC6455.'
  6. $response->end();
  7. return false;
  8. }
  9. if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $request->header['sec-websocket-key'])
  10. || 16 !== strlen(base64_decode($request->header['sec-websocket-key']))
  11. )
  12. {
  13. //Header Sec-WebSocket-Key is illegal;
  14. $response->end();
  15. return false;
  16. }
  17. $key = base64_encode(sha1($request->header['sec-websocket-key']
  18. . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
  19. true));
  20. $headers = array(
  21. 'Upgrade' => 'websocket',
  22. 'Connection' => 'Upgrade',
  23. 'Sec-WebSocket-Accept' => $key,
  24. 'Sec-WebSocket-Version' => '13',
  25. 'KeepAlive' => 'off',
  26. );
  27. foreach ($headers as $key => $val)
  28. {
  29. $response->header($key, $val);
  30. }
  31. //注意 一定要有101状态码,协议规定
  32. $response->status(101);
  33. Logger::getInstance()->console('fd is '.$request->fd);
  34. //再做标记,保证唯一性,此操作可选
  35. Server::getInstance()->getServer()->bind($request->fd,time().Random::randNumStr(6));
  36. $response->end();
  37. }
  38. );

连接验证

WS的链接建立,其实是一个特殊的HTTP请求,因此,客户端在发起链接的时候,其实是像服务端发送了一个带有特殊标记的HTTP 请求,
该请求会被handshake回调函数处理,在该回调函数中,可以根据request的cookie或者header进行判断,允许不允许该用户进行链接。

Swoole支持自定义WebSocket 握手规则。若对此握手规则有疑问的,请自行百度RFC规范,查看关于WebSocket的规定。

  1. $server->on("close",function ($ser,$fd){
  2. Logger::getInstance()->console("client {$fd} close");
  3. }
  4. );

当客户端与服务的断开链接时,均会触发此操作,注意:HTTP 协议也会触发该请求,可以通过server->connection_info()函数来判定链接类型。

HTTP对WebSocket操作

模拟简单的WebSocket客户端

在/App/Static/Template 目录下建立一个websocket_client.html

  1. <html>
  2. <head>
  3. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  4. </head>
  5. <body>
  6. <div>
  7. <div>
  8. <p>info below</p>
  9. <ul id="line">
  10. </ul>
  11. </div>
  12. <div>
  13. <select id="action">
  14. <option value="who">who</option>
  15. <option value="hello">hello</option>
  16. </select>
  17. <input type="text" id="says">
  18. <button onclick="say()">发送</button>
  19. </div>
  20. </div>
  21. </body>
  22. <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
  23. <script>
  24. var wsServer = 'ws://127.0.0.1:9501';
  25. var websocket = new WebSocket(wsServer);
  26. window.onload = function () {
  27. websocket.onopen = function (evt) {
  28. addLine("Connected to WebSocket server.");
  29. };
  30. websocket.onclose = function (evt) {
  31. addLine("Disconnected");
  32. };
  33. websocket.onmessage = function (evt) {
  34. addLine('Retrieved data from server: ' + evt.data);
  35. };
  36. websocket.onerror = function (evt, e) {
  37. addLine('Error occured: ' + evt.data);
  38. };
  39. };
  40. function addLine(data) {
  41. $("#line").append("<li>"+data+"</li>");
  42. }
  43. function say() {
  44. var content = $("#says").val();
  45. var action = $("#action").val();
  46. $("#says").val('');
  47. websocket.send(JSON.stringify({
  48. action:action,
  49. content:content
  50. }));
  51. }
  52. </script>
  53. </html>

建立对应的测试控制器

  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: yf
  5. * Date: 2017/9/27
  6. * Time: 上午11:58
  7. */
  8. namespace App\Controller;
  9. use Core\AbstractInterface\AbstractController;
  10. use Core\Http\Message\Status;
  11. use Core\Component\Logger;
  12. use Core\Swoole\AsyncTaskManager;
  13. use Core\Swoole\Server;
  14. class WebSocket extends AbstractController
  15. {
  16. function index()
  17. {
  18. // TODO: Implement index() method.
  19. $this->response()->write(file_get_contents(ROOT."/App/Static/Template/websocket_client.html"));
  20. }
  21. function push(){
  22. /*
  23. * url :/webSocket/push/index.html?fd=xxxx
  24. */
  25. $fd = $this->request()->getRequestParam("fd");
  26. $info = Server::getInstance()->getServer()->connection_info($fd);
  27. if($info['websocket_status']){
  28. Logger::getInstance()->console("push data to client {$fd}");
  29. Server::getInstance()->getServer()->push($fd,"data from server at ".time());
  30. $this->response()->write("push to fd :{$fd}");
  31. }else{
  32. $this->response()->write("fd {$fd} not a websocket");
  33. }
  34. }
  35. function connectionList(){
  36. /*
  37. * url:/webSocket/connectionList/index.html
  38. * 注意 本example未引入redis来做fd信息记录,因此每次采用遍历的形式来获取结果,
  39. * 仅供思路参考,不建议在生产环节使用
  40. */
  41. $list = array();
  42. foreach (Server::getInstance()->getServer()->connections as $connection){
  43. $info = Server::getInstance()->getServer()->connection_info($connection);
  44. if($info['websocket_status']){
  45. $list[] = $connection;
  46. }
  47. }
  48. $this->response()->writeJson(200,$list,"this is all websocket list");
  49. }
  50. function broadcast(){
  51. /*
  52. * url :/webSocket/broadcast/index.html?fds=xx,xx,xx
  53. */
  54. $fds = $this->request()->getRequestParam("fds");
  55. $fds = explode(",",$fds);
  56. AsyncTaskManager::getInstance()->add(function ()use ($fds){
  57. foreach ( $fds as $fd) {
  58. Server::getInstance()->getServer()->push($fd,"this is broadcast");
  59. }
  60. });
  61. $this->response()->write('broadcast to all client');
  62. }
  63. }

注意:客户端断线问题要处理好,否则会遇见向一个不存在链接推送数据,导致底层发出waring的问题。此问题不会导致服务出错,但对于业务逻辑与保障数据送达方面,会有影响。