依赖
<!-- socket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.3</version>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
配置类加入
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
java
基本无改动 复制粘贴用
package qiesiyv.ceshi.tool;
import com.google.gson.Gson;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/liaotian/{userId}")
@Component
public class WebSocketServer {
/**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/
private static int onlineCount = 0;
/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
private static ConcurrentHashMap<String,Session> sessions = new ConcurrentHashMap<>();
/**接收userId*/
private String userId="";
private String type ="";
private String touserid ="";
private String content ="";
private static Gson gson = new Gson();
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen( Session session, @PathParam("userId") String userId) {
sessions.put("sess"+userId,session);
this.userId= userId;//获取发送人id
System.out.println("用户"+userId+"加入WebSocketServer");
if(sessions.containsKey("sess"+userId)){
sessions.remove("sess"+userId);
sessions.put("sess"+userId,session);
}else{
sessions.put("sess"+userId,session);
//人数+1
addOnlineCount();
}
try {
Map<String,Object> map=new HashMap<>();
map.put("mag","连接成功");
sendMessage(userId,gson.toJson(map));
} catch (IOException e) {
System.out.println("对方网络异常!!!!!!");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if(sessions.containsKey("sess"+userId)){
sessions.remove("sess"+userId);
//人数-1
subOnlineCount();
}
System.out.println("用户"+userId+"退出,当前在线人数为:"+getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
//解析发送的报文
Map<String,String> xinxi=gson.fromJson(message,Map.class);
touserid =xinxi.get("toUserId");
content = xinxi.get("contentText");
System.out.println("用户消息:"+userId+",报文:"+message);
if(StringUtils.isNotBlank(message)){
try {
//执行发送操作
fasong(touserid,content);
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* @Author 翎墨袅
* @Description //TODO 发送消息
* @Param [error]
* @return void
**/
public void fasong(String touid,String neirong) throws Exception{
//传送给对应touid用户的websocket
if(StringUtils.isNotBlank(touid)&&sessions.containsKey("sess"+touid)){
sendMessage(touid,neirong);
//这里可以保存数据库
}else{
sendMessage(userId,"请求的userId:"+touid+"不在该服务器上");
//否则不在这个服务器上,进行mysql/redis保存
}
}
/**
*
* @param error
*/
@OnError
public void onError(Throwable error) {
System.out.println("用户错误:"+this.userId+",原因:"+error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public static void sendMessage(String buid,String message) throws IOException {
System.out.println("执行推送,被推送人id"+buid);
Session session=sessions.get("sess"+buid);
sessions.forEach((x,y)->{
System.out.println(x);
System.out.println(y);
});
System.out.println(session);
if (session!=null)
session.getBasicRemote().sendText(message);
}
//当前使用人数
public static synchronized int getOnlineCount() {
return onlineCount;
}
//当前使用人数+1
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
//当前使用人数-1
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
/**
* @Description: 数据推送,参数1目标uid,参数2推动的信息
* @Param:
* @return:
* @Author: 翎墨袅
* @Date: 2022/10/11
*///外部使用方法WebSocketServer.tuixinxi(对方id, 要传输的map数据);
public static boolean tuixinxi(String buid,Map xinxi){
try {
sendMessage(buid,gson.toJson(xinxi));
}catch (Exception e){
e.printStackTrace();
return false;
}
return true;
}
}
div
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>websocket通讯</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
var socket;
function openSocket() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
var socketUrl="ws://localhost:9091/liaotian/"+$("#userId").val();//如果是https则wss加域名
console.log(socketUrl);
if(socket!=null){
socket.close();
socket=null;
}
socket = new WebSocket(socketUrl);
//打开事件
socket.onopen = function(msg) {
console.log("websocket已打开",JSON.stringify(msg));
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现消息进入 开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function(close) {
console.log("websocket已关闭",JSON.stringify(close));
};
//发生了错误事件
socket.onerror = function(error) {
console.log("websocket发生了错误",JSON.stringify(error));
}
}
}
function sendMessage() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else {
console.log("您的浏览器支持WebSocket");
// socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'","type":"'+'"}');
socket.send(`{
"toUserId":"${$("#toUserId").val()}",
"contentText":"${$("#contentText").val()}"
}`)
}
}
</script>
<body>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="10"></div>
<p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="20"></div>
<p>【toUserId】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>【操作】:<div><a onclick="openSocket()">开启socket</a></div>
<p>【操作】:<div><a onclick="sendMessage()">发送消息</a></div>
</body>
</html>
uniapp(这部分自己看着改吧,业务逻辑啥的没删)
<template>
<view>
<view class="box-1" id="list-box" :style="'height:'+contentViewHeight+'px'">
<scroll-view id="scrollview" scroll-y="true" :scroll-top="scrollTop" @scroll="scroll" style="height: 100%;">
<view id="msglistview" class="talk-list">
<view v-for="(item,index) in talkList" :key="index">
<view class="item flex_col" :class=" item.userId != uid ? 'push':'pull' ">
<!-- <image :src="item.uidhead" mode="aspectFill" class="pic"></image> -->
<view class="pic">
{{item.uidname}}
</view>
<view class="content" v-if="item.type==1">{{item.contentText}}</view>
<view class="content" v-else-if="item.type==2">
<image :src="item.contentText" mode=""></image>
</view>
<view class="content" v-else>
<audio style="text-align: left" :src="item.contentText" name="语音" author="无" :action="audioAction" controls></audio>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="box-2">
<view class="flex_col">
<image @tap="up" src="../../static/imgs/add.png" mode="widthFix" class="addimg"></image>
<view class="flex_grow">
<input type="text" class="contentbox" v-model="content" placeholder="请输入聊天内容" placeholder-style="color:#DDD;"
:cursor-spacing="6">
</view>
<button class="send" @tap="send">发送</button>
</view>
<view class="imgs" v-if="ishidden">
<view class="photobox">
<image @tap="choosePhoto" src="../../static/imgs/photoimg.png"></image>
<view class="photoss">相册</view>
</view>
<jsfun-record class="photobox" voicePath="" maxTime="15" minTime="5" @okClick="saveRecord">
<image src="../../static/imgs/voice.png"></image>
<view class="photoss">录音</view>
</jsfun-record>
</view>
</view>
<uni-popup ref="popup" type="bottom">
</uni-popup>
</view>
</template>
<script>
import uniPopup from '@/components/uni-popup/uni-popup.vue'
import jsfunRecord from '@/components/jsfun-record/jsfun-record.vue'
export default {
components: {
jsfunRecord,
uniPopup
},
data() {
return {
audioAction: {
method: 'pause'
},
talkList: [],
content: '',
info: {},
myinfo: {},
ishidden: false,
scrollTop: 0,
old: {
scrollTop: 0
},
contentViewHeight: 0,
chatstate: false,
uid: 0,
timer: null,
}
},
onLoad(option) {
let that = this
that.uid = option.uid
uni.getSystemInfo({
success: function(res) { // res - 各种参数
that.contentViewHeight = res.windowHeight - uni.getSystemInfoSync().screenWidth / 750 * (120);
}
});
// 连接聊天服务器
this.openSocket()
// this.getHistoryMsg()
// 获取顶部信息
this.myUser()
},
methods: {
// 获取录音文件
saveRecord: function(recordPath) {
console.log("===音频文件地址:" + recordPath + "===")
//do... 可以使用 uni.uploadFile 接口上传到服务器
uni.uploadFile({
url: 'https://tjfx.breakday.cc/staff/upload',
filePath: recordPath,
name: 'file',
success: (uploadFileRes) => {
let obj = JSON.parse(uploadFileRes.data)
console.log(obj, 123456789)
if (obj.code == 1) {
let imgurl = obj.data.fileName
let obj = {
type: 2,
msg: imgurl,
toUid: this.uid,
fromUid: uni.getStorageSync("uid"),
toCompany: this.info.companyid,
fromCompany: uni.getStorageSync("companyid"),
fromImage: this.myinfo.headimage
}
// 发送聊天信息
uni.sendSocketMessage({
data: JSON.stringify(obj)
});
that.talkList.push(obj)
this.$forceUpdate()
this.scrollToBottom()
this.chatstate = true
this.ishidden = false
}
}
});
},
open() {
this.$refs.popup.open()
},
openSocket() {
let that = this
// 连接socket服务器
uni.connectSocket({
url: "wss://tjfx.breakday.cc/imserver/" + uni.getStorageSync('uid')
});
// 检测连接是否成功
uni.onSocketOpen(function(res) {
console.log('WebSocket连接已打开!');
});
// 获取服务器信息
uni.onSocketMessage(function(res) {
if (obj.code == 200) {} else {
let obj = JSON.parse(res.data)
console.log(obj)
that.talkList.push(obj)
that.$forceUpdate()
that.scrollToBottom()
that.chatstate = true
}
});
// 检测socket连接情况,断线重连
uni.onSocketError(function(res) {
console.log('WebSocket连接打开失败,请检查!');
uni.connectSocket({
url: "wss://tjfx.breakday.cc/imserver/" + uni.getStorageSync('uid')
});
});
// 监测socket是否挂关闭
uni.onSocketClose(function(res) {
console.log('WebSocket 已关闭!');
uni.connectSocket({
url: "wss://tjfx.breakday.cc/imserver/" + uni.getStorageSync('uid')
});
});
},
// 上传图片
choosePhoto: function() {
let that = this
uni.chooseImage({
success: (chooseImageRes) => {
const tempFilePaths = chooseImageRes.tempFilePaths;
console.log(tempFilePaths)
uni.uploadFile({
url: 'https://tjfx.breakday.cc/staff/upload',
filePath: tempFilePaths[0],
name: 'file',
success: (uploadFileRes) => {
let obj = JSON.parse(uploadFileRes.data)
if (obj.code == 1) {
let imgurl = obj.data.fileName
let obj = {
type: 2,
contentText: imgurl,
toUserId: "",
fromUserId: uni.getStorageSync("uid"),
uidname: that.myinfo.staffname,
uidhead: that.myinfo.staffimg,
}
// 发送聊天信息
uni.sendSocketMessage({
data: JSON.stringify(obj)
});
that.talkList.push(obj)
this.$forceUpdate()
this.scrollToBottom()
this.chatstate = true
this.ishidden = false
}
}
});
}
})
},
// 显示下方工具
up: function() {
this.ishidden = !this.ishidden;
},
// 获取平台消息
myUser() {
let that = this
that.$api.myselfinformation({
"id": uni.getStorageSync('uid')
}, {
'Content-Type': 'application/json;charset=UTF-8',
'token': uni.getStorageSync("token")
}).then((res) => {
this.myinfo = res.data.data.data;
console.log(this.myinfo)
}).catch((err) => {
console.log('失败', err)
})
},
// 获取历史消息
getHistoryMsg() {
let that = this
this.$api.getchatrecord({
// 传给后台的参数
"fromUid": uni.getStorageSync("uid"),
"toUid": that.uid
}, {
'Content-Type': 'application/json;charset=UTF-8',
'token': uni.getStorageSync("token")
}).then((res) => {
let arr = res.data.data.list
this.talkList = arr
this.$forceUpdate()
this.scrollToBottom()
this.chatstate = true
}).catch((error) => {
console.log(error)
})
},
// 滚动到底部
scrollToBottom(t) {
let that = this
uni.getSystemInfo({
success: function(res) { // res - 各种参数
let query = uni.createSelectorQuery()
query.select('#scrollview').boundingClientRect()
query.select('#msglistview').boundingClientRect()
query.exec((res) => {
let mainheight = res[1].height
that.old.scrollTop = mainheight
that.scrollTop = mainheight
that.gobottom()
})
}
});
},
gobottom(t) {
this.$nextTick(function() {
this.scrollTop = this.old.scrollTop + 520
});
this.scrollTop = 0
},
scroll(e) {
let that = this
if (that.chatstate == true) {
this.old.scrollTop = e.detail.scrollTop
that.chatstate = false
} else {
}
},
// 发送文本信息
send() {
let that = this
if (!this.content) {
uni.showToast({
title: '请输入有效的内容',
icon: 'none'
})
return;
}
// 发送聊天信息
let obj = {
type: 1,
contentText: this.content,
toUserId: "",
fromUserId: uni.getStorageSync("uid"),
uidname: that.myinfo.staffname,
uidhead: that.myinfo.staffimg,
}
uni.sendSocketMessage({
data: JSON.stringify(obj)
});
// 插入自己的聊天记录
this.talkList.push(obj)
// 置空输入框
this.content = '';
this.$forceUpdate()
this.scrollToBottom()
this.chatstate = true
}
}
}
</script>
<style lang="scss">
@import "../../lib/global.scss";
.status_bar {
height: var(--status-bar-height);
width: 100%;
}
page {
background-color: #F3F3F3;
font-size: 28rpx;
}
/* 加载数据提示 */
.tips {
position: fixed;
left: 0;
top: var(--window-top);
width: 100%;
z-index: 9;
background-color: rgba(0, 0, 0, 0.15);
height: 72rpx;
line-height: 72rpx;
transform: translateY(-80rpx);
transition: transform 0.3s ease-in-out 0s;
&.show {
transform: translateY(0);
}
}
.box-1 {
width: 100%;
// min-height: 100%;
padding-bottom: 100rpx;
box-sizing: content-box;
/* 兼容iPhoneX */
margin-bottom: 0;
margin-bottom: constant(safe-area-inset-bottom);
margin-bottom: env(safe-area-inset-bottom);
}
.box-2 {
position: fixed;
left: 0;
width: 100%;
bottom: 0;
height: auto;
z-index: 2;
border-top: #e5e5e5 solid 1px;
box-sizing: content-box;
background-color: #F3F3F3;
/* 兼容iPhoneX */
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
>view {
padding: 0 20rpx;
height: 100rpx;
}
.contentbox {
background-color: #fff;
height: 64rpx;
line-height: 30rpx;
padding: 0 20rpx;
border-radius: 32rpx;
font-size: 28rpx;
}
.send {
background-color: #42b983;
color: #fff;
height: 64rpx;
margin-left: 20rpx;
border-radius: 32rpx;
padding: 0;
width: 120rpx;
line-height: 62rpx;
&:active {
background-color: #5fc496;
}
}
}
.talk-list {
/* 消息项,基础类 */
.item {
padding: 20rpx 20rpx 0 20rpx;
align-items: flex-start;
align-content: flex-start;
color: #333;
.pic {
width: 92rpx;
height: 92rpx;
line-height: 92rpx;
text-align: center;
// border-radius: 50%;
// border: #fff solid 1px;
}
.content {
padding: 20rpx;
border-radius: 4px;
max-width: 500rpx;
word-break: break-all;
line-height: 52rpx;
position: relative;
}
/* 收到的消息 */
&.pull {
.content {
margin-left: 32rpx;
background-color: #fff;
&::after {
content: '';
display: block;
width: 0;
height: 0;
border-top: 16rpx solid transparent;
border-bottom: 16rpx solid transparent;
border-right: 20rpx solid #fff;
position: absolute;
top: 30rpx;
left: -18rpx;
}
}
}
/* 发出的消息 */
&.push {
/* 主轴为水平方向,起点在右端。使不修改DOM结构,也能改变元素排列顺序 */
flex-direction: row-reverse;
.content {
margin-right: 32rpx;
background-color: #a0e959;
&::after {
content: '';
display: block;
width: 0;
height: 0;
border-top: 16rpx solid transparent;
border-bottom: 16rpx solid transparent;
border-left: 20rpx solid #a0e959;
position: absolute;
top: 30rpx;
right: -18rpx;
}
}
}
}
}
.user {
position: fixed;
top: 0;
width: 750rpx;
height: 102rpx;
padding-top: var(--status-bar-height+50px);
display: flex;
align-items: center;
border-bottom: 10rpx solid #eee;
z-index: 10000;
}
.headPhoto,
.userInfo {
padding: 12rpx 0 12rpx 20rpx;
font-size: 24rpx;
}
.fcolor {
color: rgba(128, 128, 128, 1);
font-size: 20rpx;
}
.headPhoto image {
width: 78rpx;
height: 78rpx;
border-radius: 40rpx;
}
.imgs {
width: 750rpx;
min-height: 220rpx;
display: flex;
}
.imgs image {
width: 90rpx;
height: 90rpx;
margin: 0 auto 10rpx;
}
.photos {
display: flex;
min-height: 100rpx;
justify-content: space-between;
}
.photobox {
width: 130rpx;
height: 130rpx;
display: flex;
align-items: center;
flex-direction: column;
}
.photoss {
height: 30rpx;
color: rgba(80, 80, 80, 1);
font-size: 28rpx;
line-height: 30rpx;
text-align: center;
text-align: center;
}
.addimg {
width: 48rpx;
height: 48rpx;
margin-right: 5rpx;
}
.content image {
width: 164rpx;
height: 164rpx;
}
</style>