一、服务端 php
<?php
class Ws
{
private $ws = null;
public $redis;
public function __construct(Request $request = null){
$this->redis = new Redis();
$this->redis->connect('127.0.0.1','6379');
$this->redis->auth('认证密码');
$this->ws = new \Swoole\WebSocket\Server('0.0.0.0', 9501,SWOOLE_PROCESS,SWOOLE_SOCK_TCP | SWOOLE_SSL);
$this->ws->set([
'ssl_cert_file' => '/www/server/panel/vhost/cert/域名文件夹/fullchain.pem', // ssl证书
'ssl_key_file'=>'/www/server/panel/vhost/cert/域名文件夹/privkey.pem'
]);
$this->ws->on('open',[$this,'onOpen']);
$this->ws->on('message',[$this,"onMessage"]);
$this->ws->on('close',[$this,"onClose"]);
$this->ws->start();
}
public function onOpen($ws,$request) {
$data = [
'msg'=>'用户:'.$request->fd.'已连接',
'client_id'=>$request->fd,
'type'=>'connect'
];
$ws->push($request->fd,json_encode($data));
}
public function onMessage($ws,$frame) {
$json_data = $frame->data;
$data = json_decode($json_data,true);
$msg = [
'my' => $data['my'],
'to' => $data['to'],
'userName' => $data['myName'],
'userImg' => $data['myImg'],
'type' => $data['type'],
'client_id' => $frame->fd
];
switch ($data['type']) {
case 'open':
$data['msg'] = $data['myName'].'进来了';
$this->redis->set("user_id_".$data['my'],$frame->fd);
$this->setUnreadToRead($data['my'],$data['to']); // 将消息设为已读
break;
case 'send':
$msg['msg'] = $data['msg'];
$toUser = $this->redis->get($data['to']);
if(empty($toUser)) {
$this->checkUserReadable = false;
}else {
$ws->push($toUser, json_encode($msg,true)); // 推送给接收者
$this->checkUserReadable = true;
}
$ws->push($frame->fd, json_encode($msg,true)); // 推送给发送者
$this->setChatRecord($data['my'], $data['to'],$msg); // 保存聊天记录
break;
case 'leave':
$data['msg'] = $data['myName'].'退出去了';
break;
default:
break;
}
}
/*
* 将消息设为已读
* 当一个用户打开另一个用户的聊天框时,将所有未读消息设为已读
* 清楚未读消息中的缓存
* @from 消息发送者id
* @to 消息接受者id
* 返回值,成功将未读消息设为已读则返回true,没有未读消息则返回false
*/
public function setUnreadToRead($from, $to) {
$res = $this->redis->hDel('unread_' . $to, $from);
return (bool)$res;
}
/*
发送消息时保存聊天记录
* 这里用的redis存储是list数据类型
* 两个人的聊天用一个list保存
* @from 消息发送者id
* @to 消息接受者id
* @meassage 消息内容
*
* 返回值,当前聊天的总聊天记录数
*/
public function setChatRecord($from, $to, $message) {
$data = array('from' => $from, 'to' => $to, 'message' => $message, 'sent' => time()/*, 'recd' => 0*/);
$value = json_encode($data);
//生成json字符串
$keyName = 'rec:' . $this->getRecKeyName($from, $to);
$res = $this->redis->rPush($keyName, $value);
if (!$this->checkUserReadable) {//消息接受者无法立刻查看时,将消息设置为未读
$this->cacheUnreadMsg($from, $to);
}
return $res;
}
/*
* 当用户不在线时,或者当前没有立刻接收消息时,缓存未读消息,将未读消息的数目和发送者信息存到一个与接受者关联的hash数据中
* @from 发送消息的用户id
* @to 接收消息的用户id
* 返回值,当前两个用户聊天中的未读消息
*/
private function cacheUnreadMsg($from, $to) {
return $this->redis->hIncrBy('unread_' . $to, $from, 1);
}
/*
* 当用户上线时,或点开聊天框时,获取未读消息的数目
* @user 用户id
* 返回值,一个所有当前用户未读的消息的发送者和数组
* 数组格式为‘消息发送者id'=>‘未读消息数目'
*/
public function getUnreadMsgCount($user) {
return $this->redis->hGetAll('unread_' . $user);
}
/*
* 生成聊天记录的键名,即按大小规则将两个数字排序
* @from 消息发送者id
* @to 消息接受者id
*/
private function getRecKeyName($from, $to) {
return ($from > $to) ? $to . '_' . $from : $from . '_' . $to;
}
//监听WebSocket连接关闭事件。
public function onClose($ws, $fd) {
echo $fd.'链接断掉了';
}
}
new Ws();
二、小程序端代码
1、wxml代码
<view class="chat_list">
<scroll-view class="chat-content" scroll-y="true" scroll-into-view="{{scrollIntoView}}">
<view wx:for="{{messages}}" wx:key="{{index}}" class="message-item">
<view class="user_info my_info" wx:if="{{item.my==user_id}}">
<image src="{{item.userImg}}" class="head_img"></image>
<view class="my_msg msg">{{item.msg}}</view>
</view>
<view class="user_info love_info" wx:else>
<view class="love_msg msg">{{item.msg}}</view>
<image src="{{item.userImg}}" class="head_img"></image>
</view>
</view>
</scroll-view>
<view class="input-box">
<view class="inner-box flex align_center justify-between">
<view class="flex align_center">
<text class="iconfont icon-comment font-max font-c"></text>
<input class="font-min felx1" placeholder="请输入内容" bindinput='onInput' value="{{inputValue}}" />
</view>
<view class="my-btn-s flex center" bindtap='sendMessage'>发送</view>
</view>
<view style="height: 90rpx;"></view>
</view>
</view>
2、js代码
var _Util = require('../../utils/util.js');
var app = getApp();
var httpUrl = app.httpUrl;
Page({
data: {
inputValue: '', // 输入框的值
messages: [], // 聊天记录
scrollIntoView: '', // 滚动到指定位置
socketOpen: false, // WebSocket连接状态
socketMsgQueue: [], // WebSocket消息队列
url: 'wss://域名:9501', // WebSocket服务器地址
user_id: 0,
head_img: '',
client_id: '',
page:1
},
onLoad: function (options) {
var userinfo = wx.getStorageSync('userinfo');
var user_id = userinfo.id;
this.setData({
user_id: user_id
})
this.connectWebSocket() // 连接WebSocket服务器
},
onShow: function () {
// 加载之前聊过的记录
this.setData({
messages: []
})
this.getMessageList()
},
getMessageList: function () {
var that = this;
var user_id = that.data.user_id;
wx.request({
url: httpUrl + '/v1/chat/getChatRecordList',
data: {
uid: user_id,
page:that.data.page
},
method: 'POST',
header: {
'content-type': 'application/json', // 默认值
appKey: app.globalData.appKey,
appSecret: app.globalData.appSecret,
timestamp: _Util.$router.getThisTime()
},
success: res => {
if (res.data.code == '1000') {
var list = that.data.messages;
var data = res.data.data;
if (data.length != 0) {
for (var i = 0; i < data.length; i++) {
list.push(data[i])
}
that.data.page += 1;
that.setData({
messages: list
})
}
}
}
})
},
/**
* 连接WebSocket服务器
*/
connectWebSocket: function () {
var that = this
wx.connectSocket({
url: that.data.url,
success: function (res) {
console.log('WebSocket连接成功')
},
fail: function (res) {
console.log('WebSocket连接失败:', res)
}
})
wx.onSocketOpen(function (res) {
console.log('WebSocket连接已打开')
that.setData({
socketOpen: true
})
// 监听连接事件
var userinfo = wx.getStorageSync('userinfo');
let msg = {
'to': userinfo.loverid, //对方是谁
'my': userinfo.id, //我是谁
'myName': userinfo.nickname,
'myImg': userinfo.headimg,
'type': 'open' //当前状态
}
console.log('open:')
console.log(msg)
msg = JSON.stringify(msg)
that.sendSocketMessage(msg)
})
wx.onSocketError(function (res) {
console.log('WebSocket连接打开失败:', res)
})
wx.onSocketClose(function (res) {
console.log('WebSocket连接已关闭:', res)
that.setData({
socketOpen: false
})
})
// 接收服务器发送的额数据
wx.onSocketMessage(function (res) {
console.log('接收到服务器发送的数据:')
console.log(res.data)
var arr_data = JSON.parse(res.data)
console.log("arr_data:")
console.log(arr_data)
var messages = that.data.messages
console.log('arr_data.type:' + arr_data.type)
if (arr_data.type == 'connect') {
return;
// messages.push(arr_data.msg)
// that.setData({
// messages: messages,
// scrollIntoView: 'message-' + messages.length
// })
} else if (arr_data.type == 'downline') {
wx.showToast({
title: arr_data.msg,
icon: 'error',
duration: 1000
})
return;
} else {
}
messages.push(arr_data)
console.log('messages.length:' + messages.length)
that.setData({
messages: messages,
scrollIntoView: 'message-' + messages.length
})
})
},
/**
* 发送消息
*/
sendMessage: function () {
if (!this.data.socketOpen) {
wx.showToast({
title: 'WebSocket未连接1',
icon: 'none'
})
return
}
var message = this.data.inputValue
if (message == '') {
wx.showToast({
title: '消息不能为空',
icon: 'none'
})
return
}
var userinfo = wx.getStorageSync('userinfo');
let msg = {
'to': userinfo.loverid, //对方是谁
'my': userinfo.id, //我是谁
'myName': userinfo.nickname,
'myImg': userinfo.headimg,
'type': 'send', //当前状态
'msg': message
}
msg = JSON.stringify(msg)
this.sendSocketMessage(msg)
this.setData({
inputValue: ''
})
},
/**
* 发送WebSocket消息
*/
sendSocketMessage: function (message) {
console.log('socketOpen值为:' + this.data.socketOpen)
if (this.data.socketOpen) {
wx.sendSocketMessage({
data: message
})
} else {
wx.showToast({
title: 'WebSocket未连接2',
icon: 'none'
})
}
},
/**
* 监听输入框变化
*/
onInput: function (e) {
this.setData({
inputValue: e.detail.value
})
},
onHide: function () {},
onUnload: function () {
wx.closeSocket() // 关闭WebSocket连接
},
onPullDownRefresh: function () {},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
var that = this
setTimeout(function () {
that.getMessageList()
}, 1000)
},
})
3、css代码
page{
background: #F5F6F7;
}
.chat_list {
width:94%;
margin-top:20rpx;
padding-bottom:100rpx;
}
.user_info {
width:600rpx;
height:auto;
display: flex;
margin-top:20rpx;
position: relative;
align-items: center;
}
.my_info {
float: left;
margin-left:10rpx;
}
.love_info {
float:right;
}
.head_img {
width:100rpx;
height:100rpx;
border-radius: 50%;
border: 2rpx solid #ffffff;
margin-left:10rpx;
margin-top:16rpx;
}
.msg {
word-break: break-all;
width:500rpx;
height:auto;
line-height: auto;
padding:30rpx;
font-size: 32rpx;
border-radius:20rpx;
font-family: Arial, sans-serif; /* 使用 Arial 字体或者其他 sans-serif 字体 */
font-weight: normal; /* 设置字体粗细为普通 */
}
.my_msg {
margin-left:20rpx;
background: #ffffff;
border: 2rpx solid #ffffff;
color: #333; /* 设置字体颜色,可以根据需要进行调整 */
}
.love_msg {
margin-right:20rpx;
background: #6d72e8;
border: 2rpx solid #6d72e8;
color: #ffffff;
}
.input-box {
width: 100%;
}
.input-box .inner-box {
height: 90rpx;
width: 100%;
background: white;
padding: 0 20rpx;
position: fixed;
bottom: 0;
}
.input-box .iconfont {
padding-right: 10rpx;
}
.input-box input {
width: 530rpx;
}
.add-talk {
height: 80rpx;
width: 80rpx;
border-radius: 100rpx;
background: #F7C8D0;
position: fixed;
bottom: 280rpx;
right: 30rpx;
}
.end {
justify-content: flex-end;
margin-top: 10px;
}
.commentImg {
width: 18px;
height: 18px;
}
.comment {
margin-right: 10px;
}
.num {
margin-left: 4px;
}
.text-right {
justify-content: flex-end;
}
.delColor {
color: fuchsia;
}
4、php代码获取以往聊天记录
public function __construct(Request $request = null)
{
$this -> redis = new \Redis();
$this -> redis -> connect('127.0.0.1', '6379');
$this -> redis -> auth('11111');
parent::__construct($request);
}
public function getChatRecordList() {
$fromUser = $this->params['uid'];
$userModel = new UserModel();
$lover_no = $userModel->where('id',$fromUser)->value('lover_no');
$toUser = $userModel->where(['lover_no'=>$lover_no,'id'=>['neq',$fromUser]])->value('id');
$page = isset($this->params['page']) ? $this->params['page'] : 1;
$pageSize = 100;
if($page == 1) {
$start = 0;
$end = $pageSize - 1;
}else {
$start = ($page - 1) * $pageSize;
$end = $start + $pageSize - 1;
}
// page:1 start:0 end 7 page:2 start 8 end 15 page:3 start 16 end:23
$data = $this->getChatRecord($fromUser, $toUser, $start,$end);
$data_news = [];
if($data) {
foreach ($data as $k=>$val) {
$res = json_decode($val,true);
$data_news[$k]['my'] = $res['from'];
$data_news[$k]['to'] = $res['to'];
$data_news[$k]['userName'] = $res['message']['userName'];
$data_news[$k]['userImg'] = $res['message']['userImg'];
$data_news[$k]['type'] = $res['message']['type'];
$data_news[$k]['msg'] = $res['message']['msg'];
}
}
$this->result($data_news,'1000','获取成功','json');
}
/*
* 获取聊天记录
* @from 消息发送者id
* @to 消息接受者id
* @num 获取的数量
* 返回值,指定长度的包含聊天记录的数组
*/
public function getChatRecord($from, $to, $start,$num) {
$keyName = 'rec:' . $this->getRecKeyName($from, $to);
$recList = $this->redis->lRange($keyName, $start, (int)($num));
return $recList;
}