前言:了解了概念以后就应该练练手啦,否则就是语言的巨人,行动的矮子啦php
代码仓库html
实战 swoole【聊天室】laravel
在线体验git
须要先看初识 swoole【上】,了解基本的服务端 WebSocket 使用github
js WebSocket 客户端简单使用web
# 命令行1 php src/websocket/run.php # 命令行2 cd public && php -S localhost:8000 # 客户端,多开几个查看效果 访问http://localhost:8000/
官方示例面试
$server = new swoole_websocket_server("0.0.0.0", 9501); $server->on('open', function (swoole_websocket_server $server, $request) { echo "server: handshake success with fd{$request->fd}\n"; }); $server->on('message', function (swoole_websocket_server $server, $frame) { echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; $server->push($frame->fd, "this is server"); }); $server->on('close', function ($ser, $fd) { echo "client {$fd} closed\n"; }); $server->on('request', function (swoole_http_request $request, swoole_http_response $response) { global $server;//调用外部的server // $server->connections 遍历全部websocket链接用户的fd,给全部用户推送 foreach ($server->connections as $fd) { $server->push($fd, $request->get['message']); } }); $server->start();
详解:redis
swoole_websocket_server 继承自 swoole_http_serversql
设置了 onRequest 回调,websocket 服务器也能够同时做为 http 服务器shell
未设置 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
src
websocket
Config.php
run.php
WebSocketServer.php 内存表版本
WsRedisServer.php redis 版本
WebSocketServer.php 内存表版本
<?php namespace App\WebSocket; class WebSocketServer { private $config; private $table; private $server; public function __construct() { // 内存表 实现进程间共享数据,也可使用redis替代 $this->createTable(); // 实例化配置 $this->config = Config::getInstance(); } public function run() { $this->server = new \swoole_websocket_server( $this->config['socket']['host'], $this->config['socket']['port'] ); $this->server->on('open', [$this, 'open']); $this->server->on('message', [$this, 'message']); $this->server->on('close', [$this, 'close']); $this->server->start(); } public function open(\swoole_websocket_server $server, \swoole_http_request $request) { $user = [ 'fd' => $request->fd, 'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd, 'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])] ]; // 放入内存表 $this->table->set($request->fd, $user); $server->push($request->fd, json_encode( array_merge(['user' => $user], ['all' => $this->allUser()], ['type' => 'openSuccess']) ) ); } private function allUser() { $users = []; foreach ($this->table as $row) { $users[] = $row; } return $users; } public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame) { $this->pushMessage($server, $frame->data, 'message', $frame->fd); } /** * 推送消息 * * @param \swoole_websocket_server $server * @param string $message * @param string $type * @param int $fd */ private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd) { $message = htmlspecialchars($message); $datetime = date('Y-m-d H:i:s', time()); $user = $this->table->get($fd); foreach ($this->table as $item) { // 本身不用发送 if ($item['fd'] == $fd) { continue; } $server->push($item['fd'], json_encode([ 'type' => $type, 'message' => $message, 'datetime' => $datetime, 'user' => $user ])); } } /** * 客户端关闭的时候 * * @param \swoole_websocket_server $server * @param int $fd */ public function close(\swoole_websocket_server $server, int $fd) { $user = $this->table->get($fd); $this->pushMessage($server, "{$user['name']}离开聊天室", 'close', $fd); $this->table->del($fd); } /** * 建立内存表 */ private function createTable() { $this->table = new \swoole_table(1024); $this->table->column('fd', \swoole_table::TYPE_INT); $this->table->column('name', \swoole_table::TYPE_STRING, 255); $this->table->column('avatar', \swoole_table::TYPE_STRING, 255); $this->table->create(); } }
WsRedisServer.php redis 版本
<?php namespace App\WebSocket; use Predis\Client; /** * 使用redis代替table,并存储历史聊天记录 * * Class WsRedisServer * @package App\WebSocket */ class WsRedisServer { private $config; private $server; private $client; private $key = "socket:user"; public function __construct() { // 实例化配置 $this->config = Config::getInstance(); // redis $this->initRedis(); // 初始化,主要是服务端本身关闭不会清空redis foreach ($this->allUser() as $item) { $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']); } } public function run() { $this->server = new \swoole_websocket_server( $this->config['socket']['host'], $this->config['socket']['port'] ); $this->server->on('open', [$this, 'open']); $this->server->on('message', [$this, 'message']); $this->server->on('close', [$this, 'close']); $this->server->start(); } public function open(\swoole_websocket_server $server, \swoole_http_request $request) { $user = [ 'fd' => $request->fd, 'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd, 'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])] ]; // 放入redis $this->client->hmset("{$this->key}:{$user['fd']}", $user); // 给每一个人推送,包括本身 foreach ($this->allUser() as $item) { $server->push($item['fd'], json_encode([ 'user' => $user, 'all' => $this->allUser(), 'type' => 'openSuccess' ])); } } private function allUser() { $users = []; $keys = $this->client->keys("{$this->key}:*"); // 全部的key foreach ($keys as $k => $item) { $users[$k]['fd'] = $this->client->hget($item, 'fd'); $users[$k]['name'] = $this->client->hget($item, 'name'); $users[$k]['avatar'] = $this->client->hget($item, 'avatar'); } return $users; } public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame) { $this->pushMessage($server, $frame->data, 'message', $frame->fd); } /** * 推送消息 * * @param \swoole_websocket_server $server * @param string $message * @param string $type * @param int $fd */ private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd) { $message = htmlspecialchars($message); $datetime = date('Y-m-d H:i:s', time()); $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd'); $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name'); $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar'); foreach ($this->allUser() as $item) { // 本身不用发送 if ($item['fd'] == $fd) { continue; } $is_push = $server->push($item['fd'], json_encode([ 'type' => $type, 'message' => $message, 'datetime' => $datetime, 'user' => $user ])); // 删除失败的推送 if (!$is_push) { $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']); } } } /** * 客户端关闭的时候 * * @param \swoole_websocket_server $server * @param int $fd */ public function close(\swoole_websocket_server $server, int $fd) { $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd'); $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name'); $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar'); $this->pushMessage($server, "{$user['name']}离开聊天室", 'close', $fd); $this->client->hdel("{$this->key}:{$fd}", ['fd', 'name', 'avatar']); } /** * 初始化redis */ private function initRedis() { $this->client = new Client([ 'scheme' => $this->config['socket']['redis']['scheme'], 'host' => $this->config['socket']['redis']['host'], 'port' => $this->config['socket']['redis']['port'], ]); } }
config.php
<?php namespace App\WebSocket; class Config implements \ArrayAccess { private $path; private $config; private static $instance; public function __construct() { $this->path = __DIR__ . '/../../config/'; } // 单例模式 public static function getInstance() { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } public function offsetSet($offset, $value) { // 阉割 } public function offsetGet($offset) { if (empty($this->config)) { $this->config[$offset] = require $this->path . $offset . ".php"; } return $this->config[$offset]; } public function offsetExists($offset) { return isset($this->config[$offset]); } public function offsetUnset($offset) { // 阉割 } // 禁止克隆 final private function __clone(){} }
config/socket.php
<?php return [ 'host' => '0.0.0.0', 'port' => 9501, 'redis' => [ 'scheme' => 'tcp', 'host' => '0.0.0.0', 'port' => 6380 ], 'avatar' => [ './images/avatar/1.jpg', './images/avatar/2.jpg', './images/avatar/3.jpg', './images/avatar/4.jpg', './images/avatar/5.jpg', './images/avatar/6.jpg' ], 'name' => [ '科比', '库里', 'KD', 'KG', '乔丹', '邓肯', '格林', '汤普森', '伊戈达拉', '麦迪', '艾弗森', '卡哇伊', '保罗' ] ];
run.php
<?php require __DIR__ . '/../bootstrap.php'; $server = new App\WebSocket\WebSocketServer(); $server->run();
完整示例:聊天室
学完后发现生活中所谓的聊天室其实也不过如此,固然这只是简单的 demo,不少功能都没有实现,想进一步学习的话能够去 github 上找完整的项目进行深刻学习
更多学习内容能够访问【对标大厂】精品PHP架构师教程目录大全,只要你能看完保证薪资上升一个台阶(持续更新)
以上内容但愿帮助到你们,不少PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提高,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货须要的能够免费分享给你们,须要的能够加入个人PHP技术交流群953224940