workerman即时通讯聊天系统 项目地址
项目地址
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
直接运行tp
php 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 name="viewport" content="width=device-width, initial-scale=1.0">
<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" name="send" placeholder="输入要发送的内容" class="layui-input">
</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[name="send"]');
//发送按钮点击后
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 name="viewport" content="width=device-width, initial-scale=1.0">
<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 name="desc" placeholder="多行文本框" class="layui-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[name="desc"]').val(),
}
if (data.msg.trim == '' || data.msg.length == 0) {
return layer.alert('信息不能为空', { icon: 0 })
}
ws.send(JSON.stringify(data));
$('textarea[name="desc"]').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 name="viewport" content="width=device-width, initial-scale=1">
<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" name="username" value="" lay-verify="required" placeholder="用户名"
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" name="password" value="" lay-verify="required" placeholder="密 码"
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" name="captcha" value="" lay-verify="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" name="remember" lay-skin="primary" title="记住密码">
<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 name="viewport" content="width=device-width, initial-scale=1.0">
<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" name="send" placeholder="输入要发送的内容" class="layui-input">
</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[name="send"]');
//发送按钮点击后
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();
}
})