php workerman 即时通讯聊天系统
项目地址
项目下载好后
进入 tp 目录 启动 tp 框架
执行 composer install
安装依赖
使用命令 php think run
进入 workman 目录 启动 workman 服务
执行 composer install
安装依赖
使用命令 php .\ws_formal.php start
浏览器
进入 http://127.0.0.1:8000/
模拟客户
进入 http://127.0.0.1:8000/admin/login/login
后台登录页面
用户名 admin
密码 123456
验证 随便填 后台取消了验证
走到这一步就成功了
HTTP
http 协议
- 超文本传输协议
- 无状态协议
- 基于 tcp 协议的一个应用层的协议
- http 是单向的, 浏览器发起向服务器的连接, 服务器预先并不知道
http 协议工作过程
- 客户端和服务端建立连接 (三次握手),http 开始工作
- 建立连接后客户端发送给请求服务器
- 服务器接受到请求后, 给予相应的响应信息
WebSoket
websoket 协议
- websocket 是 H5 提出的在单个 TCP 协议上进行的全双工通讯协议
- 实现了浏览器与服务器全双工通信, 能更好的节省服务器资源和带宽并达到实事通讯的目的
- WebSokcet 是一个持久化的协议
工作过程
- 客户端发送 http 请求, 经过三次握手, 建立 TCP 连接, 在 http 请求里面存放 websocket 支持的版本号信息
- 服务器接收请求, 同样以 http 协议回应
- 连接成功, 客户端与服务器建立持久性的连接
websocket 与 http 差异
相同点
都是基于 tcp 的, 都是可靠的性传输协议
不同点
- websocket 是双向通信协议, 模拟 socket 协议, 可以双向发送或接受信息
- websocket 是持久化连接, http 是短连接
- websocket 是有状态的, http 是无状态的
- websocket 连接之后服务器和客户端可以双向发送数据, http 只能是客户端发起一次请求之后, 服务器才能返回数据
轮询
过程
- 客户端发起长轮询, 如果服务端的数据没有发生变化, 就会 hold 住请求, 知道服务端的数据发生变化
- 优点 是解决了 http 不能实时更新的弊端, 实现了 "伪 - 长连接"
- 轮询的本质依然是 request <-> response
弊端
- 推送延迟
- 服务端压力
- 推送延迟和服务端压力无法中和
websocket 改进
JS Websocket
简单示例
ws = new WebSocket('ws://127.0.0.1:2000');
//当 websocket 创建成功后 触发onopen事件
ws.onopen = function () {
var data = {};
data.type = 'login';
//标识 客户还是客服
data.group = 'member';
//发送信息
ws.send(JSON.stringify(data));
}
//收到服务端发来的消息 触发 onmessage
ws.onmessage = function (e) {
var data = JSON.parse(e.data);
}复制代码
Workerman 基础
安装
Composer 安装:composer require workerman/workerman
启动停止
# 以debug(调试)方式启动
php start.php start
# 以daemon(守护进程)方式启动
php start.php start -d
# 停止
php start.php stop
# 重启
php start.php restart
# 平滑重启
php start.php reload
# 查看状态
php start.php status复制代码
简单示例
实例一、使用 HTTP 协议对外提供 Web 服务
创建 start.php 文件
<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
require_once __DIR__ . '/vendor/autoload.php';
// 创建一个Worker监听2345端口,使用http协议通讯
$http_worker = new Worker("http://0.0.0.0:2345");
// 启动4个进程对外提供服务
$http_worker->count = 4;
// 接收到浏览器发送的数据时回复hello world给浏览器
$http_worker->onMessage = function(TcpConnection $connection, Request $request)
{
// 向浏览器发送hello world
$connection->send('hello world');
};
// 运行worker
Worker::runAll();复制代码
实例二、使用 WebSocket 协议对外提供服务
创建 ws_test.php 文件
<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/vendor/autoload.php';
// 注意:这里与上个例子不同,使用的是websocket协议
$ws_worker = new Worker("websocket://0.0.0.0:2000");
// 启动4个进程对外提供服务
$ws_worker->count = 4;
// 当收到客户端发来的数据后返回hello $data给客户端
$ws_worker->onMessage = function(TcpConnection $connection, $data)
{
// 向客户端发送hello $data
$connection->send('hello ' . $data);
};
// 运行worker
Worker::runAll();复制代码
测试
打开 chrome 浏览器,按 F12 打开调试控制台,在 Console 一栏输入 (或者把下面代码放入到 html 页面用 js 运行)
// 假设服务端ip为127.0.0.1
ws = new WebSocket("ws://127.0.0.1:2000");
ws.onopen = function() {
alert("连接成功");
ws.send('tom');
alert("给服务端发送一个字符串:tom");
};
ws.onmessage = function(e) {
alert("收到服务端的消息:" + e.data);
};复制代码
TP 的数据库类
composer require topthink/think-orm
ThinkPhp
安装
# 安装
composer create-project topthink/think tp
# 视图扩展
composer require topthink/think-view
# 多应用扩展
composer require topthink/think-multi-app
# 验证码扩展
composer require topthink/think-captcha复制代码
开启多应用
- 删除原始的
app/controller
目录 - 在项目跟目录下 使用命令
php think build admin
来创建应用 - 将全局的
config
和route
复制一份到创建的应用里面- 开器多应用后全局的
route
会失效, - 应用里面的
config
参数 可以覆盖全选的config
参数 - 可以针对不同的应用设置不同的配置参数和相同的配置
- 开器多应用后全局的
运行 thinkphp
直接运行 tpphp think run
设置端口php think run -p 8081
访问地址http://127.0.0.1:8000/
开启多应用后 通过 地址+应用名 +参数
来访问不同的应用http://127.0.0.1:8000/admin
默认的应用是index
可以忽略不写http://127.0.0.1:8000/index
更多的配置查看 手册
创建对应的控制器
php think make:controller admin@Service --plain
php think make:controller admin@Service --plain复制代码
获取 URL
//助手函数 返回buildUrl()
//如果需要返回客户端 需要先强制转换为字符串类型后再返回。
url();
(string)url();
//控制器方法路径 参数
// suffix URL后缀
// domain domain
// root 入口文件
url('index/blog/read', ['id'=>5])
->suffix('html')
->domain(true)
->root('/index.php');复制代码
中间件
生成命令
//多应用模式
php think make:middleware admin@Check复制代码
在 对应的应用 route/app.php
文件里面注册 路由中间件
use think\facade\Route;
Route::group(function(){
Route::get('index/index','index/index');
Route::get('service/index','service/index');
})->middleware(\app\admin\middleware\Check::class);
Route::role('login/login','login/login','get|post');复制代码
使用验证码扩展
验证码库需要开启 Session 才能生效。
在 app/middleware.php
设置
// 全局中间件定义文件
return [
// Session初始化
\think\middleware\SessionInit::class
];复制代码
config/captcha.php
为验证码的配置文件
示例
<!-- 获取验证码 -->
<div>{:captcha_img()}</div>
<div><img src="{:captcha_src()}" alt="captcha" /></div>复制代码
//两种方式
//校验验证码
$this->validate($data,[
'captcha|验证码'=>'require|captcha'
]);
if(!captcha_check($captcha)){
// 验证失败
};复制代码
HTTP Requests for PHP
安装
文档 https://requests.ryanmccue.info/download/composer require rmccue/requests
使用案例
$response = WpOrg\Requests\Requests::get('https://api.github.com/events');
var_dump($response->body);
// string(42865) "[{"id":"15624773365","type":"PushEvent","actor":{...
//post请求
$response = WpOrg\Requests\Requests::post('https://httpbin.org/post');
//设置请求头
$url = 'https://api.github.com/some/endpoint';
$headers = array('Content-Type' => 'application/json');
$data = array('some' => 'data');
$response = WpOrg\Requests\Requests::post($url, $headers, json_encode($data));复制代码
即时通讯聊天系统
简单的群聊功能
前端页面
聊天框内容分析
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/static/layui/css/layui.css">
<title>客户端聊天窗口</title>
<style type="text/css">
html,
body {
width: 98%;
height: 100%;
margin: 0 auto;
padding: 0px
}
.head_icon {
display: inline-block;
width: 50px;
height: 50px;
overflow: hidden;
border-radius: 20%;
}
#contentor{
overflow-y: auto; /* 垂直方向滚动 */
height: 500px; /* 高度自适应 */
width: 100%; /* 宽度自适应 */
}
</style>
</head>
<body>
<div class="layui-panel" >
<div class="layui-row layui-col-space32
" style="padding: 32px;">
<div class="layui-col-xs12 ">
<div style="border: 1px solid #f9f9f9;" id="contentor">
</div>
</div>
<div class="layui-col-md12">
<div class="layui-row">
<span class="layui-col-xs8">
<input type="text" >
</span>
<span class="layui-col-xs4">
<button type="button" class="layui-btn layui-bg-blue btn" style="width: 100%;">发送信息</button>
</span>
</div>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script type="text/javascript">
layui.use(function () {
$ = layui.jquery;
layer = layui.layer;
ws_connect()
send()
})
//发送消息
function send() {
var button = $('.btn'),
text = $('input[]');
//发送按钮点击后
button.click(function () {
//给框体里面添加对应的显示代码
// 获取输入框内容
if (text.val() === "") {
layer.msg('请输入内容')
} else {
data = { msg: text.val() };
ws.send(JSON.stringify(data));
data['avatarRam'] = avatarRam;
auto_chat(data);
//清空内容框
text.val('')
}
})
}
function ws_connect() {
ws = new WebSocket('ws://127.0.0.1:2000');
//当 websocket 创建成功后 触发onopen事件
ws.onopen = function () {
// auto_chat('你是零基础的吗','老手覅');
// setTimeout(()=>{auto_chat('同学你好','老手覅')}, 2000)
var data = {};
data.type = 'login';
//标识 客户还是客服
data.group = 'member';
ws.send(JSON.stringify(data));
}
//收到服务端发来的消息 触发 onmessage
ws.onmessage = function (e) {
var data = JSON.parse(e.data);
if (data.type == 'login') {
avatarRam = data.avatarRam
return ''
}
auto_chat(data)
}
}
//发送消息
function auto_chat(data) {
let html_other = `
<div class="layui-col-md12">
<div class="layui-row ">
<div class=" layui-col-xs1" style="text-align: left;">
<div class="head_icon">
<img src="/img/avatar${data.avatarRam}.png" alt=""
style="width: 100%;height: auto;display: inline-block;">
</div>
</div>
<div class=" layui-col-xs11">
<strong>游客${data.uid}</strong>
<span class="layui-font-green layui-font-16"> ${getCurrentTime()} </span>
<br>
<button class="layui-btn layui-btn-radius">${data.msg}</button>
</div>
</div>
</div>
`
let html_my = `
<div class="layui-col-md12">
<div class="layui-row">
<div class=" layui-col-xs11 " style="text-align: right;">
<span style="display: inline-block;" class="layui-font-green layui-font-16"> ${getCurrentTime()}
</span>
<br>
<button class="layui-btn layui-bg-blue layui-btn-radius">${data.msg}</button>
</div>
<div class="layui-col-xs1" style="text-align: right;">
<div class="head_icon " style="display: inline-block;">
<img src="/img/avatar${data.avatarRam}.png" alt=""
style="width: 100%;height: auto;">
</div>
</div>
</div>
</div>
`;
console.log(data);
console.log(data.uid);
//将信息添加到对应的框体内
if (data.uid === undefined) {
$('#contentor').append(html_my);
} else {
$('#contentor').append(html_other);
}
}
//获取当前时间
function getCurrentTime() {
const now = new Date();
const formattedTime = `${now.getFullYear()}-${('0' + (now.getMonth() + 1)).slice(-2)}-${('0' + now.getDate()).slice(-2)} ${('0' + now.getHours()).slice(-2)}:${('0' + now.getMinutes()).slice(-2)}:${('0' + now.getSeconds()).slice(-2)}`;
return formattedTime;
}
</script>
</body>
</html>复制代码
workerman 代码
<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/vendor/autoload.php';
// 注意:这里与上个例子不同,使用的是websocket协议
$ws_worker = new Worker("websocket://0.0.0.0:2000");
$global_uid = 0;
//有新的客户端与workman建立连接后
$ws_worker->onConnect = function (TcpConnection $connection) use (&$global_uid, $ws_worker) {
//用户id
$connection->uid = ++$global_uid;
//用户头像
$connection->avatarRam = mt_rand(0,5);
};
$ws_worker->onMessage = function (TcpConnection $connection, $data) use ($ws_worker) {
$data = json_decode($data,true);
$data['uid'] = $connection->uid;
$data['avatarRam'] = $connection->avatarRam;
//如果是login表示初次登录 返回 avatarRam 头像信息
if($data['type']=='login'){
$connection->send(json_encode($data));
}
foreach ($ws_worker->connections as $conn) {
//除了自身之外 其他人都发送
if ($connection->id != $conn->id) {
//返回的信息包含id和 头像 和 接受的msg
$conn->send(json_encode($data));
}
}
// $connection->send("游客{$connection->uid}:$data");
};
// 运行worker
Worker::runAll();复制代码
游客 客服聊天
大体框架
游客前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>客户端</title>
<link href="//unpkg.com/layui@2.8.17/dist/css/layui.css" rel="stylesheet">
<style>
.box1 {
margin: auto;
border: 1px solid red;
width: 800px;
height: 500px;
position: relative;
/* margin-top: 1%; */
/* float: left; */
}
.member-list {
float: left;
background-color: #dbe4ff;
width: 200px;
height: 100%;
display: inline;
}
.msg-container {
float: left;
width: 596px;
height: 100%;
border-color: black;
}
.msg-container .msg-list {
height: 400px;
width: 100%;
background-color: bisque;
}
.msg-container .msg-send {
height: 100px;
background-color: black;
}
.member-item {
width: 100%;
height: 50px;
font-size: 20px;
/* color:rgb(22, 186, 170); */
}
</style>
</head>
<body>
<div class="box1">
<div class="member-list layui-row"> </div>
<div class="msg-container">
<!-- 聊天区域 -->
<div class="msg-list ">
</div>
<div class="msg-send">
<div class="layui-row">
<div class="layui-col-xs10">
<textarea ></textarea>
</div>
<div class="layui-col-xs2">
<button type="button" id="send" onclick="sendmsg(this)" from_id="" class="layui-btn"
style="width: 100%;height: 100px;">发送</button>
</div>
</div>
</div>
</div>
</div>
<script src="//unpkg.com/layui@2.8.17/dist/layui.js"></script>
<script>
layui.use(['layer'], function () {
layer = layui.layer,
$ = layui.jquery;
//客服点击发送信息
// $('#send').click(function(){
// //获取当前回复的客服id
// from_id=parseInt($(this).attr('from_id'));
// if(isNaN(from_id )){
// //说明为空 没有选中
// layer.alert('请选择一个用户');
// return '';
// }
// });
})
//客服发送信息
function sendmsg(obj) {
touid = parseInt($(obj).attr('from_id'));
if (isNaN(touid)) {
//说明为空 没有选中
return layer.alert('请选择一个用户');
}
//发送信息
//获取信息
var data = {
type: 'msg',
group: 'admin',
touid: touid,
msg: $('textarea[]').val(),
}
if (data.msg.trim == '' || data.msg.length == 0) {
return layer.alert('信息不能为空', { icon: 0 })
}
ws.send(JSON.stringify(data));
$('textarea[]').val('');
auto_chat(data)
}
// 客户列表
var userList = [];
ws = new WebSocket('ws://127.0.0.1:2000')
//建立连接后触发 onopen时间
ws.onopen = function () {
let data = {};
//进行登录
data.type = 'login';
//用户标识 为客服
data.group = 'admin';
// 发送信息
ws.send(JSON.stringify(data));
init_load_user_list();
}
//接收到服务器发送的消息后
ws.onmessage = function (e) {
var data = JSON.parse(e.data);
//拉取客户列表
if (data.type == 'load_user_list') {
var uList = data.userlist;
$.each(uList, function (i, v) {
userList.push(v);
})
//构建客户列表
get_user_list();
return false;
}
//用用户退出
if (data.type == 'logout') {
// 判断是否在列表里面
var index = $.inArray(data.disc_id, userList);
if (index > -1) {
$('#' + data.disc_id).remove();
$('#member_' + data.disc_id).remove();
if ($('#send').attr('from_id') == data.disc_id) {
$('#send').attr('from_id', '')
}
}
}
//有新用户来
if (data.type == 'login') {
//如果没有找到 说明没有这个用户的信息
if ($.inArray(data.from_id, userList) == -1) {
userList.push(data.from_id)
}
get_user_list();
return;
}
//收到新的消息
if (data.type == 'msg') {
//将信息显示到对应的框里面
// var member_id = data.from_id;
// 获取到对应的对话框
// $('#member_'+member_id).
data.avatarRam = Math.ceil(5);
auto_chat(data)
}
}
//初始化拉取客户列表
function init_load_user_list() {
$data = {
type: 'load_user_list',
group: 'admin'
};
ws.send(JSON.stringify($data))
}
//部署客户端客户列表ui 并初始化对应的聊天框
function get_user_list() {
var html = '';
$.each(userList, function (i, v) {
html += `<div class="member-item layui-col-xs12 layui-btn layui-btn-primary layui-btn-fluid " style="margin:0" id="${v}" onclick="checkme(this)" member_id="${v}">客户${v}</div>`;
var htmlmsg = `<div id="member_${v}" class="layui-row" style="display: none;"> </div>`;
$('.msg-list').append(htmlmsg);
});
$('.member-list').html(html);
//
}
// 和单独某个用户聊天
function checkme(obj) {
$(obj).removeClass('layui-btn-primary').siblings('div').addClass('layui-btn-primary');
var member_id = $(obj).attr('member_id');
//创建对应的内容显示体
//如果等于0 说明不存在 进行创建 并且设置为显示
console.log($(`#member_${member_id}`).length);
if ($(`#member_${member_id}`).length <= 0) {
var htmlmsg = `<div id="member_${member_id}" class="layui-row" > <div>`;
$('.msg-list').append(htmlmsg);
}
$(`#member_${member_id}`).show().siblings().hide();
$('#send').attr('from_id', member_id);
}
//发送消息
function auto_chat(data) {
let html_other = `
<div class="layui-col-md12">
<div class="layui-row ">
<div class=" layui-col-xs1" style="text-align: left;">
<div class="head_icon">
<img src="/img/avatar${data.avatarRam}.png" alt=""
style="width: 100%;height: auto;display: inline-block;">
</div>
</div>
<div class=" layui-col-xs11">
<strong>用户${data.from_id}</strong>
<span class="layui-font-green layui-font-16"> ${getCurrentTime()} </span>
<br>
<button class="layui-btn layui-btn-radius">${data.msg}</button>
</div>
</div>
</div>
`
let html_my = `
<div class="layui-col-md12">
<div class="layui-row">
<div class=" layui-col-xs11 " style="text-align: right;">
<span style="display: inline-block;" class="layui-font-green layui-font-16"> ${getCurrentTime()}
</span>
<br>
<button class="layui-btn layui-bg-blue layui-btn-radius">${data.msg}</button>
</div>
<div class="layui-col-xs1" style="text-align: right;">
<div class="head_icon " style="display: inline-block;">
<img src="https://pic.qqtn.com/up/2017-12/15132234795879682.jpg" alt=""
style="width: 100%;height: auto;">
</div>
</div>
</div>
</div>
`;
//将信息添加到对应的框体内
//如果 group = admin 说明是发消息
if (data.group === 'admin') {
$('#member_' + data.touid + '').append(html_my);
} else {
//收到消息
$('#member_' + data.from_id + '').append(html_other);
}
}
//获取当前时间
function getCurrentTime() {
const now = new Date();
const formattedTime = `${now.getFullYear()}-${('0' + (now.getMonth() + 1)).slice(-2)}-${('0' + now.getDate()).slice(-2)} ${('0' + now.getHours()).slice(-2)}:${('0' + now.getMinutes()).slice(-2)}:${('0' + now.getSeconds()).slice(-2)}`;
return formattedTime;
}
</script>
</body>
</html>复制代码
后端 workerman
<?php
use Workerman\Worker;
use Workerman\Timer;
use Workerman\Connection\TcpConnection;
use WpOrg\Requests\Requests;
use think\facade\Db;
require_once __DIR__ . '/vendor/autoload.php';
$ws_workder = new Worker("websocket://127.0.0.1:2000");
//接受到信息
$ws_workder->onMessage = function (TcpConnection $connection, $data) use ($ws_workder) {
$data = json_decode($data, true);
//说明是初次登录 上线操作
if ($data['type'] == 'login') {
//设置分组
$connection->group = $data['group'];
//给链接对象添加属性 isreplied false
$connection->isreplied = false;
//当客户进来的时候
if ($connection->group == 'member') {
$serviceList = [];
foreach ($ws_workder->connections as $conn) {
// 找当前在线的客服
if ($conn->group == 'admin') {
$serviceList[] = $conn->id;
}
}
//如果当前客服有在线的
if (!empty($serviceList)) {
//随机取出一个客服的id
$connection->touid = $serviceList[array_rand($serviceList, 1)];
foreach ($ws_workder->connections as $conn) {
// 找到对应的客服id 准备接待
if ($connection->touid == $conn->id) {
$data['from_id'] = $connection->id;
$conn->send(json_encode($data));
$connection->isreplied = true;
}
}
}
}
}
//有新消息发送
if ($data['type'] == 'msg') {
//客户发送信息
if ($data['group'] == 'member') {
// 获取当前用户的id
foreach ($ws_workder->connections as $conn) {
//如果有客服并且客服在线
//如果用户的touid 等于 连接的id 说明匹配到了对应的客服
if ($conn->id == $connection->touid) {
$data['from_id'] = $connection->id;
//把消息客服发送消息
$conn->send(json_encode($data));
$posts = ['from_id' => $connection->id, 'to_id' => $connection->touid, 'msg' => $data['msg']];
//方案1 提交保存
$response=Requests::post('http://127.0.0.1:8000/admin/service/save_msg',data:$posts);
return ;
}
// 如果没有客服 还没有制作 可以直接保存发送的信息到数据库 等客服上线后发送给客服
}
}
//当客服发送信息
if ($data['group'] == 'admin') {
$touid = $data['touid'];
foreach ($ws_workder->connections as $con) {
if ($touid == $con->id) {
// 发送信息
$msgData=[
'type'=>'msg',
'from_id'=>$connection->id,
'msg'=>$data['msg'],
];
$con->send(json_encode($msgData));
$posts = ['from_id' => $connection->id, 'to_id' => $touid, 'msg' => $data['msg']];
//方案1 提交保存
// $response=Requests::post('http://127.0.0.1:8000/admin/service/save_msg',data:$posts);
//方案2 直接保存到数据库
// Db::table('msg')->save($posts);
return ;
}
}
}
}
// 客服发来消息,请求客户列表
if ($connection->group == 'admin' and $data['type'] == 'load_user_list') {
$userlist = [];
foreach ($ws_workder->connections as $conn) {
//如果这个人是客户 并且 没有客服对象
if ($conn->group == 'member' and !$conn->isreplied) {
$userlist[] = $conn->id;
$conn->isreplied = true;
$conn->touid = $connection->id;
}
}
$data['type'] = 'load_user_list';
$data['userlist'] = $userlist;
$connection->send(json_encode($data));
}
};
//连接断开
$ws_workder->onClose = function (TcpConnection $connection) use ($ws_workder) {
//客户断开连接 发送给对应的客服发送下线提醒
if ($connection->group == 'member') {
//遍历当前客户所属客服的id
foreach ($ws_workder->connections as $conn) {
//如果有分配客服
if (!empty($connection->touid) and $conn->id == $connection->touid) {
$data['type'] = 'logout';
$data['disc_id'] = $connection->id;
$conn->send(json_encode($data));
}
}
};
//客服 下线
if ($connection->group == 'admin') {
// 遍历出这位客服所管理的在线客户
foreach ($ws_workder->connections as $conn) {
//如果有分配客服
if ($conn->group == 'member' and $conn->touid == $connection->id) {
//将所管理的客户回归
$conn->isreplied = false;
}
}
};
};
//存储交流的信息
// 1 发送到tp服务器去存储
// 2. 直接在workerman中去请求mysql存储
// 运行worker
Worker::runAll();复制代码
客服代码
登录前端
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta >
<title>登录页面</title>
<!-- 请勿在项目正式环境中引用该 layui.css 地址 -->
<link href="//unpkg.com/layui@2.8.17/dist/css/layui.css" rel="stylesheet">
</head>
<body>
<style>
.demo-login-container {
width: 320px;
margin: 21px auto 0;
}
.demo-login-other .layui-icon {
position: relative;
display: inline-block;
margin: 0 2px;
top: 2px;
font-size: 26px;
}
.layui-panel {
height: 98vh;
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
/* 如果你也希望水平居中 */
}
.layui-panel>div {
width: 360px;
height: 330px;
border: 1px solid red;
}
</style>
<div class="layui-panel">
<div style="padding: 32px;">
<form class="layui-form">
<div class="demo-login-container">
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text"
lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password"
lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
</div>
</div>
<div class="layui-form-item">
<div class="layui-row">
<div class="layui-col-xs7">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-vercode"></i>
</div>
<input type="text" required|captcha"
placeholder="验证码" lay-reqtext="请填写验证码" autocomplete="off" class="layui-input"
lay-affix="clear">
</div>
</div>
<div class="layui-col-xs5">
<div style="margin-left: 10px;">
<img width="100%" src="{:captcha_src()}"
onclick="this.src='{:captcha_src()}?_='+ new Date().getTime();">
</div>
</div>
</div>
</div>
<div class="layui-form-item">
<input type="checkbox" >
<a href="#forget" style="float: right; margin-top: 7px;">忘记密码?</a>
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-login">登录</button>
</div>
<div class="layui-form-item demo-login-other">
<label>社交账号登录</label>
<span style="padding: 0 21px 0 6px;">
<a href="javascript:;"><i class="layui-icon layui-icon-login-qq"
style="color: #3492ed;"></i></a>
<a href="javascript:;"><i class="layui-icon layui-icon-login-wechat"
style="color: #4daf29;"></i></a>
<a href="javascript:;"><i class="layui-icon layui-icon-login-weibo"
style="color: #cf1900;"></i></a>
</span>
或 <a href="#reg">注册帐号</a>
</div>
</div>
</form>
</div>
</div>
<!-- 请勿在项目正式环境中引用该 layui.js 地址 -->
<script src="//unpkg.com/layui@2.8.17/dist/layui.js"></script>
<script>
layui.use(function () {
var form = layui.form;
var layer = layui.layer;
var $ = layui.jquery;
//自定义验证
form.verify({
captcha: function (value, elem) {
var msg = '';
// console.log(value);
if (value.length == 4) {
//如果为4发送ajax验证测试验证码是否通过
var obj = $.ajax({
url: '{:url("admin/login/captchaCheck")}',
method: 'post',
async: false,
data: { captcha: value },
})
obj.done((res) => {
if (res.code == 0) {
} else {
//没有通过验证
// return res.msg;
msg = res.msg;
}
// console.log(res);
})
obj.fail((err) => {
// console.log(err);
})
return msg;
} else {
return '长度不对'
}
}
})
// 提交事件
form.on('submit(demo-login)', function (data) {
var field = data.field; // 获取表单字段值
$.ajax({
url: '{:url(domain:true)}',
method: 'post',
data: field,
}).then((res) => {
if (res.code != 0) {
layer.msg(res.msg)
} else {
location.href = res.href
}
})
return false; // 阻止默认 form 跳转
});
});
</script>
</body>
</html>复制代码
后端登录校验
Login.php
<?php
declare(strict_types=1);
namespace app\admin\controller;
use app\BaseController;
class Login extends BaseController
{
function login()
{
if ($this->request->isGet()) {
return view('index/login');
} elseif ($this->request->isPost()) {
// return '12345';
$data = $this->request->post();
//进行登录验证
$this->validate($data, [
//校验规则
'username' => 'require',
'password' => 'require',
// 'captcha|验证码'=>'require|captcha'
], [
//校验校验失败返回
'username.require' => '用户名不能为空',
'password.require' => '密码不能为空',
// 'captcha.require'=>'验证码不能为空',
]);
//就不查询数据库处理了
$pwd = md5('123456');
// 直接判断
if ($data['username'] == 'admin' and md5($data['password']) == $pwd) {
//通过校验 跳转到控制页面
//让前端去跳转页面
//将用户登录信息写入到 缓存 或者session中去
$href = (string)url('admin/index/index');
session('user',$data['username']);
return json(['code' => 0, 'msg' => '登录成功', 'href' => $href]);
// return view('index/index');
}
//没有通过校验 返回错误信息
return json(['code' => 1001, 'msg' => '密码错误']);
}
}
function captchaCheck()
{
return json(['code' => 0, 'msg' => '通过']);
// return '12345';
//单独对验证码进行校验
//获取验证码内容
$captcha = $this->request->param('captcha');
if (!captcha_check($captcha)) {
//失败
return json(['code' => 1001, 'msg' => '验证码错误']);
} else {
return json(['code' => 0, 'msg' => '通过']);
}
}
}复制代码
客服页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/static/layui/css/layui.css">
<title>客户端聊天窗口</title>
<style type="text/css">
html,
body {
width: 98%;
height: 100%;
margin: 0 auto;
padding: 0px
}
.head_icon {
display: inline-block;
width: 50px;
height: 50px;
overflow: hidden;
border-radius: 20%;
}
#contentor{
overflow-y: auto; /* 垂直方向滚动 */
height: 500px; /* 高度自适应 */
width: 100%; /* 宽度自适应 */
}
</style>
</head>
<body>
<div class="layui-panel" >
<div class="layui-row layui-col-space32
" style="padding: 32px;">
<div class="layui-col-xs12 ">
<div style="border: 1px solid #f9f9f9;" id="contentor">
</div>
</div>
<div class="layui-col-md12">
<div class="layui-row">
<span class="layui-col-xs8">
<input type="text" >
</span>
<span class="layui-col-xs4">
<button type="button" class="layui-btn layui-bg-blue btn" style="width: 100%;">发送信息</button>
</span>
</div>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script type="text/javascript">
layui.use(function () {
$ = layui.jquery;
layer = layui.layer;
ws_connect()
send()
})
//发送消息
function send() {
var button = $('.btn'),
text = $('input[]');
//发送按钮点击后
button.click(function () {
//给框体里面添加对应的显示代码
// 获取输入框内容
if (text.val() === "") {
layer.msg('请输入内容')
} else {
data = {
group:'member',
type:'msg',
msg: text.val()
};
ws.send(JSON.stringify(data));
// data['avatarRam'] =avatarRam;
data['avatarRam'] = 1;
auto_chat(data);
//清空内容框
text.val('')
}
})
}
function ws_connect() {
ws = new WebSocket('ws://127.0.0.1:2000');
//当 websocket 创建成功后 触发onopen事件
ws.onopen = function () {
// auto_chat('你是零基础的吗','老手覅');
// setTimeout(()=>{auto_chat('同学你好','老手覅')}, 2000)
var data = {};
data.type = 'login';
//标识 客户还是客服
data.group = 'member';
ws.send(JSON.stringify(data));
}
//收到服务端发来的消息 触发 onmessage
ws.onmessage = function (e) {
var data = JSON.parse(e.data);
if (data.type == 'login') {
// avatarRam = data.avatarRam
avatarRam = 1
return ''
}
auto_chat(data)
}
}
//发送消息
function auto_chat(data) {
let html_other = `
<div class="layui-col-md12">
<div class="layui-row ">
<div class=" layui-col-xs1" style="text-align: left;">
<div class="head_icon">
<img src="https://pic.qqtn.com/up/2017-12/15132234795879682.jpg" alt=""
style="width: 100%;height: auto;display: inline-block;">
</div>
</div>
<div class=" layui-col-xs11">
<strong>游客${data.uid}</strong>
<span class="layui-font-green layui-font-16"> ${getCurrentTime()} </span>
<br>
<button class="layui-btn layui-btn-radius">${data.msg}</button>
</div>
</div>
</div>
`
let html_my = `
<div class="layui-col-md12">
<div class="layui-row">
<div class=" layui-col-xs11 " style="text-align: right;">
<span style="display: inline-block;" class="layui-font-green layui-font-16"> ${getCurrentTime()}
</span>
<br>
<button class="layui-btn layui-bg-blue layui-btn-radius">${data.msg}</button>
</div>
<div class="layui-col-xs1" style="text-align: right;">
<div class="head_icon " style="display: inline-block;">
<img src="/img/avatar${data.avatarRam}.png" alt=""
style="width: 100%;height: auto;">
</div>
</div>
</div>
</div>
`;
console.log(data);
console.log(data.uid);
//将信息添加到对应的框体内
if (data.group === 'member') {
$('#contentor').append(html_my);
} else {
$('#contentor').append(html_other);
}
}
//获取当前时间
function getCurrentTime() {
const now = new Date();
const formattedTime = `${now.getFullYear()}-${('0' + (now.getMonth() + 1)).slice(-2)}-${('0' + now.getDate()).slice(-2)} ${('0' + now.getHours()).slice(-2)}:${('0' + now.getMinutes()).slice(-2)}:${('0' + now.getSeconds()).slice(-2)}`;
return formattedTime;
}
</script>
</body>
</html>复制代码
简单总结
课程链接
用到的知识点
- php
- thinkphp
- wrokerman
- http request
- 前端
- jquery
- layui
存在问题
以游客的身份也无法进行鉴权
可以通过 $connection->getRemoteIp() 获得对方 ip 但是如果游客的 ip 也在变化就没啥用了
注意:onConnect 事件仅仅代表客户端与 Workerman 完成了 TCP 三次握手,这时客户端还没有发来任何数据,此时除了通过 $connection->getRemoteIp() 获得对方 ip,
可以通过隐藏对话框来模拟关闭
页面不够美观
代码
layer.open({
type: 2,
closeBtn: 0,
maxmin: true,
title: '聊天通信',
area: ['800px', '800px'],
// btn: ['发送'],
shade: 0,
content: '/index/index/chat',
//点击按钮后触发的函数
yes: function (index, layero) {
//获取到打开iframe对象
var iframeWin = window[layero.find('iframe')[0]['name']];
// console.log(iframeWin);
//调用对应的send方法
iframeWin.send();
}
})复制代码
原文地址 www.cnblogs.com