引言:什么是WebSocket?
WebSocket和http一样,都是一种网络传输协议,但是和Http协议相比,它有一点不同,它可以在单个TCP连接上进行全双工通信,通俗来说就是客户端可以向服务端发送请求,服务端也可以向客户端发送请求;
这张图网上有很多,完美展示了http和webSocket的区别:
我在这里再解释一下:
-
http协议:客户端需要向服务端发送request请求,然后服务端会对该请求进行相应的处理,处理完成后响应Response到客户端;这就是一个流程;
如果客户端不向服务端发送请求,那么服务端就不会进行响应;
-
webSocket协议:首先客户端和服务端握手之后,它们之间的通道就打通了,此时客户端依然可以向服务端发送请求,服务端也可以主动的向客户端发送请求;这里是和http协议最大的差别;
总的来说:http协议服务端响应到客户端是被动的,而webSocket协议服务端请求到客户端是主动的;
案例说明
光说概念可能体会不出来,下面我来举个例子:
看直播时会有实时的弹幕,那么这个弹幕是怎么实时的显示的?
-
假设使用http协议,客户端请求服务端获取弹幕列表,服务端响应到客户端后确实可以获取到弹幕;但是一次请求就只有一次响应,但是弹幕是实时的,那么这样就无法实时获取到弹幕信息;
当然也有解决方法,可以通过轮询,一段时间内就自动发送一次获取弹幕的请求,但是这样的体验也不是特别好,因为只是请求获取的一个时间段的信息;弹幕还好,如果是游戏或者协同编辑等那么体验就非常不好了;
-
那么使用webSocket协议就可以轻松实现这种操作,只要某个客户端A向服务端发送了弹幕,那么服务端就可以把该弹幕发送给每一个在直播间的客户端,每个客户端就可以接收到该客户端A发送的弹幕了;
所以正是因为WebSocket的这种双向通信的特点,它常用于以下领域:
- 聊天、消息、点赞
- 直播评论(弹幕)
- 游戏、协同编辑、基于位置的应用
代码展示
下面我就简单展示一下WebSocket的功能,这里就模拟一个弹幕的发送;
前后端分离项目主要技术:
后端:java8+springboot+jwt+websocket
前端:vue3+js+websocket
因为后端和前端都需要发送webSocket请求,所以前后端都需要配置websocket
首先介绍以下websocket库中几个重要的方法:
onOpen() // 连接时调用
onClose() // 关闭连接时调用
onMessage() // 获取到信息时调用
onError() // 出现错误时调用
send() // 发送webSocket请求(携带数据)
说一下前后端的交互,既然前后端有信息发送,那么信息的格式就需要确定;http请求时经常用JSON格式进行交互,所以这里前后端最好也使用JSON格式进行交互;
后端
后端java代码:
@Component
@ServerEndpoint("/websocket/message/{token}")
@Slf4j
public class WebSocketMessageServer {
// onlineUsers可以当成该直播间的所有用户集合,key为用户的id,value为该用户的WebSocketMessageServer对象;
// 注意:是一个用户有一个WebSocketMessageServer对象!!!!!
// webSocket是多线程的,所以要使用ConcurrentHashMap保证线程安全
private static final ConcurrentHashMap<Long, WebSocketMessageServer> onlineUsers = new ConcurrentHashMap<>();
private User user; // 当前登录的用户(当前登录用户要建立连接)
private Session session = null; // 该对象可以发送消息给指定用户
private static RedisCacheUtil redisCacheUtil; // redis工具类
// 因为Spring自动注入是单例的,而webSocket是多线程的,所以无法直接注入,可以通过这个方法给每一个线程注入
@Autowired
public void setRedisCacheUtil(RedisCacheUtil redisCacheUtil) {
WebSocketMessageServer.redisCacheUtil = redisCacheUtil;
}
// 建立连接
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
this.session = session;
log.info("connect user...");
// 获取当前登录的用户
Long userId = JwtAuthentication.getUserId(token); // 从jwt中获取用户id
this.user = redisCacheUtil.getCacheObject("login:" + userId); // 从redis中获取用户信息
if (this.user != null) { // 如果当前登录用户存在,则存入当前直播间集合中
onlineUsers.put(userId, this); // this是当前登录用户的WebSocketMessageServer对象,可以使用该对象和前端进行信息交互
} else {
this.session.close();
}
log.info("当前建立连接的用户userId=>" + userId);
}
// 关闭连接
@OnClose
public void onClose() {
if (this.user != null) {
log.info("disconnect user..." + this.user.getId());
onlineUsers.remove(this.user.getId()); // 从该直播间中移除该用户
}
}
// 消息通信(前端传来的通信消息)
@OnMessage
public void onMessage(String message, Session session) {
log.info("receive match message...");
JSONObject data = JSON.parseObject(message); // 接收前端的信息
String sendUser = data.getString("sendUser"); // 获取弹幕发送者
String sendMessage = data.getString("message"); // 获取弹幕内容
// 将该信息发送到每一个连接用户的客户端
onlineUsers.forEach((key, value) -> {
JSONObject responseMessage = new JSONObject();
responseMessage.put("userInfo", sendUser); // 弹幕发送者
responseMessage.put("message", sendMessage); // 弹幕内容
// value就是每一个用户的WebSocketMessageServer对象
value.sendMessage(responseMessage.toJSONString()); // 将弹幕信息发送到前端
});
}
/**
* 发送信息
* @param message 响应信息
*/
private void sendMessage(String message) {
synchronized (this.session) {
try { // 发送信息
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
}
⚠这个方法一定要理解,主要理解的是:一个用户对应一个WebSocketMessageServer对象;只要有了这个对象,那么这个用户就可以通过该对象给自己的客户端发送信息:
大致就是图中的意思,理解了这一点才能在服务端写好交互逻辑,不然你向客户端发送个信息都不知道到底发送给了谁;这一点我认为是最重要的,因为复杂的请求逻辑都是写在后端,只有理解了这里,才能写出逻辑更复杂的功能;
所以上面我的代码中:
这一段就是把从某个客户端A接收到的弹幕发送给所有客户端,这样就实现了所有客户端都会接收到客户端A发送的弹幕;
前端
<template>
<div>
<ContentField style="text-align: center">
<ul class="list-group" v-for="message in messages">
<li class="list-group-item">{{message}}</li>
</ul>
</ContentField>
<div class="col-12" style="text-align: center;">
<div class="col-sm-10" style="width: 500px; margin-left: 500px; margin-top: 20px;">
<input class="form-control" id="inputPassword" v-model="inputMessage">
<button @click="sendMessage" type="button" class="btn btn-warning" style="margin-top: 10px">发送</button>
</div>
</div>
</div>
</template>
<script>
import ContentField from "@/components/ContentField.vue";
import {onMounted, ref, onUnmounted} from "vue";
import { useStore } from 'vuex'
export default {
name: "MessageWall",
components: {
ContentField
},
setup() {
let messages = ref([]) // 弹幕列表
let socket = null // socket对象
const store = useStore()
let inputMessage = ref('') // 输入的弹幕
onMounted(() => {
// 创建WebSocket对象,请求地址即为后端的@ServerEndpoint中的地址
socket = new WebSocket(`ws://127.0.0.1:8080/service/websocket/message/${store.state.user.token}/`)
// 建立连接
socket.onopen = () => {
console.log('connected...')
}
// 接收信息
socket.onmessage= msg => {
const data = JSON.parse(msg.data); // 获取信息并解析
let showMessage = data.userInfo + ':' + data.message
messages.value.push(showMessage) // 存入弹幕列表
showMessage = ''
}
// 结束连接
socket.onclose = () => {
console.log("disconnected...");
}
})
// 当前页面关闭时(刷新/路由切换)也要关闭连接
onUnmounted(() => {
socket.close();
})
// 发送弹幕(以JSON格式发送到后端)
const sendMessage = () => {
console.log(inputMessage.value)
socket.send(JSON.stringify({
sendUser: store.state.user.username, // 发送信息的用户名
message: inputMessage.value // 发送的信息
}))
}
return {
messages,
sendMessage,
inputMessage
}
}
}
</script>
前端代码没有什么难的,主要是前端向后端建立连接需要写后端的WebSocket对应的url;
然后就是正常的建立连接一套流程,接收到后端message就进行处理显示到前端界面;
而发送弹幕则需要向后端发送请求,携带JSON格式的弹幕数据,后端会通过它的onMessage接收该数据;
代码就是这样,看一看最后效果吧:
可以看到我开了三个不同浏览器窗口,每个浏览器窗口模拟一个客户端,三个浏览器都登录了不同的账号:ylx\test\admin三个账号,只要有一个客户端发送弹幕,那么另外两个也就可以接收到,这样就简单实现类一个弹幕发送;
因为案例比较简单,前端为了保证简洁,就用列表表示一条条弹幕,但是整体逻辑就是这样的,只要理解了这个,就可以做出对应的延伸拓展;
可以看一下后端日志:
开始三个客户端都进行了对应的连接,后面也都接收到了信息;
再次强调:一定要清楚一个客户端一个WebSocket对象,WebSocket对象可以为该客户端和服务端建立连接;
总结
之前了解过websocket,但是没有真正实操过,这两天也是在项目中使用到了websocket,感觉比较有意思,可以通过websocket做很多有趣的功能;所以简单总结复习一下;
如果有问题欢迎交流!