laravel+GatewayWorker 完成IM即时通信以及文件互传功能(第五章:前后端代码开发+演示)
功能简介
本专题将带手把手带你搭建 仿某信的 即时通信功能 并完成文件互传
应用场景
本专题实际应用场景:
- 聊天客服:即时通信,消息实时互传,互相发送文字、语音消息以及文件;
- 小规模线上竞拍;
- 视频实时弹幕;
- 物联网;
- 以及其他与实时消息相关的功能;
专题章节
第四章:前端代码开发
一、所需页面
- 用户登录页面(注册页面:用户存在就登录,不能存在就自动注册)
- 用户好友列表页面
- 添加好友页面
- 用户一对一聊天页面
二、业务逻辑
- 用户登录→进入好友列表页面→添加好友→点击选择好友→进入好友一对一聊天页面→开始聊天→结束聊天End;
三、前端代码编写
相信大家的前端能力都不错,我在这里就不着重写css样式了,重点为大家演示功能实现
- 代码在下面,又臭又长估计你也不爱看;我简单说下前端逻辑;
- 用户进入页面直接连socket,连上了后端会给你返回client_id用户的临时设备唯一id
- 用户拿着后端给的临时设备id发ajax到后端进行我方用户id与临时设备id的绑定;
- 绑定好了就可以进行消息发送了
- 字符串发送接直接把内容和收件人id发送到后端,后端自行调用GatewayWorker发送消息;
- 发送文件和发送文字消息一样,文件的本质其实也是一堆字符串,前端把文件转base64成一堆字符串发给后端,后端当字符串在给你转发出去,等到客户端接受到base64后直接还原成文件就好了;
- 前端核心代码
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="{{asset('layui-v2.6.8/layui/css/layui.css')}}">
<script src="{{asset('js/jquery.min.js')}}"></script>
<script src="{{asset('layui-v2.6.8/layui/layui.js')}}"></script>
<link rel="stylesheet" href="{{asset('css/im_chat.css')}}">
<title>聊天页面</title>
</head>
<body>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
<legend>聊天页面</legend>
</fieldset>
<main>
<section class="message_box">
{{--
<div class="left">
<img class="header_img" src="{{asset('images/linux.png')}}" alt="">
<div class="content_left">
<p>对方方对方</p>
</div>
</div>
<div class="right">
<div class="content_right">
<p>我方我方</p>
</div>
<img class="header_img" src="{{asset('images/php.png')}}" alt="">
</div>
<div class="left">
<img class="header_img" src="{{asset('images/linux.png')}}" alt="">
<div class="content_left">
<img src="{{asset('images/demo1.jpg')}}" alt="">
</div>
</div>
<div class="right">
<div class="content_right">
<img src="{{asset('images/demo1.jpg')}}" alt="">
</div>
<img class="header_img" src="{{asset('images/php.png')}}" alt="">
</div>
--}}
</section>
</main>
<section class="import_box">
<div id="string">
<a href="javascript:;" onclick="transitionToggle()">+</a>
<textarea name="" id="string_msg_val" cols="30" rows="5" required placeholder="请输入聊天内容"></textarea>
<button type="button" onclick="sendStringMsg()">发送</button>
</div>
<div id="file_box" style="display: none">
<a href="javascript:;" onclick="transitionToggle()">←</a>
<input type="file" id="file_input" onchange="filechange('file_input')">
<input type="hidden" id="base64_value">
<button type="button" onclick="sendImgMsg()">发送</button>
</div>
</section>
</body>
<script>
/**
* 与GatewayWorker建立websocket连接,域名和端口改为你实际的域名端口,
* 其中端口为Gateway端口,即start_gateway.php指定的端口。
* start_gateway.php 中需要指定websocket协议,像这样
* $gateway = new Gateway(websocket://0.0.0.0:8282);
*/
ws = new WebSocket("ws://im.liutong.pro:8282");//这里的端口要与.\app\GatewayWorker\Applications\YourApp\start_gateway.php中$gateway = new Gateway("websocket://0.0.0.0:8282");的端口保持一致;
//检测websocket是否链接
ws.onopen = function () {
console.log("连接成功");
};
// 关闭 websocket
ws.onclose = function () {
console.log("连接已关闭...");
};
// 服务端主动推送消息时会触发这里的onmessage
ws.onmessage = function(e){
// json数据转换成js对象
var data = eval("("+e.data+")");
var type = data.type || '';
console.log(type);
switch(type){
// Events.php中返回的init类型的消息,将client_id发给后台进行uid绑定
case 'init'://监听初始化
console.log(data);
// 利用jquery发起ajax请求,将client_id发给后端进行uid绑定
binding(data.client_id);
break;
case 'binding_success'://监听绑定成功信息
console.log(data);
break;
case 'string'://监听收到的文字信息
console.log(data);
if(data.addresser_id != "{{$friend_id}}"){
return false;
}
//将文字信息渲染到视图上
let _html = "<div class=\"left\">\n" +
" <img class=\"header_img\" src=\"{{asset('images/linux.png')}}\" alt=\"\">\n" +
" <div class=\"content_left\">\n" +
" <p>"+ data.msg +"</p>\n" +
" </div>\n" +
" </div>";
$('.message_box').append(_html);//将新的文字信息追加到模板上
break;
case 'img'://监听收到的图片信息
console.log(data);
if(data.addresser_id != "{{$friend_id}}"){
return false;
}
let _img_html = "<div class=\"left\">\n" +
" <img class=\"header_img\" src=\"{{asset('images/linux.png')}}\" alt=\"\">\n" +
" <div class=\"content_left\">\n" +
" <img src=" + data.msg + ">\n" +
" </div>\n" +
" </div>";
$('.message_box').append(_img_html);//将新的文字信息追加到模板上
break;
default :
alert(e.data);
}
};
//切换输入框
function transitionToggle(){
$('#string').toggle();
$('#file_box').toggle();
}
//绑定uid
function binding(client_id){
//绑定业务逻辑
$.ajax({
type : "POST", //提交方式
url : "{{route('im.binding')}}",//路径
data : {
'_token':'{{csrf_token()}}'//laravel 防止ajax POST请求报419错误方法
,'user_id' : "{{session('user')['id']}}"
,'client_id' :client_id
},//数据,这里使用的是Json格式进行传输
success : function(result) {//返回数据根据结果进行相应的处理
var data = JSON.parse(result);//接受到服务器给我返回的JSON数据并进行解析
if (data.success) {
console.log('绑定结果:' + data.msg);
} else {
alert('绑定失败');
}
}
});
}
//发送文字消息
function sendStringMsg() {
let msg = $('#string_msg_val').val();
if(!msg){
layer.msg('请输入内容', {time: 5000, icon: 2});
return false;
}
$.ajax({
type : "POST", //提交方式
url : "{{route('im.sendMessage')}}",//路径
data : {
'_token':'{{csrf_token()}}'//laravel 防止ajax POST请求报419错误方法
,'recipients_id' :"{{$friend_id}}"//好友id
,'type':'string'
,'msg':msg
},//数据,这里使用的是Json格式进行传输
success : function(result) {//返回数据根据结果进行相应的处理
var data = JSON.parse(result);//接受到服务器给我返回的JSON数据并进行解析
if (data.success) {
console.log('文字发送结果:' + data.msg);
//将发送的信息显示到html中
let _string = " <div class=\"right\">\n" +
" <div class=\"content_right\">\n" +
" <p>"+ data.msg +"</p>\n" +
" </div>\n" +
" <img class=\"header_img\" src=\"{{asset('images/php.png')}}\" alt=\"\">\n" +
" </div>";
$('.message_box').append(_string);//将新的文字信息追加到模板上
$('#string_msg_val').val('');//清理输入框
} else {
layer.msg('发送失败', {time: 5000, icon: 2});
}
}
});
}
//发送图片文件
function sendImgMsg(){
let msg = $('#base64_value').val();//获取base64发送给php接口
$.ajax({
type : "POST", //提交方式
url : "{{route('im.sendMessage')}}",//路径
data : {
'_token':'{{csrf_token()}}'//laravel 防止ajax POST请求报419错误方法
,'recipients_id' :"{{$friend_id}}"//好友id
,'type':'img'
,'msg':msg
},//数据,这里使用的是Json格式进行传输
success : function(result) {//返回数据根据结果进行相应的处理
var data = JSON.parse(result);//接受到服务器给我返回的JSON数据并进行解析
if (data.success) {
console.log('文字发送结果:' + data.msg);
//将发送的信息显示到html中
let _string = "<div class=\"right\">\n" +
" <div class=\"content_right\">\n" +
" <img src="+data.msg+" alt=\"\">\n" +
" </div>\n" +
" <img class=\"header_img\" src=\"{{asset('images/php.png')}}\" alt=\"\">\n" +
" </div>";
$('.message_box').append(_string);//将新的文字信息追加到模板上
$('#string_msg_val').val('');//清理输入框
} else {
layer.msg('发送失败', {time: 5000, icon: 2});
}
}
});
}
/**
* 获取文件base64
*/
function filechange(_id_name) {
var selectFile = document.getElementById(_id_name);
var filePath = selectFile.value;//文件路径
var fileName = selectFile.files[0].name;//上传的文件名称
var file = selectFile.files[0];//上传的文件
var extn = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();//文件后缀
//文件base64string获取
if (window.FileReader) {
var reader = new FileReader();
reader.readAsDataURL(file);
//监听文件读取结束后事件
reader.onloadend = function (e) {
var base64String=e.target.result;
$('#base64_value').val(base64String);
};
}
}
</script>
</html>
- 路由文件routes\web.php
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
//加载登录页面
Route::get('login','LoginController@index');
//登录+注册处理
Route::post('login/loginOperation','LoginController@loginOperation')->name('login.loginOperation');
//需要中间件验证的路由
Route::middleware(['im_auth'])->group(function () {
//退出登录
Route::post('login/loginOut','LoginController@loginOut')->name('login.loginOut');
//加载im用户好友列表
Route::get('im/indexList', 'ImController@indexList')->name('im.indexList');
//添加好友
Route::post('im/addFriends', 'ImController@addFriends')->name('im.addFriends');
//删除好友
Route::post('im/deleteFriends/{friend_lists_id}', 'ImController@deleteFriends')->name('im.deleteFriends');
//加载im用户聊天页面
Route::get('im/chat/{friend_id}', 'ImController@chat')->name('im.chat');
//绑定用户
Route::post('im/binding', 'ImController@binding')->name('im.binding');
//发送消息
Route::post('im/sendMessage', 'ImController@sendMessage')->name('im.sendMessage');
});
- 数据库表结构文件
1.im用户表 database/migrations/2021_11_29_093718_create_im_users_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateImUsersTable extends Migration
{
/**
* im用户信息表
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('im_users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
$table->char('tel',15)->unique()->comment('手机号|唯一索引');
$table->string('password');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('im_users');
}
}
2.好友关系表 database/migrations/2021_11_30_033101_create_friend_lists_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateFriendListsTable extends Migration
{
/**
* 好友列表
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('friend_lists', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
$table->integer('user_id')->comment('用户id');
$table->integer('friend_id')->comment('好友id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('friend_lists');
}
}
- 后端业务逻辑控制器
<?php
namespace App\Http\Controllers;
// 自动加载类
require_once __DIR__ .'/../../GatewayWorker/vendor/autoload.php';//引入绝对路径上的GatewayWorker组件上的自动加载类,不然回报找不到"GatewayClient\Gateway"的错误,或者你也可以在项目更目录composer安装"GatewayClient\Gateway"这个
use App\FriendList;
use App\ImUser;
use GatewayClient\Gateway;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Im控制器
* Class ImController
* @package App\Http\Controllers
*/
class ImController extends Controller
{
/**
* 好友列表
*/
public function indexList()
{
$user = session('user');
$data = FriendList::where('user_id', $user['id'])->paginate(20);
return view('im.indexList', compact('data'));
}
/**
* 添加好友
* @param Request $request
*/
public function addFriends(Request $request)
{
$friend_id = ImUser::where('tel', $request->tel)->value('id');
$user = session('user');
if ($friend_id) {
//对方id存在
$is_friend = FriendList::where([
['user_id', $user['id']],
['friend_id', $friend_id],
])->exists();
if(!$is_friend){
FriendList::create([
'user_id'=>$user['id'],
'friend_id'=>$friend_id,
]);
FriendList::create([
'user_id'=>$friend_id,
'friend_id'=>$user['id'],
]);
return redirect()->route('im.indexList');
}
}
dd('对方手机号有误');
}
/**
* 删除好友
* @param $id
*/
public function deleteFriends($friend_lists_id){
$friendList = FriendList::where('id',$friend_lists_id)->first();
DB::beginTransaction();
try{
$del_1 = FriendList::where([
['user_id',$friendList['user_id']],
['friend_id',$friendList['friend_id']]
])->delete();
$del_2 = FriendList::where([
['user_id',$friendList['friend_id']],
['friend_id',$friendList['user_id']]
])->delete();
DB::commit();
}catch (\Exception $exception){
DB::rollBack();
dd('删除失败:' . $exception->getMessage());
}
if($del_1 && $del_2){
return redirect()->back();
}
dd('删除失败');
}
/**
* 聊天页面
* @param Request $request
* @param $friend_id 好友id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function chat(Request $request, $friend_id)
{
return view('im.chat',compact('friend_id'));
}
/**
* 绑定用户
* @param Request $request
*/
public function binding(Request $request){
$client_id = $request->client_id;//GatewayWorker 给我们分配的连接id
$user = session('user');
$uid = $user['id'];//我在我方数据库IM用户表中的主键id
Gateway::bindUid($client_id,$uid);
$message = [
'type' => 'binding_success',
'msg' => '绑定成功'
];
Gateway::$registerAddress = '127.0.0.1:1238';
Gateway::sendToUid($uid, json_encode($message));//向任意uid的网站页面发送数据
$message['code'] = 200;
$message['success'] = 1;//1:成功,0:失败
return json_encode($message);
}
/**
* 发送消息
* @param Request $request->recipients_id 收件人id,在我方friend_lists好友表中的friend_id字段值
* @param Request $request->type 信息类型 string:文字信息,img:图片
* @param Request $request->msg 信息内容
*/
public function sendMessage(Request $request){
$post = $request->all();
$user = session('user');
$user_id = $user['id'];
$message = [
'type' => $request->type,
'addresser_id' => $user_id,//发件人id
'recipients_id' => $post['recipients_id'],//收件人id
'msg' => $post['msg'],
];
//检测是不是文件,是的话给文件一个随机生成的文件id,方便前端操作
switch ($request->type){
case 'img':
$message['file_id'] = time() . rand(10,99);//文件id
break;
default:
$message['file_id'] = '';
break;
}
Gateway::$registerAddress = '127.0.0.1:1238';
Gateway::sendToUid($post['recipients_id'], json_encode($message));//给发件人的uid网站页面发送数据
$message['code'] = 200;
$message['success'] = 1;//1:成功,0:失败
return json_encode($message);
}
}
注意:后端业逻辑控制器要是报错说:"找不到GatewayClient\Gateway的错误"记得要在php代码顶部使用require_once 进行引入;
要是引入了还是找不到:那一定是 项目根目录\app\GatewayWorker里面没有composer require workerman/gatewayclient
切仅目录安装已就好了;
代码我传到gitee(码云)上面啦!!!
https://gitee.com/liutong199309/php-laravel–gateway-worker
- laravel框架中我改动了这些文件
图1:
图2:
- 项目体验地址:http://im.liutong.pro/login