项目地址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

开启多应用

  1. 删除原始的 app/controller 目录
  2. 在项目跟目录下 使用命令 php think build admin 来创建应用
  3. 将全局的 configroute 复制一份到创建的应用里面
    • 开器多应用后全局的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 --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()}
captcha
//两种方式//校验验证码$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();            }        })