一起来学SpringBoot(十八)轻松集成WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

springboot提供了相关整合starter

<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

基础配置

我们就试着配置使用一下

@RestController
@ServerEndpoint(value = "/tipschant")
@Slf4j
public class WebScoketController {
@OnOpen
public void openSession(Session session) {
log.info("打开WebScoket");
}

@OnMessage
public void onMessage(String message) {
log.info("发送的信息是:"+message);
}

@OnClose
public void onClose(Session session) {
log.info("关闭了连接");
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}

@OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
log.info("出错了:{}",e.getMessage());
e.printStackTrace();
}
throwable.printStackTrace();
}

}

**@ServerEndpoint ** 中的内容就是 WebSocket 协议的地址

  • HTTP 协议:http://localhost:8080/path
  • WebSocket 协议:ws://localhost:8080/path

*@OnOpen、@OnMessage、@OnClose、@OnError * 注解与 WebSocket 中监听事件是相对应的。

  • onopen 建立 WebSocket 连接时触发。
  • message 客户端监听服务端事件,当服务端向客户端推送消息时会被监听到。
  • error WebSocket 发生错误时触发。
  • close 关闭 WebSocket 连接时触发。

然后在配置一个Bean就行了

@Configuration
public class SpringWebSocketConfig{
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

扩展一些

与 AJAX 轮训的方式差不多,但长连接不像 AJAX 轮训一样,而是采用的阻塞模型(一直打电话,没收到就不挂电话);客户端发起连接后,如果没消息,就一直不返回 Response 给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

在没有 WebSocket 之前,大家常用的手段应该就是轮训了,比如每隔几秒发起一次请求,但这样带来的就是高性能开销,都知道一次 HTTP 响应是需要经过三次握手和四次挥手,远不如 TCP 长连接来的划算。

假设我们要做一个 后台管理系统登陆在线发信息的玩意怎么办呢?因为在之后的服务器消息类中要用到用户信息,所以要提前加入用户信息

可以这样玩

/**
* (websocket配置)
* @author fulin
* @date 2018年9月15日 上午10:27:58
*/
@Configuration
public class SpringWebSocketConfig extends ServerEndpointConfig.Configurator{

/**
* 修改握手,就是在握手协议建立之前修改其中携带的内容
* @param sec
* @param request
* @param response
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//比如说这里用的是shiro
sec.getUserProperties().put("user", SecurityUtils.getSubject().getPrincipal().toString());
super.modifyHandshake(sec, request, response);
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

然后在呢想用这个配置的话可以这样写

@RestController
@ServerEndpoint(value = "/tipschant", configurator = SpringWebSocketConfig.class)
@Slf4j
public class WebScoketController {
@OnOpen
public void openSession(Session session) {
String user = session.getUserProperties().get("user").toString();
//存入大map
LIVING_SESSIONS_CACHE.put(user, session);
}

@OnMessage
public void onMessage(String message) {
log.info(message);
}

@OnClose
public void onClose(Session session) {
String user = session.getUserProperties().get("user").toString();
LIVING_SESSIONS_CACHE.remove(user);
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}

@OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
e.getMessage();
}
throwable.printStackTrace();
}
}

顺便把我的工具类也贴出来

/**
* @author fulin
* @since 2018/6/26 0026
*/
public final class WebSocketUtils {

/**
* 模拟存储 websocket session 使用
*/
public static final Map<String, Session> LIVING_SESSIONS_CACHE = new ConcurrentHashMap<>();

public static void sendMessageAll(String message) {
LIVING_SESSIONS_CACHE.forEach((sessionId, session) -> sendMessage(session, message));
}

/**
* 发送给指定用户消息
*
* @param session 用户 session
* @param message 发送内容
*/
public static void sendMessage(Session session, String message) {
if (session == null) {
return;
}
final RemoteEndpoint.Basic basic = session.getBasicRemote();
if (basic == null) {
return;
}
try {
basic.sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

然后呢贴一下前台代码

var ws = null;
$(document).ready(function () {
var curWwwPath=window.document.location.href;
var sos=curWwwPath.indexOf("//");
//获取主机地址,如: localhost:8080
var localhostPaht=curWwwPath.substring(sos+2);
//判断支持不支持webSocket
if ('WebSocket' in window) {
ws = new WebSocket("ws://"+localhostPaht+"tipschant");
}
else {
alert('Not support websocket')
}
ws.onopen = function () {
console.log("建立 websocket 连接...");
};
ws.onmessage = function (event) {
//服务端发送的消息
alert(event.data);
};
ws.onclose = function () {
console.log("关闭 websocket 连接...");
};
// 关闭页面的时候管理websocket
window.onbeforeunload = function (event) {
if (ws) {
ws.close();
}
};
});

顺便说一下怎么发消息写一个get请求即可

@GetMapping("/tips/{username}")
public void Message(@PathVariable("username") String username, String message) {
Session session = LIVING_SESSIONS_CACHE.get(username);
sendMessage(session, message);
}

注意

在服务器端消息处理类中如果直接注入会失败,需要通过ApplicationContext对象去获取你想用的类。需要在springboot的启动类中配置:

@SpringBootApplication
public class TestApplication {

private final static Logger logger = LoggerFactory.getLogger(MisApplication.class);

public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(MisApplication.class);
ConfigurableApplicationContext configurableApplicationContext = springApplication.run(args);
UserMessageSocket.setApplicationContext(configurableApplicationContext);//解决WebSocket不能注入的问题
logger.info("MisApplication is success!");
}
}

就搞定了

本博文是基于springboot2.x 如果有什么不对的请在下方留言。