## SocketIO消息推送 PHP系列 | PHP跨平台实时通讯框架 Socket.IO 的应用 ### PHPSocket.IO 是什么? 1. PHPSocket.IO是PHP版本的Socket.IO服务端实现,基于workerman开发,用于替换node.js版本Socket.IO服务端。 2. PHPSocket.IO底层采用websocket协议通讯,如果客户端不支持websocket协议, 则会自动采用http长轮询的方式通讯。 3. PHPSocket.IO实现的Polling通信机制包括Adobe Flash Socket、AJAX长轮询、JSONP轮询等。具体采用哪种机制通讯对于开发者完全透明, 开发者使用的是统一的接口。 ### 设计的目标 利用PHP构建能够在不同浏览器和移动设备上良好运行的实时应用,如实时分析系统、在线聊天室、在线客服系统、评论系统、WebIM等。 PHPSocket.IO与workerman的区别是,PHPSocket.IO基于workerman开发,workerman有的特性PHPSocket.IO都支持。 PHPSocket.IO最大的优势是对各种浏览器的兼容性更好。 ### 文档&&安装 文档仓库:[https://github.com/walkor/phpsocket.io](https://github.com/walkor/phpsocket.io) 使用composer安装 ``` composer require workerman/phpsocket.io ``` ### 应用案例 #### Nginx 主机配置 ```nginx server { listen 443 ssl http2; server_name www.tinywan.com; ssl_certificate letsencrypt/iot.tinywan.com/full_chain.pem; ssl_certificate_key letsencrypt/iot.tinywan.com/private.key; set $root_path /var/www/iot.tinywan.com/public; root $root_path; more_set_headers "X-Frame-Options: SAMEORIGIN"; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=/$1 last; break; } } location ~ \.php$ { fastcgi_index index.php; fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_buffer_size 128k; fastcgi_buffers 4 256k; fastcgi_busy_buffers_size 256k; } location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { access_log off; error_log off; expires 30d; } location ~ .*\.(js|css)?$ { access_log off; error_log off; expires 12h; } # SocketIO 配置 location /socket.io { proxy_pass http://127.0.0.1:2120; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header X-Real-IP $remote_addr; } } ``` #### 服务端完整代码 ```php <?php namespace app\http\controller; use PHPSocketIO\SocketIO; use think\facade\Log; use Workerman\Worker; class Server { /** * @desc: Socket Server * @time: 2019/4/22 19:51 */ public function server() { // 全局数组保存uid在线数据 $uidConnectionMap = array(); // 记录最后一次广播的在线用户数 $last_online_count = 0; // 记录最后一次广播的在线页面数 $last_online_page_count = 0; // PHPSocketIO服务 $sender_io = new SocketIO(2120); // 客户端发起连接事件时,设置连接socket的各种事件回调 $sender_io->on('connection', function ($socket) { Log::info('客户端发起连接事件 '); // 当客户端断开连接是触发(一般是关闭网页或者跳转刷新导致) $socket->on('login', function ($uid) use ($socket) { Log::info('客户端登录uid ' . $uid); global $uidConnectionMap, $last_online_count, $last_online_page_count; // 已经登录过了 if (isset($socket->uid)) { return; } // 更新对应uid的在线数据 $uid = (string)$uid; if (!isset($uidConnectionMap[$uid])) { $uidConnectionMap[$uid] = 0; } // 这个uid有++$uidConnectionMap[$uid]个socket连接 ++$uidConnectionMap[$uid]; // 将这个连接加入到uid分组,方便针对uid推送数据 $socket->join($uid); $socket->uid = $uid; }); // 当客户端断开连接是触发(一般是关闭网页或者跳转刷新导致) $socket->on('disconnect', function () use ($socket) { Log::info('客户端断开 ' . json_encode($socket)); if (!isset($socket->uid)) { return; } global $uidConnectionMap, $sender_io; // 将uid的在线socket数减一 if (--$uidConnectionMap[$socket->uid] <= 0) { unset($uidConnectionMap[$socket->uid]); } }); }); // 当$sender_io启动后监听一个http端口,通过这个端口可以给任意uid或者所有uid推送数据 $sender_io->on('workerStart', function () use ($sender_io) { // 监听一个http端口 $inner_http_worker = new Worker('http://0.0.0.0:2121');//这里IP不用改变,用的内网通讯,端口不能与socket端口想通 $inner_http_worker->onMessage = function ($http_connection, $data) use ($sender_io) { $postData = $data['post']; $to = $postData['to'] ?? ''; Log::info('发送到用户to ' . json_encode($to)); $content = htmlspecialchars($postData['content']); // 有指定uid则向uid所在socket组发送数据 if ($to) { $sender_io->to($to)->emit('new_msg', $content); } else { // 否则向所有uid推送数据 $sender_io->emit('new_msg', $content); } // http接口返回,如果用户离线socket返回fail if ($to && !isset($uidConnectionMap[$to])) { return $http_connection->send('offline'); } else { return $http_connection->send('ok'); } }; // 执行监听 $inner_http_worker->listen(); }); Worker::runAll(); } } ``` 1、启动服务端端:`php web_msg.php start -d` ``` $ php web_msg.php start -d Workerman[web_msg.php] start in DAEMON mode ------------------------------------------- WORKERMAN ------------------------------------------- Workerman version:3.5.20 PHP version:7.2.9 -------------------------------------------- WORKERS -------------------------------------------- proto user worker listen processes status tcp www PHPSocketIO socketIO://0.0.0.0:2120 1 [OK] ``` 2、发送消息 ``` function send_web_msg($to_uid = 1, $content) { if (empty($content)) { return ["error_code" => 404, "reason" => '缺少参数']; } $push_api_url = "http://127.0.0.1:2121/"; $post_data = [ "type" => "publish", "content" => $content, "to" => $to_uid ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $push_api_url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); curl_setopt($ch, CURLOPT_HTTPHEADER, array("Expect:")); $return = curl_exec($ch); curl_close($ch); return $return; } send_web_msg(1, "哈喽,Tinywan先生"); ``` #### 客户端完整代码 html 代码 ``` <div><p id="notice-content" style="float: left">系统公告</p></div> ``` JS 代码 ``` <script src='https://cdn.bootcss.com/socket.io/2.0.3/socket.io.js'></script> <script src="https://cdn.bootcss.com/notify.js/3.0.0/notify.js"></script> <script> $(document).ready(function () { $uid = "1"; var socket = io("https://www.tinywan.com", { path: '/socket.io' }); socket.on('connect', function () { console.log('连接成功'); socket.emit('login', $uid); }); socket.on('new_msg', function (msg) { console.log('系统消息:' + msg); $('#notice-content').html('系统提示:' + msg); $('.notification.sticky').notify(); }); }); </script> ``` ### 效果图 ![](https://box.kancloud.cn/952198f9df16268640509a8c0f358b3f_677x70.png)