一篇就够 | WebSocket的使用
朋友们,好久不见,三个月没更新了,想起开博之初的目标:月更、周更,都没有做到。因为我自己的技术确实还不够到位,一知半解的水平未能写出对某些技术理解得很到位的文章,于是便不好意思写文章了。我是一个在学习上具有 BFS 风格的人,最近两天又开始 Unity 游戏开发的学习,一天跟着官方 hello world 级别的教程熟悉 Unity 开发的基础操作,一天自己 DIY 做了一个双人单机游戏。现在是第三天的早上,今天准备实现即时通讯联机对战,而昨晚睡前已经想好了策略:重温 WebSocket 知识 ===> 改用 JavaScript 写控制脚本 ===> 制作登录场景和游戏大厅场景 ===> 落实功能 ===> 制作安装包并找身边的朋友测试。于是现在便开始了复习整理 WebSocket 知识的行动。
内容预告
- 本人将基于两个月前做课程作业的时候用过的 WebSocket 的经历与收获制作此篇文章,希望可以起到类似"游戏存档"一般的效果,下次自己阅读时可以读取"游戏记录",当然也希望对其他读者也能有同样的效果。
- 本文的内容偏应用,以更快掌握使用为主要目的,底层原理用笔较少。
资料整理
WebSocket的使用流程整理一、WebSocket概述
笔者在去年的这个时候,学过一个叫 DWR 的 socket 通信框架,DWR 对 socket 做了封装,可以方便开发者更快地使用 socket 实现 IM (Instant Messaging),以为 DWR 已经很方便了,但前段时间学用了 WebSocket 后,发现 WebSocket 比 DWR 方便更多。但这两者都需要有 JavaScript 的支持,需要用到一些封装好的工具包,倘若用其他语言(如 C#、Python 进行调用)进行调用,不可行。WebSocket 真的是一个很方便的 IM 工具。
先来几张 demo 项目的运行效果图,后面再针对图进行解释:
页面一:用户1页面 index.html
页面效果页面二:用户2页面 index2.html
页面效果页面三:广播页面 topic.html
页面效果二、源码解析
2.1 发消息
后端代码:WebSocketConfig.java
作用解释:配置完成后,前端才能与后端相应的接口连接,并生成 StompClient 对象(前端的所有 IM 都将使用该对象来实现)
/***
* 注册 Stomp的终端
* addEndpoint:添加STOMP协议的终端。提供WebSocket或SockJS客户端访问的地址
* withSockJS:使用SockJS协议
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/C_lobby") // 广播式 stompClient 的注册方式
.withSockJS() ;
registry.addEndpoint("/C_player") // 对点式stompClient 的注册方式
.setAllowedOrigins("*") // 添加允许跨域访问
.withSockJS() ;
}
前端代码:index1.html
作用解释:再下面的一端代码中,注释里有写到 "/send" 是所有客户端发往服务器的 IM 请求的前缀,而 "/byPlayer" 则是截获请求的 url 后缀;于是,向 "/send/byPlayer" 发送数据后便可被后端程序截获并处理;
// 选择后端设置的注册方式生成socket,其中ball是项目名,C_player 是玩家端的注册方式
var socket = new SockJS("/ball/C_player");
// 由 socket 生成 stompClient
stompClient = Stomp.over(socket);
// 发送名称到后台
function sendName(){
// 获取输入框的内容 (PS:字段名称没来得及改过来,我觉得不应该用 name)
var name = $("#name").val();
stompClient.send("/send/byPlayer", {}, JSON.stringify({'message':name,
'senderId':123, 'senderName': 'hello' }));
}
2.1 收消息
后端配置:WebSocketConfig.java
作用解释:分别将客户端 IM 请求前缀和向客户端推送消息的 url 前缀设置为 "/send" 和 "/player";
/**
* 配置消息代理
* 启动Broker,消息的发送的地址符合配置的前缀来的消息才发送到这个broker
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//registry.enableSimpleBroker("/player", "/lobby"); // 推送消息前缀(可有可无)
registry.setApplicationDestinationPrefixes("/send"); // 玩家发消息的前缀 服务器听消息
registry.setUserDestinationPrefix("/player"); // 推送玩家信息的前缀
}
后端处理消息的代码:WebSocketController.java
作用解释:由于此后端项目服务的是游戏,需要更快的相应速度,所有此处采取的方案是直接将 json 数据返回给所有订阅了该玩家的客户端(IM 后台只做广播的效果)
/**
* 主要功能为实现玩家之间的广播(即对点式即时通信)
* @param message 已经装配好的 json 字符串
* @return json原样返回
*/
@MessageMapping("/byPlayer")
public Object toPlayer(RequestMessage message) {
System.out.println("player "+message.getSenderId()+" ----------------" + message.getMessage());
this.messagingTemplate.convertAndSendToUser(""+message.getSenderId(),"/say", message);
return message;
}
RequestMessage 的结构:
@Data
public class RequestMessage {
/***
* 请求消息
*/
private String message;
private String senderName;
private int senderId;
}
前端代码:index1.html
作用解释: "/player/{playerId}/say" 为 id = playerId 的玩家的 IM 广播 url 后缀,订阅(stompClient.subscribe)后即可接收并处理该玩家的消息;
stompClient.connect({},function (frame) {
setConnected(true);
console.log("connected : "+frame);
// 此页面发送消息时设置的 id 为 123
stompClient.subscribe('/player/' + 123 + '/say',function (response) {
showResponse("我说:" + response.body);
})
// index2页面发送消息时设置的 id 为 1234
stompClient.subscribe('/player/' + 1234 + '/say',function (response) {
showResponse("他说:" + response.body);
})
// 表示来自大厅的消息
stompClient.subscribe('/lobby/newMessage',function (response) {
showResponse("群发说:" + response.body);
})
})
三、注意事项
3.1 不可以使用 @MessageMapping 实现路径传值
若代码这样写:
后端:
/**
* 提供一个转发消息的渠道,将用户发过来的消息原封不动地转发到其他订阅者(游戏进行时的对手)
* @param message 来自玩家的数据(已装配好的 json 字符串)
* @param playerId 当前玩家的id
* @return json格式的数据,发送给当前对战的双方
*/
@MessageMapping("/byPlayer/{playerId}")
public Object toPlayer(RequestMessage message,
@PathVariable(name = "playerId")int playerId) {
System.out.println("player "+playerId+" ----------------" + message.getMessage());
this.messagingTemplate.convertAndSendToUser(""+playerId,"/say", message.getMessage());
return message;
}
前端:
//发送名称到后台
function sendName(){
var name = $("#name").val();
stompClient.send("/send/byPlayer/123", {}, JSON.stringify({'message':name, 'senderId':123, 'senderName':'hello'}));
}
将会有这样的错误:
ERROR 6452 --- [boundChannel-13] .WebSocketAnnotationMethodMessageHandler : Unhandled exception from message handler method
org.springframework.messaging.converter.MessageConversionException: Could not read JSON: Cannot deserialize instance of `int` out of START_OBJECT token
at [Source: (byte[])"{"message":"有人","senderId":123,"senderName":"hello"}"; line: 1, column: 1]; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `int` out of START_OBJECT token
at [Source: (byte[])"{"message":"有人","senderId":123,"senderName":"hello"}"; line: 1, column: 1]
at org.springframework.messaging.converter.MappingJackson2MessageConverter.convertFromInternal(MappingJackson2MessageConverter.java:234) ~[spring-messaging-5.1.3.RELEASE.jar:5.1.3.RELEASE]
at org.springframework.messaging.converter.AbstractMessageConverter.fromMessage(AbstractMessageConverter.java:181)
...
...
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3121) ~[jackson-databind-2.9.7.jar:2.9.7]
at org.springframework.messaging.converter.MappingJackson2MessageConverter.convertFromInternal(MappingJackson2MessageConverter.java:221) ~[spring-messaging-5.1.3.RELEASE.jar:5.1.3.RELEASE]
... 15 common frames omitted
解决办法:
- 阅读 @MessageMapping的官方文档 后发现 @MessageMapping 似乎并不支持那样传参
- 在IDE里面
Ctrl+左键
查看 @MessageMapping 源码后发现它确实只有一个属性,并没有像 @GetMapping 或 @PostMapping 那样拥有多个属性,自然就不能被 像 @PathVariable 这样的注解截获想要通过 url 传递的值了
所以,笔者认为若想达到这一目的,应该采用前文提到的策略——将想要传的值在前端部分装配成 json 数据,IM 后台直接对 json 数据进行广播
四、总结
首尾呼应,再发一遍这张图感想:
1.学 Unity 的时候发现网上的资源似乎比较零散,想要系统地学习的话,不容易呀;同时也感受到一篇有质量的文章对于学习新技术的小白来说是多么重要,简直就像救世的圣音一般。希望我写这篇文章也给初涉 WebSocket 的朋友们带来便利。
2.没想到最后写完只有这点内容,连思维导图都不用画。
3.原本我对 WebSocket 也是一知半解的,整理完这篇文章,从此 WebSocket 对我来说算是一个舒适区了。
如果觉得本文对你有帮助的话,不妨点个喜欢哦,我会很开心的
学习知识 = 进食 + 咀嚼 + 消化 + 吸收
进食 = 阅读/学习,掌握基本理论、掌握赏析能力
咀嚼 = 做笔记整理/解读优秀作品(顺便再次阅读/学习)
消化 = 反复思考+理解
吸收 = 在实际生活学习或项目中尝试使用该知识、感受该知识
——《如何高效学习》