项目地址HTTPhttp协议
- 超文本传输协议
- 无状态协议
- 基于tcp协议的一个应用层的协议
- http是单向的,浏览器发起向服务器的连接,服务器预先并不知道
http协议工作过程
- 客户端和服务端建立连接(三次握手),http开始工作
- 建立连接后客户端发送给请求服务器
- 服务器接受到请求后,给予相应的响应信息
WebSoketwebsoket协议
- 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));}//收到服务端发来的消息 触发 onmessagews.onmessage = function (e) { var data = JSON.parse(e.data);}
Workerman基础
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文件
count = 4;// 接收到浏览器发送的数据时回复hello world给浏览器$http_worker->onMessage = function(TcpConnection $connection, Request $request){ // 向浏览器发送hello world $connection->send('hello world');};// 运行workerWorker::runAll();
实例二、使用WebSocket协议对外提供服务
创建ws_test.php文件
count = 4;// 当收到客户端发来的数据后返回hello $data给客户端$ws_worker->onMessage = function(TcpConnection $connection, $data){ // 向客户端发送hello $data $connection->send('hello ' . $data);};// 运行workerWorker::runAll();
测试
打开chrome浏览器,按F12打开调试控制台,在Console一栏输入(或者把下面代码放入到html页面用js运行)
// 假设服务端ip为127.0.0.1ws = 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 --plainphp 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
为验证码的配置文件
示例
{:captcha_img()}//两种方式//校验验证码$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));
即时通讯聊天系统简单的群聊功能前端页面
聊天框内容分析
前端代码
客户端聊天窗口 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%; /* 宽度自适应 */ } 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 = ` 游客${data.uid} ${getCurrentTime()}
` let html_my = ` ${getCurrentTime()}
`; 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; } workerman代码
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");};// 运行workerWorker::runAll();
游客 客服聊天
大体框架
游客前端代码
客户端 .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); */ } 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 += `客户${v}`; var htmlmsg = ` `; $('.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 = ` `; $('.msg-list').append(htmlmsg); } $(`#member_${member_id}`).show().siblings().hide(); $('#send').attr('from_id', member_id); } //发送消息 function auto_chat(data) { let html_other = ` 用户${data.from_id} ${getCurrentTime()}
` let html_my = ` ${getCurrentTime()}
`; //将信息添加到对应的框体内 //如果 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; } 后端workerman
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存储// 运行workerWorker::runAll();
客服代码登录前端
登录页面 .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; } 忘记密码? 或 注册帐号 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 跳转 }); }); 后端登录校验
Login.php
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' => '通过']); } }}
客服页面
客户端聊天窗口 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%; /* 宽度自适应 */ } 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 = ` 游客${data.uid} ${getCurrentTime()}
` let html_my = ` ${getCurrentTime()}
`; 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; } 简单总结课程链接
课程链接
课程链接
用到的知识点
- 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(); } })