本文做者芋艿,原题“芋道 Spring Boot WebSocket 入门”,本次有修订和改动。html
WebSocket现在在Web端即时通信技术应用里使用普遍,不只用于传统PC端的网页里,也被不少移动端开发者用于基于HTML5的混合APP里。对于想要在基于Web的应用里添加IM、推送等实时通讯功能,WebSocket几乎是必需要掌握的技术。前端
本文将基于Tomcat和Spring框架实现一个逻辑简单的入门级IM应用,对于即时通信初学者来讲,能找到一个简单直接且能顺利跑通的实例代码,显然意义更大,本文正是如此。但愿能给你的IM开发和学习带来启发。java
注:源码在本文第4、五节开头的附件处可下载。node
学习交流:git
- 即时通信/推送技术开发交流5群:215477170 [推荐]
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK
(本文同步发布于:http://www.52im.net/thread-3483-1-1.html)github
若是你对Web端即时通信知识一头雾水,务必先读:《新手入门贴:史上最全Web端即时通信技术原理详解》、《Web端即时通信技术盘点:短轮询、Comet、Websocket、SSE》。web
限于篇幅,本文不会深究WebSocket技术理论,若有兴趣请从基础学习:算法
若是想要更硬核一点的,能够读读下面这几篇:spring
相比 HTTP 协议来讲,WebSocket 协议对大多数后端开发者是比较陌生的。数据库
相对而言:WebSocket 协议重点是提供了服务端主动向客户端发送数据的能力,这样咱们就能够完成实时性较高的需求。例如:聊天 IM 即便通信功能、消息订阅服务、网页游戏等等。
同时:由于 WebSocket 使用 TCP 通讯,能够避免重复建立链接,提高通讯质量和效率。例如:美团的长链接服务,具体能够看看 《美团点评的移动端网络优化实践:大幅提高链接成功率、速度等》 。
友情提示:
这里有个误区,WebSocket 相比普通的 Socket 来讲,仅仅是借助 HTTP 协议完成握手,建立链接。后续的全部通讯,都和 HTTP 协议无关。
看到这里,你们必定觉得又要开始哔哔 WebSocket 的概念。哈哈,我偏不~若是对这块不了的朋友,能够阅读本文“二、知识准备”这一章。
要想使用WebSocket,通常有以下几种解决方案可选:
目前笔者手头有个涉及到 IM 即便通信的项目,采用的是方案三。
主要缘由是:咱们对 Netty 框架的实战、原理与源码,都相对熟悉一些,因此就考虑了它。而且,除了须要支持 WebSocket 协议,咱们还想提供原生的 Socket 协议。
若是仅仅是仅仅提供 WebSocket 协议的支持,能够考虑采用方案一或者方案二,在使用上,两个方案是比较接近的。相比来讲,方案一 Spring WebSocket 内置了对 STOMP 协议的支持。
不过:本文仍是采用方案二“Tomcat WebSocket”来做为入门示例。咳咳咳,没有特殊的缘由,主要是开始写本文以前,已经花了 2 小时使用它写了一个示例。实在是有点懒,不想改。若是能重来,我要选李白,哈哈哈哈~
固然,不要慌,方案一和方案二的实现代码,真心没啥差异。
在开始搭建 Tomcat WebSocket 入门示例以前,咱们先来了解下 JSR-356 规范,定义了 Java 针对 WebSocket 的 API :即 Javax WebSocket 。规范是大哥,打死不会提供实现,因此 JSR-356 也是如此。目前,主流的 Web 容器都已经提供了 JSR-356 的实现,例如说 Tomcat、Jetty、Undertow 等等。
示例代码下载:
(因附件没法上传到此处,请从同步连接处下载: http://www.52im.net/thread-3483-1-1.html)
代码目录内容是这样:
在本小节中,咱们会使用 Tomcat WebSocket 搭建一个 WebSocket 的示例。
提供以下消息的功能支持:
考虑到让示例更加易懂,咱们先作成全局有且仅有一个大的聊天室,即创建上 WebSocket 的链接,都自动动进入该聊天室。
下面,开始遨游 WebSocket 这个鱼塘...
在 pom.xml 文件中,引入相关依赖。
<?xml version="1.0"encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 [url= http://maven.apache.org/xsd/m...">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lab-25-01</artifactId>
<dependencies>
<!-- 实现对 WebSocket 相关依赖的引入,方便~ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 引入 Fastjson ,实现对 JSON 的序列化,由于后续咱们会使用它解析消息 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
</project>
具体每一个依赖的做用,本身认真看下注释。
在 cn.iocoder.springboot.lab25.springwebsocket.websocket 包路径下,建立 WebsocketServerEndpoint 类,定义 Websocket 服务的端点(EndPoint)。
代码以下:
// WebsocketServerEndpoint.java
@Controller
@ServerEndpoint("/")
public class WebsocketServerEndpoint {
private Logger logger = LoggerFactory.getLogger(getClass());
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
logger.info("onOpen", session);
}
@OnMessage
public void onMessage(Session session, String message) {
logger.info("onOpen", session, message); // 生产环境下,请设置成 debug 级别
}
@OnClose
public void onClose(Session session, CloseReason closeReason) {
logger.info("onClose", session, closeReason);
}
@OnError
public void onError(Session session, Throwable throwable) {
logger.info("onClose", session, throwable);
}
}
如代码所示:
这是最简版的 WebsocketServerEndpoint 的代码。在下文,咱们会慢慢把代码补全。
在 cn.iocoder.springboot.lab24.springwebsocket.config 包路径下,建立 WebsocketServerEndpoint 配置类。
代码以下:
// WebSocketConfiguration.java
@Configuration
// @EnableWebSocket // 无需添加该注解,由于咱们并非使用 Spring WebSocket
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
PS:在 #serverEndpointExporter() 方法中,建立 ServerEndpointExporter Bean 。该 Bean 的做用,是扫描添加有 @ServerEndpoint 注解的 Bean 。
建立 Application.java 类,配置 @SpringBootApplication 注解便可。
代码以下:
// Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
执行 Application 启动该示例项目。
考虑到你们可能不会或者不肯意写前端代码,因此咱们直接使用 WebSocket在线测试工具,测试 WebSocket 链接。
以下图:
至此,最简单的一个 WebSocket 项目的骨架,咱们已经搭建完成。下面,咱们开始改造,把相应的逻辑补全。
在 HTTP 协议中,是基于 Request/Response 请求响应的同步模型,进行交互。在 Websocket 协议中,是基于 Message 消息的异步模型,进行交互。这一点,是很大的不一样的,等会看到具体的消息类,感觉会更明显。
由于 WebSocket 协议,不像 HTTP 协议有 URI 能够区分不一样的 API 请求操做,因此咱们须要在 WebSocket 的 Message 里,增长可以标识消息类型,这里咱们采用 type 字段。
因此在这个示例中,咱们采用的 Message 采用 JSON 格式编码。
格式以下:
{
type: "", // 消息类型
body: {} // 消息体
}
解释一下:
实际上:咱们在该示例中,body 字段对应的 Message 相关的接口和类,实在想不到名字了。全部的 Message 们,咱们都放在 cn.iocoder.springboot.lab25.springwebsocket.message 包路径下。
4.6.1 Message
建立 Message 接口,基础消息体,全部消息体都要实现该接口。
代码以下:
// Message.java
publicinterfaceMessage {
}
目前做为一个标记接口,未定义任何操做。
4.6.2 认证相关 Message
建立 AuthRequest 类,用户认证请求。
代码以下:
// AuthRequest.java
public class AuthRequest implements Message {
public static final String TYPE = "AUTH_REQUEST";
/**
* 认证 Token
*/
private String accessToken;
// ... 省略 set/get 方法
}
解释一下:
对于第2)点,在 WebSocket 协议中,咱们也须要认证当前链接,用户身份是什么。通常状况下,咱们采用用户调用 HTTP 登陆接口,登陆成功后返回的访问令牌 accessToken 。这里,咱们先不拓展开讲,过后能够看看 《基于 Token 认证的 WebSocket 链接》 文章。
虽说,WebSocket 协议是基于 Message 模型,进行交互。可是,这并不意味着它的操做,不须要响应结果。例如说,用户认证请求,是须要用户认证响应的。因此,咱们建立 AuthResponse 类,做为用户认证响应。
代码以下:
// AuthResponse.java
public class AuthResponse implements Message {
public static final String TYPE = "AUTH_RESPONSE";
/**
* 响应状态码
*/
private Integer code;
/**
* 响应提示
*/
private String message;
// ... 省略 set/get 方法
}
解释一下:
对于第1)点,实际上,咱们在每一个 Message 实现类上,都增长了 TYPE 静态属性,做为消息类型。下面,咱们就不重复赘述了。
在本示例中,用户成功认证以后,会广播用户加入群聊的通知 Message ,使用 UserJoinNoticeRequest 。
代码以下:
// UserJoinNoticeRequest.java
public class UserJoinNoticeRequest implements Message {
public static final String TYPE = "USER_JOIN_NOTICE_REQUEST";
/**
* 昵称
*/
private String nickname;
// ... 省略 set/get 方法
}
实际上,咱们能够在须要使用到 Request/Response 模型的地方,将 Message 进行拓展:
这样,在使用到同步模型的业务场景下,Message 实现类使用 Request/Reponse 做为后缀。例如说,用户认证请求、删除一个好友请求等等。
而在使用到异步模型能的业务场景下,Message 实现类仍是继续 Message 做为后缀。例如说,发送一条消息,用户操做完后,无需阻塞等待结果
4.6.3 发送消息相关 Message
建立 SendToOneRequest 类,发送给指定人的私聊消息的 Message。
代码以下:
// SendToOneRequest.java
public class SendToOneRequest implements Message {
public static final String TYPE = "SEND_TO_ONE_REQUEST";
/**
* 发送给的用户
*/
private String toUser;
/**
* 消息编号
*/
private String msgId;
/**
* 内容
*/
private String content;
// ... 省略 set/get 方法
}
每一个字段,本身看注释噢。
建立 SendToAllRequest 类,发送给全部人的群聊消息的 Message。
代码以下:
// SendToAllRequest.java
public class SendToAllRequest implements Message {
public static final String TYPE = "SEND_TO_ALL_REQUEST";
/**
* 消息编号
*/
private String msgId;
/**
* 内容
*/
private String content;
// ... 省略 set/get 方法
}
每一个字段,本身看注释噢。
在服务端接收到发送消息的请求,须要异步响应发送是否成功。因此,建立 SendResponse 类,发送消息响应结果的 Message 。
代码以下:
// SendResponse.java
public class SendResponse implements Message {
public static final String TYPE = "SEND_RESPONSE";
/**
* 消息编号
*/
private String msgId;
/**
* 响应状态码
*/
private Integer code;
/**
* 响应提示
*/
private String message;
// ... 省略 set/get 方法
}
重点看 msgId 字段:即消息编号。客户端在发送消息,经过使用 UUID 算法,生成全局惟一消息编号(惟一ID的生成技术见:《重新手到专家:如何设计一套亿级消息量的分布式IM系统》的“_五、惟一ID的技术方案_”章节)。这样,服务端经过 SendResponse 消息响应,经过 msgId 作映射。
在服务端接收到发送消息的请求,须要转发消息给对应的人。因此,建立 SendToUserRequest 类,发送消息给一个用户的 Message 。
代码以下:
// SendResponse.java
public class SendToUserRequest implements Message {
public static final String TYPE = "SEND_TO_USER_REQUEST";
/**
* 消息编号
*/
private String msgId;
/**
* 内容
*/
private String content;
// ... 省略 set/get 方法
}
相比 SendToOneRequest 来讲,少一个 toUser 字段。由于,咱们能够经过 WebSocket 链接,已经知道发送给谁了。
每一个客户端发起的 Message 消息类型,咱们会声明对应的 MessageHandler 消息处理器。这个就相似在 SpringMVC 中,每一个 API 接口对应一个 Controller 的 Method 方法。
全部的 MessageHandler 们,咱们都放在 cn.iocoder.springboot.lab25.springwebsocket.handler 包路径下。
4.7.1 MessageHandler
建立 MessageHandler 接口,消息处理器接口。
代码以下:
// MessageHandler.java
public interface MessageHandler<T extends Message> {
/**
* 执行处理消息
*
* @param session 会话
* @param message 消息
*/
void execute(Session session, T message);
/**
* @return 消息类型,即每一个 Message 实现类上的 TYPE 静态字段
*/
String getType();
}
解释一下:
4.7.2 AuthMessageHandler
建立 AuthMessageHandler 类,处理 AuthRequest 消息。
代码以下:
// AuthMessageHandler.java
@Component
public class AuthMessageHandler implements MessageHandler<AuthRequest> {
@Override
public void execute(Session session, AuthRequest message) {
// 若是未传递 accessToken
if(StringUtils.isEmpty(message.getAccessToken())) {
WebSocketUtil.send(session, AuthResponse.TYPE,
new AuthResponse().setCode(1).setMessage("认证 accessToken 未传入"));
return;
}
// 添加到 WebSocketUtil 中
WebSocketUtil.addSession(session, message.getAccessToken()); // 考虑到代码简化,咱们先直接使用 accessToken 做为 User
// 判断是否定证成功。这里,伪装直接成功
WebSocketUtil.send(session, AuthResponse.TYPE,newAuthResponse().setCode(0));
// 通知全部人,某我的加入了。这个是可选逻辑,仅仅是为了演示
WebSocketUtil.broadcast(UserJoinNoticeRequest.TYPE,
newUserJoinNoticeRequest().setNickname(message.getAccessToken())); // 考虑到代码简化,咱们先直接使用 accessToken 做为 User
}
@Override
public String getType() {
return AuthRequest.TYPE;
}
}
代码比较简单,跟着代码读读便可。
关于 WebSocketUtil 类,咱们在「5.八、WebSocketUtil」一节中再来详细看看。
4.7.3 SendToOneRequest
建立 SendToOneHandler 类,处理 SendToOneRequest 消息。
代码以下:
// SendToOneRequest.java
@Component
public class SendToOneHandler implements MessageHandler<SendToOneRequest> {
@Override
public void execute(Session session, SendToOneRequest message) {
// 这里,伪装直接成功
SendResponse sendResponse = newSendResponse().setMsgId(message.getMsgId()).setCode(0);
WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
// 建立转发的消息
SendToUserRequest sendToUserRequest = newSendToUserRequest().setMsgId(message.getMsgId())
.setContent(message.getContent());
// 广播发送
WebSocketUtil.send(message.getToUser(), SendToUserRequest.TYPE, sendToUserRequest);
}
@Override
public String getType() {
return SendToOneRequest.TYPE;
}
}
代码比较简单,跟着代码读读便可。
4.7.4 SendToAllHandler
建立 SendToAllHandler 类,处理 SendToAllRequest 消息。
代码以下:
// SendToAllRequest.java
@Component
public class SendToAllHandler implements MessageHandler<SendToAllRequest> {
@Override
public void execute(Session session, SendToAllRequest message) {
// 这里,伪装直接成功
SendResponse sendResponse = newSendResponse().setMsgId(message.getMsgId()).setCode(0);
WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
// 建立转发的消息
SendToUserRequest sendToUserRequest = newSendToUserRequest().setMsgId(message.getMsgId())
.setContent(message.getContent());
// 广播发送
WebSocketUtil.broadcast(SendToUserRequest.TYPE, sendToUserRequest);
}
@Override
public String getType() {
return SendToAllRequest.TYPE;
}
}
代码比较简单,跟着代码读读便可。
代码在 cn.iocoder.springboot.lab25.springwebsocket.util 包路径下。
建立 WebSocketUtil 工具类,主要提供两方面的功能:
总体代码比较简单,本身瞅瞅哟。
代码在目录中的以下位置:
在本小节,咱们会修改 WebsocketServerEndpoint 的代码,完善其功能。
4.9.1 初始化 MessageHandler 集合
实现 InitializingBean 接口,在 #afterPropertiesSet() 方法中,扫描全部 MessageHandler Bean ,添加到 MessageHandler 集合中。
代码以下:
// WebsocketServerEndpoint.java
/**
* 消息类型与 MessageHandler 的映射
*
* 注意,这里设置成静态变量。虽说 WebsocketServerEndpoint 是单例,可是 Spring Boot 仍是会为每一个 WebSocket 建立一个 WebsocketServerEndpoint Bean 。
*/
private static final Map<String, MessageHandler> HANDLERS = newHashMap<>();
@Autowired
private ApplicationContext applicationContext;
@Override
public void afterPropertiesSet() throws Exception {
// 经过 ApplicationContext 得到全部 MessageHandler Bean
applicationContext.getBeansOfType(MessageHandler.class).values() // 得到全部 MessageHandler Bean.forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler)); // 添加到 handlers 中
logger.info("afterPropertiesSet", HANDLERS.size());
}
经过这样的方式,能够避免手动配置 MessageHandler 与消息类型的映射。
4.9.2 onOpen
从新实现 #onOpen(Session session, EndpointConfig config) 方法,实现链接时,使用 accessToken 参数进行用户认证。
代码以下:
// WebsocketServerEndpoint.java
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
logger.info("onOpen", session);
// <1> 解析 accessToken
List<String> accessTokenValues = session.getRequestParameterMap().get("accessToken");
String accessToken = !CollectionUtils.isEmpty(accessTokenValues) ? accessTokenValues.get(0) : null;
// <2> 建立 AuthRequest 消息类型
AuthRequest authRequest = newAuthRequest().setAccessToken(accessToken);
// <3> 得到消息处理器
MessageHandler<AuthRequest> messageHandler = HANDLERS.get(AuthRequest.TYPE);
if(messageHandler == null) {
logger.error("onOpen");
return;
}
messageHandler.execute(session, authRequest);
}
如代码所示:
打开三个浏览器建立,分别设置服务地址以下:
而后,逐个点击「开启链接」按钮,进行 WebSocket 链接。
最终效果以下图:
如上图所示:
4.9.3 onMessage
从新实现 _#onMessage(Session session, String message)_ 方法,实现不一样的消息,转发给不一样的 MessageHandler 消息处理器。
代码以下:
// WebsocketServerEndpoint.java
@OnMessage
public void onMessage(Session session, String message) {
logger.info("onOpen", session, message); // 生产环境下,请设置成 debug 级别
try{
// <1> 得到消息类型
JSONObject jsonMessage = JSON.parseObject(message);
String messageType = jsonMessage.getString("type");
// <2> 得到消息处理器
MessageHandler messageHandler = HANDLERS.get(messageType);
if(messageHandler == null) {
logger.error("onMessage", messageType);
return;
}
// <3> 解析消息
Class<? extendsMessage> messageClass = this.getMessageClass(messageHandler);
// <4> 处理消息
Message messageObj = JSON.parseObject(jsonMessage.getString("body"), messageClass);
messageHandler.execute(session, messageObj);
} catch(Throwable throwable) {
logger.info("onMessage", session, throwable);
}
}
代码中:
代码以下:
// WebsocketServerEndpoint.java
private Class<? extends Message> getMessageClass(MessageHandler handler) {
// 得到 Bean 对应的 Class 类名。由于有可能被 AOP 代理过。
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(handler);
// 得到接口的 Type 数组
Type[] interfaces = targetClass.getGenericInterfaces();
Class<?> superclass = targetClass.getSuperclass();
while((Objects.isNull(interfaces) || 0== interfaces.length) && Objects.nonNull(superclass)) { // 此处,是以父类的接口为准
interfaces = superclass.getGenericInterfaces();
superclass = targetClass.getSuperclass();
}
if(Objects.nonNull(interfaces)) {
// 遍历 interfaces 数组
for(Type type : interfaces) {
// 要求 type 是泛型参数
if(type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
// 要求是 MessageHandler 接口
if(Objects.equals(parameterizedType.getRawType(), MessageHandler.class)) {
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 取首个元素
if(Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) {
return(Class<Message>) actualTypeArguments[0];
} else{
thrownewIllegalStateException(String.format("类型(%s) 得到不到消息类型", handler));
}
}
}
}
}
throw new IllegalStateException(String.format("类型(%s) 得到不到消息类型", handler));
}
这是参考 rocketmq-spring 项目的 DefaultRocketMQListenerContainer#getMessageType() 方法,进行略微修改。
若是你们对 Java 的泛型机制没有作过一点了解,可能略微有点硬核。能够先暂时跳过,知道意图便可。
<4> 处,调用 MessageHandler#execute(session, message) 方法,执行处理请求。
另外:这里增长了 try-catch 代码,避免整个执行的过程当中,发生异常。若是在 onMessage 事件的处理中,发生异常,该消息对应的 Session 会话会被自动关闭。显然,这个不符合咱们的要求。例如说,在 MessageHandler 处理消息的过程当中,发生一些异常是没法避免的。
继续基于上述建立的三个浏览器,咱们先点击「清空消息」按钮,清空下消息,打扫下上次测试展现出来的接收获得的 Message 。固然,WebSocket 的链接,不须要去断开。
在第一个浏览器中,分别发送两种聊天消息。
一条 SendToOneRequest 私聊消息:
{
type: "SEND_TO_ONE_REQUEST",
body: {
toUser: "番茄",
msgId: "eaef4a3c-35dd-46ee-b548-f9c4eb6396fe",
content: "我是一条单聊消息"
}
}
一条 SendToAllHandler 群聊消息:
{
type: "SEND_TO_ALL_REQUEST",
body: {
msgId: "838e97e1-6ae9-40f9-99c3-f7127ed64747",
content: "我是一条群聊消息"
}
}
最终结果以下图:
如上图所示:
4.9.4 onClose
从新实现 _#onClose(Session session, CloseReason closeReason)_ 方法,实现移除关闭的 Session 。
代码以下:
// WebsocketServerEndpoint.java
@OnClose
public void onClose(Session session, CloseReason closeReason) {
logger.info("onClose", session, closeReason);
WebSocketUtil.removeSession(session);
}
4.9.5 onError
#onError(Session session, Throwable throwable) 方法,保持不变。
代码以下:
// WebsocketServerEndpoint.java
@OnError
public void onError(Session session, Throwable throwable) {
logger.info("onClose", session, throwable);
}
示例代码下载:
(因附件没法上传到此处,请从同步连接处下载: http://www.52im.net/thread-3483-1-1.html)
仔细一个捉摸,虎躯一震,仍是提供一个 Spring WebSocket 快速入门的示例。
在 上章「Tomcat WebSocket 实战入门」 的 _lab-websocket-25-01_ 示例的基础上,咱们复制出 lab-websocket-25-02 项目,进行改造。
改造的代码目录内容是这样:
由于 Tomcat WebSocket 使用的是 Session 做为会话,而 Spring WebSocket 使用的是 WebSocketSession 做为会话,致使咱们须要略微修改下 WebSocketUtil 工具类。改动很是略微,点击 WebSocketUtil.java 查看下,秒懂的噢。
主要有两点:
将 _cn.iocoder.springboot.lab25.springwebsocket.handler_ 包路径下的消息处理器们,使用到 Session 类的地方,调整成 WebSocketSession 类。
在 _cn.iocoder.springboot.lab25.springwebsocket.websocket_ 包路径下,建立 DemoWebSocketShakeInterceptor 拦截器。由于 WebSocketSession 没法得到 ws 地址上的请求参数,因此只好经过该拦截器,得到 accessToken 请求参数,设置到 attributes 中。
代码以下:
// DemoWebSocketShakeInterceptor.java
public class DemoWebSocketShakeInterceptor extends HttpSessionHandshakeInterceptor {
@Override// 拦截 Handshake 事件
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, Map<String, Object> attributes) throwsException {
// 得到 accessToken
if(request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
attributes.put("accessToken", serverRequest.getServletRequest().getParameter("accessToken"));
}
// 调用父方法,继续执行逻辑
return super.beforeHandshake(request, response, wsHandler, attributes);
}
}
在 _cn.iocoder.springboot.lab25.springwebsocket.websocket_ 包路径下,建立 DemoWebSocketHandler 处理器。该处理器参考 「5.九、完善 WebsocketServerEndpoint」 小节,编写它的代码。
DemoWebSocketHandler.java代码位于以下目录处,具体内容就不贴出来了,自已去读一读:
代码极其类似,简单撸下便可。
修改 WebSocketConfiguration 配置类,代码以下:
// WebSocketConfiguration.java
@Configuration
@EnableWebSocket// 开启 Spring WebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(this.webSocketHandler(), "/") // 配置处理器
.addInterceptors(newDemoWebSocketShakeInterceptor()) // 配置拦截器
.setAllowedOrigins("*"); // 解决跨域问题
}
@Bean
public DemoWebSocketHandler webSocketHandler() {
return new DemoWebSocketHandler();
}
@Bean
public DemoWebSocketShakeInterceptor webSocketShakeInterceptor() {
return new DemoWebSocketShakeInterceptor();
}
}
解释一下:
至此,咱们已经完成 Spring WebSocket 的示例。
后面,咱们执行 Application 来启动项目。具体的测试,这里就不重复了,能够本身使用 WebSocket 在线测试工具 来测试下。
虽说,WebSocket 协议已经在主流的浏览器上,获得很是好的支持,可是总有一些“异类”,是不兼容的。因此就诞生了 SockJS、Socket.io这类库。关于它们的介绍与使用,能够看看 《SockJS 简单介绍》 、《Web端即时通信技术的发展与WebSocket、Socket.io的技术实践》文章。
实际场景下,咱们在使用 WebSocket 仍是原生 Socket 也好,都须要考虑“如何保证消息必定送达给用户?”
你们确定可以想到的是:若是用户不处于在线的时候,消息持久化到 MySQL、MongoDB 等等数据库中。这个是正确,且是必需要作的。
咱们在一块儿考虑下边界场景:客户端网络环境较差,特别是在移动端场景下,出现网络闪断,可能会出现链接实际已经断开,而服务端觉得客户端处于在线的状况。此时,服务端会将消息发给客户端,那么消息实际就发送到“空气”中,产生丢失的状况。
要解决这种状况下的问题,须要引入客户端的 ACK 消息机制。
目前,主流的有两种作法。
第一种:基于每一条消息编号 ACK
总体流程以下:
这种方案,由于客户端逐条 ACK 消息编号,因此会致使客户端和服务端交互次数过多。固然,客户端能够异步批量 ACK 多条消息,从而减小次数。
不过由于服务端仍然须要定时轮询,也会致使服务端压力较大。因此,这种方案基本已经不采用了。
第二种:基于滑动窗口 ACK
总体流程以下:
这种方式,在业务被称为推拉结合的方案,在分布式消息队列、配置中心、注册中心实现实时的数据同步,常常被采用。
而且,采用这种方案的状况下,客户端和服务端不必定须要使用长链接,也可使用长轮询所替代。
作法好比,客户端发送带有消息版本号的 HTTP 请求到服务端:
若是你们对消息可靠投递这块感兴趣,能够看看下面这几篇:
毕竟,本篇这里写的有点简略哈 ~
最后:若是你想系统的学习IM开发方面方面的知识,推荐详读:《新手入门一篇就够:从零开发移动端IM》。若是你自认为已经有点小牛x了,能够看看生产环境下的大用户量IM系统架构设计方面的知识:《重新手到专家:如何设计一套亿级消息量的分布式IM系统》。
限于篇幅,这里就再也不继续展开了。
《自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)》
《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》
《手把手教你用Netty实现网络通讯程序的心跳机制、断线重连机制》
《[轻量级即时通信框架MobileIMSDK的iOS源码(开源版)[附件下载]](http://www.52im.net/thread-35...》
《[开源IM工程“蘑菇街TeamTalk”2015年5月前未删减版完整代码 [附件下载]](http://www.52im.net/thread-77...》
《[NIO框架入门(一):服务端基于Netty4的UDP双向通讯Demo演示 [附件下载]](http://www.52im.net/thread-36...》
《[NIO框架入门(二):服务端基于MINA2的UDP双向通讯Demo演示 [附件下载]](http://www.52im.net/thread-37...》
《[NIO框架入门(三):iOS与MINA二、Netty4的跨平台UDP双向通讯实战 [附件下载]](http://www.52im.net/thread-37...》
《[NIO框架入门(四):Android与MINA二、Netty4的跨平台UDP双向通讯实战 [附件下载]](http://www.52im.net/thread-38...》
《[一个WebSocket实时聊天室Demo:基于node.js+socket.io [附件下载]](http://www.52im.net/thread-51...》
《适合新手:从零开发一个IM服务端(基于Netty,有完整源码)》
《拿起键盘就是干:跟我一块儿徒手开发一套分布式IM系统》
《正确理解IM长链接的心跳及重连机制,并动手实现(有完整IM源码)》
《适合新手:手把手教你用Go快速搭建高性能、可扩展的IM系统(有源码)》
《跟着源码一块儿学:手把手教你用WebSocket打造Web端IM聊天》本文已同步发布于“即时通信技术圈”公众号。
▲ 本文在公众号上的连接是:点此进入。同步发布连接是:http://www.52im.net/thread-3483-1-1.html