WebSocket入门案例——聊天室
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.添加配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.编写WebSocket操作类
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/chat/{username}")
@Component
@Slf4j
public class WebsocketServer {
public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();//存储连接对象
@OnOpen //开启ws连接
public void onOpen(Session session, @PathParam("username") String username) {
sessionMap.put(username, session);
log.info("用户{}登录,当前在线人数:{}", username,sessionMap.size());
JSONObject result = new JSONObject();
JSONArray array = new JSONArray();
result.set("users",array);
for (String key : sessionMap.keySet()) {
JSONObject jsonObject = new JSONObject();
jsonObject.set("username",key);
array.add(jsonObject);
}
sendAllMessage(JSONUtil.toJsonStr(result));
}
@OnClose //关闭连接
public void onclose(Session session, @PathParam("username") String username){
sessionMap.remove(username);
log.info("用户{}退出,当前在线人数:{}", username,sessionMap.size());
}
@OnMessage //消息的转发
public void onMessage(String message, Session session, @PathParam("username") String username){
log.info("用户{}发送消息:{}", username,message);
JSONObject obj = JSONUtil.parseObj(message);
String to = obj.getStr("to");
String text = obj.getStr("text");
Session toSession = sessionMap.get(to);
if (toSession != null) {
JSONObject result = new JSONObject();
result.set("from",username);
result.set("text",text);
this.sendMessage(result.toString(),toSession);
}else{
log.error("发送失败,未找到用户username={}",to);
}
}
@OnError
public void onError(Session session, Throwable throwable){
log.error("发生错误");
throwable.printStackTrace();
}
private void sendMessage(String message,Session tosession) {
try{
log.info("服务端给客户端【{}】发送消息:{}",tosession.getId(),message);
tosession.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
private void sendAllMessage(String jsonStr) {
sessionMap.forEach((sessionId, session) -> {
try {
log.info("服务端给客户端【{}】发送消息:{}",sessionId,jsonStr);
session.getBasicRemote().sendText(jsonStr);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
4.前端页面(Vue)
<template>
<div style="padding: 10px; margin-bottom: 50px">
<el-row>
<el-col :span="8">
<el-card style="width: 100%; min-height: 300px; color: #333">
<div style="padding-bottom: 10px; border-bottom: 1px solid #ccc">在线用户<span style="font-size: 12px">(点击聊天气泡开始聊天)</span></div>
<div v-for="user in users" :key="user.username" style="padding: 10px 0">
<div @click="chatUser = user.username">
<span>{{ user.username }}</span>
<i
class="el-icon-chat-dot-round"
style="margin-left: 10px; font-size: 16px; cursor: pointer"
@click="chatUser = user.username"
/>
<span v-if="user.username === chatUser" style="font-size: 12px;color: limegreen; margin-left: 5px">chatting...</span>
</div>
</div>
</el-card>
</el-col>
<el-col :span="16">
<div
style="width: 800px; margin: 0 auto; background-color: white;
border-radius: 5px; box-shadow: 0 0 10px #ccc"
>
<div style="text-align: center; line-height: 50px;">
Web聊天室({{ chatUser }})
</div>
<div style="height: 350px; overflow:auto; border-top: 1px solid #ccc">
<ChatBubble
v-for="(message, index) in messages"
:key="index"
:is-remote-user="message.user !== user.username"
:user-avatar="user.avatarPath"
:text="message.text"
/>
</div>
<div style="height: 200px">
<textarea
v-model="text"
style="height: 160px; width: 100%; padding: 20px; border: none; border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc; outline: none"
/>
<div style="text-align: right; padding-right: 10px">
<el-button type="primary" size="mini" @click="send">发送</el-button>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import ChatBubble from '../compoments/ChatBubble.vue'
import { mapGetters } from 'vuex'
let socket
export default {
name: 'Im',
components: { ChatBubble },
data() {
return {
isCollapse: false,
users: [],
chatUser: '',
text: '',
messages: [],
content: ''
}
},
computed: {
...mapGetters([
'user',
'baseApi'
])
},
created() {
this.init()
},
methods: {
send() {
if (!this.chatUser) {
this.$message({ type: 'warning', message: '请选择聊天对象' })
return
}
if (!this.text) {
this.$message({ type: 'warning', message: '请输入内容' })
} else {
if (typeof (WebSocket) === 'undefined') {
console.log('您的浏览器不支持WebSocket')
} else {
console.log('您的浏览器支持WebSocket')
// 组装待发送的消息 json
// {"from": "zhang", "to": "admin", "text": "聊天文本"}
const message = { from: this.user.username, to: this.chatUser, text: this.text }
socket.send(JSON.stringify(message)) // 将组装好的json发送给服务端,由服务端进行转发
this.messages.push({ user: this.user.username, text: this.text })
this.text = ''
}
}
},
init() {
console.log(this.user)
const username = this.user.username
const _this = this
if (typeof (WebSocket) === 'undefined') {
console.log('您的浏览器不支持WebSocket')
} else {
console.log('您的浏览器支持WebSocket')
const socketUrl = 'ws://localhost:8000/chat/' + username
if (socket != null) {
socket.close()
socket = null
}
// 开启一个websocket服务
socket = new WebSocket(socketUrl)
// 打开事件
socket.onopen = function() {
console.log('websocket已打开')
}
// 浏览器端收消息,获得从服务端发送过来的文本消息
socket.onmessage = function(msg) {
console.log('收到数据====' + msg.data)
const data = JSON.parse(msg.data) // 对收到的json数据进行解析, 类似这样的: {"users": [{"username": "zhang"},{ "username": "admin"}]}
// console.log(data)
if (data.users) { // 获取在线人员信息
console.log('在线用户列表====' + data.users)
_this.users = data.users.filter(user => user.username !== username) // 获取当前连接的所有用户信息,并且排除自身,自己不会出现在自己的聊天列表里
} else {
// 如果服务器端发送过来的json数据 不包含 users 这个key,那么发送过来的就是聊天文本json数据
// // {"from": "zhang", "text": "hello"}
if (data.from === _this.chatUser) {
_this.messages.push(data)
// 构建消息内容
// _this.createContent(data.from, null, data.text)
}
}
}
// 关闭事件
socket.onclose = function() {
console.log('websocket已关闭')
}
// 发生了错误事件
socket.onerror = function() {
console.log('websocket发生了错误')
}
}
}
}
}
</script>
<style>
.tip {
color: white;
text-align: center;
border-radius: 10px;
font-family: sans-serif;
padding: 10px;
width:auto;
display:inline-block !important;
display:inline;
}
.right {
background-color: deepskyblue;
}
.left {
background-color: forestgreen;
}
</style>
页面中使用的气泡组件
<template>
<div class="el-row" :class="bubbleClass" style="padding: 5px 0">
<div class="el-col el-col-2" v-if="isRemoteUser" style="text-align: right">
<span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">
<img src="@/assets/images/avatar.png" style="object-fit: cover;">
</span>
</div>
<div class="el-col el-col-22" :style="textStyle">
<div class="tip" :class="bubbleColor">{{ text }}</div>
</div>
<div class="el-col el-col-2" v-if="!isRemoteUser">
<span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">
<img src="@/assets/images/avatar.png" style="object-fit: cover;">
</span>
</div>
</div>
</template>
<script>
export default {
props: {
isRemoteUser: {
type: Boolean,
required: true
},
userAvatar: {
type: String,
required: true
},
text: {
type: String,
required: true
}
},
computed: {
bubbleClass() {
return this.isRemoteUser ? 'remote-user' : 'current-user'
},
textStyle() {
return this.isRemoteUser ? 'text-align: left; padding-left: 10px' : 'text-align: right; padding-right: 10px'
},
bubbleColor() {
return this.isRemoteUser ? 'right' : 'left'
}
},
mounted() {
// console.log(this.isRemoteUser)
console.log(this.userAvatar)
// console.log(this.text)
}
}
</script>
<style scoped>
.tip {
color: white;
text-align: center;
border-radius: 10px;
font-family: sans-serif;
padding: 10px;
width:auto;
display:inline-block !important;
display:inline;
}
.right {
background-color: deepskyblue;
}
.left {
background-color: forestgreen;
}
</style>