SpringBoot坚持学习第四天:集成WebSocket
一、WebSocket第一次使用
首先要掌握的是webSocket的4个事件。
open event | Sokcket @OnOpen | 连接建立时触发 |
message event | Sokcket @OnMessage | 客户端接收服务端数据时触发 |
error event | Sokcket @OnError | 通讯发生错误时触发 |
close event | Sokcket @OnClose | 链接关闭时触发 |
先把结论说在最前面:
(1)前端JS里面 在new WebSocket(url)的时候,与后台某个@ServerEndpoint注解类建立连接。此类触发@OnOpen注解方法。方法上可以编写一个javax.websocket.Session作为接收参数,是有值的,必须将其存放到一个全局对象中。我跟踪代码后发现,每次建立连接都创建@ServerEndpoint注解类的一个对象,所以session放到全局对象中就很有必要。
(2)前端向后台发送消息。 在JS中,WebSocket对象调用 ws.send("message"); 在后台代码中,@OnMessage注解方法就会执行。一般情况下,前端期望得到后台的响应。后台可以使用javax.websocket.Session的相关方法,向所有其它用户(session)发送消息。如果对某个session发送消息,可视为"私聊"。原来私聊的实现就是这么简单。
(3)前端在接收到后台的响应信息的时候,将会触发message事件。一般在message 事件中,将后台发送的消息呈现在大屏幕上。
(4)断开连接。前端JS在执行ws.close()方法的时候,将会让后台@OnClose注解方法执行。此方法能够获取到javax.websocket.Session,从而将其从全局变量中移除,并调用此session对象的close()方法。
在JavaScript中使用WebSocket
var ws = new WebSocket("ws://localhost:8080/url");
ws.onopen = function(evt) {
console.log("Connection open ...");
//前端向后台发送消息
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
//前端接收后台消息
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
二、页面学习
今天学习了如何使用WebSocket。同时也学习了如何使用Bootstrap搭建较为漂亮的页面。
原来,在页面中使用bootstrap非常的简单,只需要引入JQuery.js与bootstrap.css就行了。
以后我将尽量把页面弄的漂亮一点,原因是知识只有越用越活。
原来,body是用container修饰的,form表单使用 form-group修饰的,input标签使用 form - control 修饰的,按钮是可以带颜色的btn btn-success。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室</title>
<link rel="stylesheet" href="bootstrap.min.css">
<script src="jquery-3.2.1.min.js"></script>
</head>
<body class="container" style="width: 60%">
<div class="form-group"></br>
<h5>聊天室</h5>
<textarea id="message_content" class="form-control" readonly cols="50" rows="10"></textarea>
</div>
<div class="form-group">
<label for="user_name">你的名字</label>
<input class="form-control" id="user_name"><br>
<button id="btn_join" class="btn btn-success">加入聊天室</button>
<button id="btn_left" class="btn btn-warning">离开聊天室</button>
</div>
<!-- 表单样式:form-group -->
<!-- 普通组件样式:form-contorl -->
<!-- 按钮颜色 btn btn-success -->
<div class="form-group">
<label for="user_message">发送消息</label>
<input class="form-control" id="user_message">
<button id="send_all_message" class="btn btn-info">发送消息</button>
</div>
</body>
<script>
//页面一加载就执行的JS
$(document).ready(
function () {
//需要以ws开头,后台Controller代码需要监听此请求
var urlPrefix = 'ws://localhost:8080/chatroom/';
//定义全局变量ws
var ws = null;
$('#btn_join').click(function () {
var username = $('#user_name').val();
if (username == '') {
alert("请输入用户名!");
return;
}
//拼接URL,Controller中监听此请求 @ServerEndpoint("/chat-room/{username}")
var url = urlPrefix + username;
//new 一个url的时候,就会向后台发送一个URL请求
//会被@OnOpen注解捕获到
ws = new WebSocket(url);
//为此对象配置全局监听器
ws.onopen = function () {
console.log("建立连接");
};
//核心:当后台发送给前端消息的时候,触发此事件
ws.onmessage = function (event) {
debugger;
$("#message_content").append(event.data + '\n')
}
//当ws对象关闭的时候触发
ws.onclose = function () {
$('#message_content').append('用户[' + username + '] 已经离开聊天室!');
console.log("关闭 websocket 连接...");
}
});
$("#send_all_message").click(function () {
var msg = $('#user_message').val();
if(msg==''){
alert("请填写消息!");
return;
}
//获取全局变量WebSocket
if(ws && msg!=''){
//核心:使用ws向后台发送消息,触发后台的@OnMessage注解代码
ws.send(msg);
}
})
//退出聊天室
$('#btn_left').click(function(){
if(ws){
debugger;
ws.close();
}
});
}
)
</script>
</html>
三、如何在SpringBoot中使用WebSocket
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
(1)在启动类上新增一个@EnableWebSocket注解,并配置一个@Bean。后来我发现这个@Bean正是配置在其它Controller上的类似注解。
@SpringBootApplication
@EnableWebSocket //启用WebSocket
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean //监听WebSocket的Controller类上的注解与这个Bean长得很像
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
我在跟踪代码后发现,每次生成的webSocket后台对象都是一个独立的,所以我之前尝试将存放Map<username,session>的map设置为使用@ServerEndpoint("/chatroom/{username}") 注解的类的私有变量,发现每次这个map打断点进来都是size=0。最终解决办法就是创建一个全局类,使用static修饰。
package com.websocket.demo;
import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author jay.zhou
* @date 2019/1/5
* @time 10:27
*/
public class WebSocketUtils {
/**
* 使用ConcurrentHashMap可以提高并发效率,
* https://blog.csdn.net/yanluandai1985/article/details/83051643
*/
public static Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
private WebSocketUtils() {
}
public static void sendMessageAll(String message) {
//这里提供了一种快速遍历Map集合的代码,值得学习
ONLINE_USER_SESSIONS.forEach((username, session) -> sendMessage(session, message));
}
/**
* 核心:后端服务器向前端发送数据
*
* @param session 用户的session
* @param message 发送的消息
*/
public static void sendMessage(Session session, String message) {
if (session == null) {
return;
}
//核心:通过session向前端页面发送数据,前端将会触发message事件
final RemoteEndpoint.Basic basic = session.getBasicRemote();
try {
//核心:后台发送数据到前台,触发前台的Message事件
basic.sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
下面就是核心的类了,每次创建一个前端的websocket对象,后台也会创建一个使用@ServerEndpoint注解类的对象,前后端websocket对象一一对应。后台@ServerEndpoint注解类的4个注解方法需要掌握。
本例也是第一次在实例中使用并发包下的ConcurrentHashMap,终于把储备的知识用起来了。同时也掌握了快速遍历Map集合的方法,前几天一直困惑遍历Map集合很麻烦该怎么解决,这次终于见到大牛的代码,很佩服。
package com.websocket.demo.web;
import org.springframework.web.bind.annotation.RestController;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import static com.websocket.demo.WebSocketUtils.ONLINE_USER_SESSIONS;
import static com.websocket.demo.WebSocketUtils.sendMessageAll;
/**
* @author jay.zhou
* @date 2019/1/4
* @time 19:06
*/
@RestController
@ServerEndpoint("/chatroom/{username}")
public class WsController {
/**
* 核心:javax.websocket.Session通过WEB环境自动注入
* 注意注解:@PathParam 不是@PathVariable
*
* @param username 从URL路径中解析的数据
* @param session 由WEB环境生成的websocket的session
*/
@OnOpen
public void openSession(@PathParam("username") String username, Session session) {
//存入map
ONLINE_USER_SESSIONS.put(username, session);
String message = "欢迎用户[" + username + "] 来到聊天室!";
sendMessageAll(message);
}
/**
* 核心:捕获前端ws发送给后台服务器的数据
* 前端:var ws = new WebSocket(url); ws.send(message);
*
* @param username 从URL路径中解析的数据
* @param message 捕获前台的消息
*/
@OnMessage
public void onMessage(@PathParam("username") String username, String message) {
//basic由session生成,为每个session都发消息
//basic.sendText(message);
sendMessageAll("用户[" + username + "] : " + message);
}
@OnClose
public void onClose(@PathParam("username") String username, Session session) {
//当前的Session 移除
ONLINE_USER_SESSIONS.remove(username);
//通知所有的session
sendMessageAll("用户[" + username + "] 已经离开聊天室了!");
//还需要关闭session
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
}
}
}