Hi,大家好,我是希留。
在项目的开发工程中,可能会遇到实时性比较高的场景需求,例如说,聊天 IM 即使通讯功能、消息订阅服务、在线客服等等。那遇到这种功能的时候应该怎么去做呢?通常是使用WebSocket去实现。
那么,本篇文章就带大家来了解一下是什么是WebSocket,以及使用SpringBoot搭建一个简易的聊天室功能。如果对你有帮助的话,还不忘点赞支持一下,感谢~
源码地址:
https://github.com/277769738/java-sjzl-demo/tree/master/springboot-websocket
https://gitee.com/huoqstudy/java-sjzl-demo/tree/master/springboot-websocket
提示:以下是本篇文章正文内容,下面案例可供参考
WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。WebSocket是真正实现了全双工通信的服务器向客户端推的互联网技术。它是一种在单个TCP连接上进行全双工通讯协议。Websocket通信协议与2011年倍IETF定为标准RFC 6455,Websocket API被W3C定为标准。
全双工和单工的区别?
http协议是短连接,因为请求之后,都会关闭连接,下次重新请求数据,需要再次打开链接。
WebSocket协议是一种长链接,只需要通过一次请求来初始化链接,然后所有的请求和响应都是通过这个TCP链接进行通讯。
<!--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>
因为 WebSocket 协议,不像 HTTP 协议有 URI 可以区分不同的 API 请求操作,所以我们需要在 WebSocket 的 Message 里,增加能够标识消息类型,这里我们采用 type 字段。所以在这个示例中,我们采用的 Message 采用 JSON 格式编码,格式如下:
{
type : "", //消息类型
boby: {} //消息体
}
type 字段,消息类型。通过该字段,我们知道使用哪个 MessageHandler 消息处理器。关于 MessageHandler ,我们在 「2.6 消息处理器」中,详细解析。
body 字段,消息体。不同的消息类型,会有不同的消息体。
创建 Message 接口,基础消息体,所有消息体都要实现该接口。目前作为一个标记接口,未定义任何操作。
public interface Message {
}
创建 AuthRequest 类,用户认证请求。TYPE 静态属性,消息类型为 AUTH_REQUEST 。
accessToken 属性,认证 Token 。在 WebSocket 协议中,我们也需要认证当前连接,用户身份是什么。一般情况下,我们采用用户调用 HTTP 登录接口,登录成功后返回的访问令牌 accessToken 。
代码如下:
public class AuthRequest implements Message{
public static final String TYPE = "AUTH_REQUEST";
/**
* 认证 Token
*/
private String accessToken;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
}
WebSocket 协议是基于 Message 模型,进行交互。但是,这并不意味着它的操作,不需要响应结果。例如说,用户认证请求,是需要用户认证响应的。所以,我们创建 AuthResponse 类,作为用户认证响应。代码如下:
public class AuthResponse implements Message {
public static final String TYPE = "AUTH_RESPONSE";
/**
* 响应状态码
*/
private Integer code;
/**
* 响应提示
*/
private String message;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
创建 SendToOneRequest 类,发送给指定人的私聊消息的 Message。代码如下:
public class SendToOneRequest implements Message {
public static final String TYPE = "SEND_TO_ONE_REQUEST";
/**
* 发送给的用户
*/
private String toUser;
/**
* 消息编号
*/
private String msgId;
/**
* 发送的内容
*/
private String content;
public String getToUser() {
return toUser;
}
public void setToUser(String toUser) {
this.toUser = toUser;
}
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId = msgId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
在服务端接收到发送消息的请求,需要异步响应发送是否成功。所以,创建 SendResponse 类,发送消息响应结果的 Message 。代码如下:
public class SendResponse implements Message{
public static final String TYPE = "SEND_RESPONSE";
/**
* 消息编号
*/
private String msgId;
/**
* 响应状态码
*/
private Integer code;
/**
* 响应提示
*/
private String message;
}
在服务端接收到发送消息的请求,需要转发消息给对应的人。所以,创建 SendToUserRequest 类,发送消息给一个用户的 Message 。代码如下:
public class SendToUserRequest implements Message{
public static final String TYPE = "SEND_TO_USER_REQUEST";
/**
* 消息编号
*/
private String msgId;
/**
* 内容
*/
private String content;
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId = msgId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
每个客户端发起的 Message 消息类型,我们会声明对应的 MessageHandler 消息处理器。这个就类似在 SpringMVC 中,每个 API 接口对应一个 Controller 的 Method 方法。
创建 MessageHandler 接口,消息处理器接口。定义了泛型 ,需要是 Message 的实现类。定义的两个接口方法。代码如下:
public interface MessageHandler<T extends Message> {
/**
* 执行处理消息
* @param session 会话
* @param message 消息
*/
void execute(WebSocketSession session, T message);
/**
* 消息类型,即每个 Message 实现类上的 TYPE 静态字段
* @return
*/
String getType();
}
创建 AuthMessageHandler 类,处理 AuthRequest 消息。代码如下:
@Component
public class AuthMessageHandler implements MessageHandler<AuthRequest>{
@Override
public void execute(WebSocketSession session, AuthRequest message) {
// 如果未传递 accessToken
if (StringUtils.isEmpty(message.getAccessToken())) {
AuthResponse authResponse = new AuthResponse();
authResponse.setCode(1);
authResponse.setMessage("认证 accessToken 未传入");
WebSocketUtil.send(session, AuthResponse.TYPE,authResponse);
return;
}
// 添加到 WebSocketUtil 中,考虑到代码简化,我们先直接使用 accessToken 作为 User
WebSocketUtil.addSession(session, message.getAccessToken());
// 判断是否认证成功。这里,假装直接成功
AuthResponse authResponse = new AuthResponse();
authResponse.setCode(0);
WebSocketUtil.send(session, AuthResponse.TYPE, authResponse);
}
@Override
public String getType() {
return AuthRequest.TYPE;
}
}
创建 SendToOneHandler 类,处理 SendToOneRequest 消息。代码如下:
@Component
public class SendToOneHandler implements MessageHandler<SendToOneRequest>{
@Override
public void execute(WebSocketSession session, SendToOneRequest message) {
// 这里,假装直接成功
SendResponse sendResponse = new SendResponse();
sendResponse.setMsgId(message.getMsgId());
sendResponse.setCode(0);
WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
// 创建转发的消息
SendToUserRequest sendToUserRequest = new SendToUserRequest();
sendToUserRequest.setMsgId(message.getMsgId());
sendToUserRequest.setContent(message.getContent());
// 广播发送
WebSocketUtil.send(message.getToUser(), SendToUserRequest.TYPE, sendToUserRequest);
}
@Override
public String getType() {
return SendToOneRequest.TYPE;
}
}
创建 SendToAllHandler 类,处理 SendToAllRequest 消息。代码如下:
@Component
public class SendToAllHandler implements MessageHandler<SendToAllRequest> {
@Override
public void execute(WebSocketSession session, SendToAllRequest message) {
// 这里,假装直接成功
SendResponse sendResponse = new SendResponse();
sendResponse.setMsgId(message.getMsgId());
sendResponse.setCode(0);
WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
// 创建转发的消息
SendToUserRequest sendToUserRequest = new SendToUserRequest();
sendToUserRequest.setMsgId(message.getMsgId());
sendToUserRequest.setContent(message.getContent());
// 广播发送
WebSocketUtil.broadcast(SendToUserRequest.TYPE, sendToUserRequest);
}
@Override
public String getType() {
return SendToAllRequest.TYPE;
}
}
创建 WebSocketUtil 工具类,代码如下,主要提供两方面的功能:
public class WebSocketUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);
/**
* Session 与用户的映射
*/
private static final Map<WebSocketSession, String> SESSION_USER_MAP = new ConcurrentHashMap<>();
/**
* 用户与 Session 的映射
*/
private static final Map<String, WebSocketSession> USER_SESSION_MAP = new ConcurrentHashMap<>();
/**
* 添加 Session 。在这个方法中,会添加用户和 Session 之间的映射
* @param session Session
* @param user 用户
*/
public static void addSession(WebSocketSession session, String user) {
// 更新 USER_SESSION_MAP
USER_SESSION_MAP.put(user, session);
// 更新 SESSION_USER_MAP
SESSION_USER_MAP.put(session, user);
}
/**
* 发送消息给单个用户的 Session
* @param session Session
* @param type 消息类型
* @param message 消息体
* @param 消息类型
*/
public static <T extends Message> void send(WebSocketSession session, String type, T message) {
// 创建消息
TextMessage messageText = buildTextMessage(type, message);
// 遍历给单个 Session ,进行逐个发送
sendTextMessage(session, messageText);
}
/**
* 广播发送消息给所有在线用户
* @param type 消息类型
* @param message 消息体
* @param 消息类型
*/
public static <T extends Message> void broadcast(String type, T message) {
// 创建消息
TextMessage messageText = buildTextMessage(type, message);
// 遍历 SESSION_USER_MAP ,进行逐个发送
for (WebSocketSession session : SESSION_USER_MAP.keySet()) {
sendTextMessage(session, messageText);
}
}
/**
* 发送消息给指定用户
* @param user 指定用户
* @param type 消息类型
* @param message 消息体
* @param 消息类型
* @return 发送是否成功
*/
public static <T extends Message> boolean send(String user, String type, T message) {
// 获得用户对应的 Session
WebSocketSession session = USER_SESSION_MAP.get(user);
if (session == null) {
LOGGER.error("[send][user({}) 不存在对应的 session]", user);
return false;
}
// 发送消息
send(session, type, message);
return true;
}
/**
* 构建完整的消息
* @param type 消息类型
* @param message 消息体
* @param 消息类型
* @return 消息
*/
private static <T extends Message> TextMessage buildTextMessage(String type, T message) {
JSONObject messageObject = new JSONObject();
messageObject.put("type", type);
messageObject.put("body", message);
return new TextMessage(messageObject.toString());
}
/**
* 真正发送消息
*
* @param session Session
* @param textMessage 消息
*/
private static void sendTextMessage(WebSocketSession session, TextMessage textMessage) {
if (session == null) {
LOGGER.error("[sendTextMessage][session 为 null]");
return;
}
try {
session.sendMessage(textMessage);
} catch (IOException e) {
LOGGER.error("[sendTextMessage][session({}) 发送消息{}) 发生异常",
session, textMessage, e);
}
}
}
处理类,在Spring中,处理消息的具体业务逻辑,进行开启、关闭连接等操作。
public class MyHandler extends TextWebSocketHandler implements InitializingBean {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 消息类型与 MessageHandler 的映射
* 无需设置成静态变量
*/
private final Map<String, MessageHandler> HANDLERS = new HashMap<>();
@Autowired
private ApplicationContext applicationContext;
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
System.out.println("获取到消息 >> " + message.getPayload());
logger.info("[handleMessage][session({}) 接收到一条消息({})]", session, message);
// 获得消息类型
JSONObject jsonMessage = JSON.parseObject(message.getPayload());
String messageType = jsonMessage.getString("type");
// 获得消息处理器
MessageHandler messageHandler = HANDLERS.get(messageType);
if (messageHandler == null) {
logger.error("[onMessage][消息类型({}) 不存在消息处理器]", messageType);
return;
}
// 解析消息
Class<? extends Message> messageClass = this.getMessageClass(messageHandler);
// 处理消息
Message messageObj = JSON.parseObject(jsonMessage.getString("body"), messageClass);
messageHandler.execute(session, messageObj);
}
/**
* 连接建立时触发
**/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("[afterConnectionEstablished][session({}) 接入]", session);
// 解析 accessToken
String accessToken = (String) session.getAttributes().get("accessToken");
// 创建 AuthRequest 消息类型
AuthRequest authRequest = new AuthRequest();
authRequest.setAccessToken(accessToken);
// 获得消息处理器
MessageHandler<AuthRequest> messageHandler = HANDLERS.get(AuthRequest.TYPE);
if (messageHandler == null) {
logger.error("[onOpen][认证消息类型,不存在消息处理器]");
return;
}
messageHandler.execute(session, authRequest);
}
/**
* 关闭连接时触发
**/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("断开连接!");
}
@Override
public void afterPropertiesSet() throws Exception {
// 通过 ApplicationContext 获得所有 MessageHandler Bean
applicationContext.getBeansOfType(MessageHandler.class).values()
// 添加到 handlers 中
.forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler));
logger.info("[afterPropertiesSet][消息处理器数量:{}]", HANDLERS.size());
}
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 {
throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
}
}
}
}
}
throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
}
}
在Spring中提供了websocket拦截器,可以在建立连接之前写些业务逻辑,比如校验登录等。
public class MyHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
/**
* @Description 握手之前,若返回false,则不建立链接
* @Date 21:59 2021/5/16
* @return boolean
**/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse,
WebSocketHandler webSocketHandler,
Map<String, Object> attributes) throws Exception {
//获得 accessToken ,将用户id放入socket处理器的会话(WebSocketSession)中
if (serverHttpRequest instanceof ServletServerHttpRequest) {
ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) serverHttpRequest;
attributes.put("accessToken", serverRequest.getServletRequest().getParameter("accessToken"));
}
// 调用父方法,继续执行逻辑
return super.beforeHandshake(serverHttpRequest, serverHttpResponse, webSocketHandler, attributes);
}
开启spring websocket功能。
@Configuration
@EnableWebSocket //开启spring websocket功能
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//配置处理器
registry.addHandler(this.myHandler(), "/")
//配置拦截器
.addInterceptors(new MyHandshakeInterceptor())
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
@Bean
public MyHandshakeInterceptor webSocketShakeInterceptor() {
return new MyHandshakeInterceptor();
}
}
创建SpringBoot启动类
@SpringBootApplication
public class MyWebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(MyWebsocketApplication.class,args);
}
}
打开三个浏览器,输入在线测试websocket地址
创建三个连接。分别设置服务地址如下:
ws://localhost:8080/?accessToken=1001
ws://localhost:8080/?accessToken=1002
ws://localhost:8080/?accessToken=1003
发送单人消息
{
tpye: "SEND_TO_ONE_REQUEST",
boby: {
toUser: "1002",
msgId: "qwwerqrsfd123",
centent: "这是1001发送给1002的单聊消息"
}
}
可以看到1002收到了1001发的单聊信息,1003未收到。效果图如下:
发送多人消息
{
tpye: "SEND_TO_ALL_REQUEST",
boby: {
msgId: "qwerqcfwwerqrsfd123",
centent: "我是一条群聊消息"
}
}
可以看到1001,1002,1003都收到了消息,效果图如下:
好了,以上就是今天要讲的内容,本文介绍了WebSocket协议以及使用它简单的实现及时聊天的场景。
喜欢的朋友,欢迎点赞支持一下。
感谢大家的阅读,如果有什么疑问或者建议,给我留言或者加我个人微信:xiliudd,做个朋友圈点赞之交
喜欢的朋友也可以扫码关注我,更多精彩内容等你~