WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議。WebSocket通信協(xié)議于2011年被IETF定為標(biāo)準(zhǔn)RFC 6455,并由RFC7936補(bǔ)充規(guī)范。WebSocket API也被W3C定為標(biāo)準(zhǔn)。
WebSocket使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
廢話不多說(shuō):上才藝 ^_^
要實(shí)現(xiàn)聊天記錄的保存就要?jiǎng)?chuàng)建聊天記錄表
?建表語(yǔ)句
DROP TABLE IF EXISTS `user_message`;
CREATE TABLE `user_message` (
`from_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '發(fā)送人',
`message` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '消息',
`to_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '接收人',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '日期'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
java引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
配置類(lèi)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @authoer:majinzhong
* @Date: 2022/11/7
* @description:
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
controller類(lèi)
import com.shangfei.pojo.socket.SocketMsg;
import com.shangfei.response.WebResponse;
import com.shangfei.service.socket.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
/**
* @authoer:majinzhong
* @Date: 2022/11/7
* @Description: websocket的具體實(shí)現(xiàn)類(lèi)
* 使用springboot的唯一區(qū)別是要@Component聲明下,而使用獨(dú)立容器是由容器自己管理websocket的,
* 但在springboot中連容器都是spring管理的。
* 雖然@Component默認(rèn)是單例模式的,但springboot還是會(huì)為每個(gè)websocket連接初始化一個(gè)bean,
* 所以可以用一個(gè)靜態(tài)set保存起來(lái)。
*/
@RestController
public class WebSocketController {
@Autowired
WebSocketService webSocketService;
/**
* 打開(kāi)發(fā)送消息頁(yè)面
* @return
*/
@RequestMapping("/webSocketPage")
public ModelAndView page(){
return new ModelAndView("index");
}
/**
* 獲得在線人信息
* @return
*/
@RequestMapping("/members")
public WebResponse members(){
return webSocketService.members();
}
/**
* 查詢聊天記錄
* @return
*/
@RequestMapping("/message")
public WebResponse message(@RequestBody SocketMsg socketMsg){
return webSocketService.message(socketMsg);
}
}
service類(lèi)
import cn.hutool.core.collection.CollectionUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.shangfei.mapper.plan.UserMapper;
import com.shangfei.mapper.socket.UserMessageMapper;
import com.shangfei.pojo.socket.SocketMsg;
import com.shangfei.pojo.socket.UserMessage;
import com.shangfei.response.WebResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.CrossOrigin;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @authoer:majinzhong
* @Date: 2022/11/16
* @description:
*/
@Component
@ServerEndpoint(value = "/websocket/{nickname}")
@CrossOrigin
@Service
public class WebSocketService {
@Autowired
UserMapper userMapper;
private static UserMessageMapper userMessageMapper;
@Autowired
public void setLocationMapper(UserMessageMapper userMessageMapper) {
WebSocketService.userMessageMapper = userMessageMapper;
}
/**
* 新建list集合存儲(chǔ)數(shù)據(jù)
*/
private static List<UserMessage> messageList = new ArrayList<>();
/**
* 設(shè)置一次性存儲(chǔ)數(shù)據(jù)的list的長(zhǎng)度為固定值,每當(dāng)list的長(zhǎng)度達(dá)到固定值時(shí),向數(shù)據(jù)庫(kù)存儲(chǔ)一次
*/
private static final Integer LIST_SIZE = 5;
/**
* 用來(lái)存放每個(gè)客戶端對(duì)應(yīng)的MyWebSocket對(duì)象。
**/
private static CopyOnWriteArraySet<WebSocketService> webSocketSet = new CopyOnWriteArraySet<>();
/**
* 與某個(gè)客戶端的連接會(huì)話,需要通過(guò)它來(lái)給客戶端發(fā)送數(shù)據(jù)
**/
private Session session;
/**
* 用戶名稱
**/
private String nickname;
/**
* 用來(lái)記錄sessionId和該session進(jìn)行綁定
**/
private static Map<String,Session> map = new HashMap<String, Session>();
/**
* 查詢當(dāng)前在線人
* @return
*/
public WebResponse members(){
Set<String> members = map.keySet();
if(!CollectionUtil.isEmpty(members)) {
List<Map<String,String>> userList=new ArrayList<>();
for(String member:members){
//通過(guò)工號(hào)查詢名字
Map<String, String> user = userMapper.selectName(member);
if(!CollectionUtil.isEmpty(user)){
userList.add(user);
}else{
Map<String, String> userMap = new HashMap<>();
userMap.put("username",member);
userMap.put("name","");
userList.add(userMap);
}
}
return WebResponse.success(userList);
}else{
return WebResponse.success(members);
}
}
/**
* 查詢聊天記錄
* @param socketMsg
* @return
*/
public WebResponse message(SocketMsg socketMsg) {
List<UserMessage> userMessageList=userMessageMapper.selectByType(socketMsg);
return WebResponse.success(userMessageList);
}
/**
* 連接建立成功調(diào)用的方法
*/
@OnOpen
public void onOpen(Session session,@PathParam("nickname") String nickname) {
this.session = session;
this.nickname=nickname;
map.put(nickname, session);
webSocketSet.add(this);
System.out.println("有新連接加入:"+nickname+",當(dāng)前在線人數(shù)為" + webSocketSet.size());
broadcast("人員更新");
}
/**
* 連接關(guān)閉調(diào)用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
List<String> nickname = this.session.getRequestParameterMap().get("nickname");
for(String nick:nickname) {
map.remove(nick);
}
//當(dāng)有人退出連接時(shí),將集合里的信息保存到數(shù)據(jù)庫(kù)
if(messageList.size()>0){
userMessageMapper.insertMessageList(messageList);
//清除集合
messageList.clear();
}
broadcast("人員更新");
System.out.println("有一連接關(guān)閉!當(dāng)前在線人數(shù)為" + webSocketSet.size());
}
/**
* 收到客戶端消息后調(diào)用的方法
*/
@OnMessage
public void onMessage(String message, Session session,@PathParam("nickname") String nickname) {
System.out.println("來(lái)自客戶端的消息-->"+nickname+": " + message);
//從客戶端傳過(guò)來(lái)的數(shù)據(jù)是json數(shù)據(jù),所以這里使用jackson進(jìn)行轉(zhuǎn)換為SocketMsg對(duì)象,
// 然后通過(guò)socketMsg的type進(jìn)行判斷是單聊還是群聊,進(jìn)行相應(yīng)的處理:
ObjectMapper objectMapper = new ObjectMapper();
SocketMsg socketMsg;
try {
socketMsg = objectMapper.readValue(message, SocketMsg.class);
//將聊天記錄保存到數(shù)據(jù)庫(kù)
UserMessage userMessage = new UserMessage();
userMessage.setFromName(nickname);
if(socketMsg.getType() == 1){
userMessage.setToName(socketMsg.getToUser());
}else{
userMessage.setToName("all");
}
userMessage.setMessage(socketMsg.getMsg());
userMessage.setCreateTime(new Date());
messageList.add(userMessage);
//當(dāng)集合里的數(shù)據(jù)已經(jīng)達(dá)到最大值,就將信息進(jìn)行保存
if(messageList.size()==LIST_SIZE){
userMessageMapper.insertMessageList(messageList);
//清除集合
messageList.clear();
}
//將賬號(hào)換成名字
String name=userMessageMapper.getName(nickname);
if(socketMsg.getType() == 1){
//單聊.需要找到發(fā)送者和接受者.
socketMsg.setFromUser(nickname);
Session fromSession = map.get(socketMsg.getFromUser());
Session toSession = map.get(socketMsg.getToUser());
//將賬號(hào)換成名字
String toName=userMessageMapper.getName(toSession.getPathParameters().get("nickname"));
//發(fā)送給接受者.
if(toSession != null){
//發(fā)送給發(fā)送者.
fromSession.getAsyncRemote().sendText(name+"->"+toName+":"+socketMsg.getMsg());
toSession.getAsyncRemote().sendText(name+"->"+toName+":"+socketMsg.getMsg());
}else{
//發(fā)送給發(fā)送者.
fromSession.getAsyncRemote().sendText("系統(tǒng)消息:對(duì)方不在線或者您輸入的頻道號(hào)不對(duì)");
}
}else{
//群發(fā)消息
broadcast(name+": "+socketMsg.getMsg());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 發(fā)生錯(cuò)誤時(shí)調(diào)用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("發(fā)生錯(cuò)誤");
error.printStackTrace();
}
/**
* 群發(fā)自定義消息
*/
public void broadcast(String message) {
for (WebSocketService item : webSocketSet) {
/**
* 同步異步說(shuō)明參考:http://blog.csdn.net/who_is_xiaoming/article/details/53287691
*
* this.session.getBasicRemote().sendText(message);
**/
item.session.getAsyncRemote().sendText(message);
}
}
}
實(shí)體類(lèi)
import lombok.Data;
/**
* @authoer:majinzhong
* @Date: 2022/11/7
* @description:
*/
@Data
public class SocketMsg {
/**
* 聊天類(lèi)型0:群聊,1:?jiǎn)瘟? **/
private int type;
/**
* 發(fā)送者
**/
private String fromUser;
/**
* 接受者
**/
private String toUser;
/**
* 消息
**/
private String msg;
}
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
public class UserMessage {
private String fromName;
private String message;
private String toName;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date createTime;
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName == null ? null : fromName.trim();
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message == null ? null : message.trim();
}
public String getToName() {
return toName;
}
public void setToName(String toName) {
this.toName = toName == null ? null : toName.trim();
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
Vue代碼
<template>
<el-dialog
v-model="dialogVisible"
title="聊天"
width="70%"
@close="closeWebSocket"
>
<div class="chat-big-box">
<div class="chat-left">
<el-collapse v-model="activeNames">
<el-collapse-item name="1" >
<template #title>
<div class="online-personnel user-select">
<div>在線人員</div>
<div class="sum">{{userArr.length>0?userArr.length-1:0}}人</div>
</div>
</template>
<div class="chat-user-name user-select"
v-for="(item,index) in userArr" :key="item.username"
v-show="index>0"
@click="checkUser(item)"
:class="{'user-name-check':item.isCheck}">
<div>員工名稱:</div>
<div>{{item.name}}</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
<div class="chat-right">
<div class="content" ref="smgContent">
<div v-for="item in msgArr" :key="item">{{item}}</div>
</div>
<div class="input-box">
<el-input
v-model="textarea"
@keydown.enter="keydown"
:autosize="{ minRows: 7, maxRows: 7 }"
type="textarea"/>
<el-button type="success" size="large" plain @click="sendBtn" class="input-btn">發(fā)送</el-button>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref, defineExpose, nextTick } from 'vue'
import axios from 'axios'
const dialogVisible = ref(false)
const smgContent = ref()
const textarea = ref('')
let chatWS = null
const userName= localStorage.getItem('userName')
let toUser = {id:null,name:null} // 要發(fā)送人的ID
const userArr = ref([]) // 人員數(shù)組
const activeNames = ref(['0'])
const msgArr = ref([])
// connectWebSocket()
function connectWebSocket () {
// 判斷當(dāng)前瀏覽器是否支持WebSocket
dialogVisible.value = true
if ('WebSocket' in window) {
// chatWS = new WebSocket('ws://172.16.26.37:8081/websocket/' + userName)
chatWS = new WebSocket('ws://192.168.1.125:8080/websocket/' + userName)
} else {
alert('當(dāng)前瀏覽器不支持Websocket')
}
chatWS.onmessage = (evt) => {
console.log(evt.data,'人員更新');
if (evt.data==='人員更新') {
obtainName() // 調(diào)取人員接口
}else{
msgArr.value.push(evt.data)
}
nextTick(()=>{
smgContent.value.scrollTop = smgContent.value.scrollHeight
})
}
}
// 獲取聊天人員接口
function obtainName () {
// axios.get('http://172.16.26.37:8002/java_backend/members').then(res => {
axios.get('http://192.168.1.125:8080/members').then(res => {
userArr.value = res.data.data.map(item => {
item.isCheck = false
if (toUser.id === item.username) {
item.isCheck = true
}
return item
})
})
}
// 選擇群聊或者單聊 獲取聊天記錄
function checkUser (item) {
msgArr.value = [] // 清空聊天框內(nèi)容
userArr.value.forEach(i => {
if (i.username !== item.username) {
i.isCheck = false
}
})
// 保存 選擇人員的ID和name 并通過(guò)id 來(lái)區(qū)分是群聊還是單聊
item.isCheck = !item.isCheck
toUser.name = item.name
if (item.isCheck) {
toUser.id = item.username
} else {
toUser.id = ''
}
let data = {
type: toUser.id === '' ? 0 : 1,
toUser:toUser.id,
fromUser:userName
}
// 獲取聊天記錄
axios.post('http://192.168.1.125:8080/message',data).then(res => {
let type =toUser.id === '' ? true : false
if (type) {
res.data.data.forEach(item=>{
msgArr.value.push(`${item.fromName}:${item.message}`) // 群聊
})
}else{
res.data.data.forEach(item=>{
msgArr.value.push(`${item.fromName}->${item.toName}:${item.message}`) //單聊
})
}
nextTick(()=>{
smgContent.value.scrollTop = smgContent.value.scrollHeight
})
})
}
// 發(fā)送消息
function keydown(e) {
if (e.ctrlKey && e.keyCode === 13) {
// 換行
textarea.value = textarea.value + '\n'
}
if (e.ctrlKey === false && e.keyCode === 13) {
// 阻止瀏覽器默認(rèn)行為 禁止發(fā)送時(shí)輸入框換行
e.preventDefault ? e.preventDefault() : (e.returnValue = false);
// 發(fā)送
sendBtn()
}
}
function sendBtn () {
if (textarea.value !== '') {
const socketMsg = {msg: textarea.value,toUser:toUser.id,type: toUser.id === '' ? 0 : 1}
chatWS.send(JSON.stringify(socketMsg))
textarea.value = ''
}
}
// 關(guān)閉連接
function closeWebSocket () {
chatWS.close()
userArr.value = []
textarea.value = ''
msgArr.value = []
dialogVisible.value = false
}
// 監(jiān)聽(tīng)窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時(shí),主動(dòng)去關(guān)閉websocket連接,防止連接還沒(méi)斷開(kāi)就關(guān)閉窗口,server端會(huì)拋異常。
window.onbeforeunload = function () {
chatWS.close()
}
defineExpose({
connectWebSocket
})
</script>
<style lang='css' scoped>
</style>
<style lang='less'>
.el-dialog__body{
padding-top: 0px;
}
.chat-big-box{
.user-select{
user-select:none;
}
width: 100%;
height: 60vh;
display: flex;
.chat-left{
flex: 1;
padding: 10px;
border: solid 1px #ccc;
overflow:auto;
margin-right: 10px;
.user-name-check{
background: #ccc !important;
}
.chat-user-name{
display: flex;
padding: 10px;
background: #dddadab4;
line-height: 20px;
margin-bottom: 5px;
div:nth-child(1){
margin-right: 10px;
}
div:nth-child(2){
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
}
}
.chat-right{
flex: 4;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
position: relative;
.content{
border: solid 1px #ccc;
width: 100%;
height: 75%;
text-align: left;
overflow: auto;
margin-bottom: 10px;
div{
margin: 10px;
}
}
.input-box{
width: 100%;
}
.input-btn{
position: absolute;
right: 1px;
bottom: 1px;
}
}
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-thumb {
/*滾動(dòng)條里面小方塊*/
border-radius: 8px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: #4da1ff;
}
::-webkit-scrollbar-track {
/*滾動(dòng)條里面軌道*/
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: #ededed;
border-radius: 8px;
}
}
.online-personnel{
width: 100%;
display: flex;
justify-content: space-between;
padding: 10px;
.sum{
margin-right: 10px;
}
}
</style>
對(duì)比:相較于之前的那篇博客添加了以下幾個(gè)點(diǎn):
?
?
?遇到問(wèn)題:相對(duì)于接口調(diào)用@Autowired會(huì)將對(duì)象加載進(jìn)容器,但是webSocket不是接口調(diào)用,所以在保存聊天記錄的時(shí)候會(huì)報(bào)空指針,(mapp對(duì)象的空指針)
所以要對(duì)mappr對(duì)象進(jìn)行提前加載,類(lèi)似于餓漢模式
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-510298.html
之前:java實(shí)現(xiàn)聊天室(websocket)_奔馳的小野碼的博客-CSDN博客_java websocket聊天室文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-510298.html
到了這里,關(guān)于Java+Vue實(shí)現(xiàn)聊天室(WebSocket進(jìn)階-聊天記錄)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!