使用websocket实现服务端推送消息到客户端
现在很多web网站上都有站内消息通知,用于给用户及时推送站内信消息。大多数是在网页头部导航栏上带一个小铃铛图标,有新的消息时,铃铛会出现相应提示,用于提醒用户查看。例如下图:
我们都知道,web应用都是C/S模式,客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现给用户。所以很容易想到的一种解决方式就是:
而对于Ajax轮询方案,优点是实现起来简单,适用于对消息实时性要求不高,用户量小的场景下,缺点就是客户端给服务器带来很多无谓请求,浪费带宽,效率低下,做不到服务端的主动推送
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建长连接,并进行双向数据传输。
springboot集成websocket作为服务端,非常简单,以下以springboot 2.2.0版本为例:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
package com.learn.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
package com.learn.demo.ws;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ServerEndpoint(value = "/testWebSocket/{id}")
public class WebSocketProcess {
/*
* 持有每个webSocket对象,以key-value存储到线程安全ConcurrentHashMap,
*/
private static ConcurrentHashMap<Long, WebSocketProcess> concurrentHashMap = new ConcurrentHashMap<>(12);
/**
* 会话对象
**/
private Session session;
/*
* 客户端创建连接时触发
* */
@OnOpen
public void onOpen(Session session, @PathParam("id") long id) {
//每新建立一个连接,就把当前客户id为key,this为value存储到map中
this.session = session;
concurrentHashMap.put(id, this);
log.info("Open a websocket. id={}", id);
}
/**
* 客户端连接关闭时触发
**/
@OnClose
public void onClose(Session session, @PathParam("id") long id) {
//客户端连接关闭时,移除map中存储的键值对
concurrentHashMap.remove(id);
log.info("close a websocket, concurrentHashMap remove sessionId= {}", id);
}
/**
* 接收到客户端消息时触发
*/
@OnMessage
public void onMessage(String message, @PathParam("id") String id) {
log.info("receive a message from client id={},msg={}", id, message);
}
/**
* 连接发生异常时候触发
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("Error while websocket. ", error);
}
/**
* 发送消息到指定客户端
* @param id
* @param message
* */
public void sendMessage(long id, String message) throws Exception {
//根据id,从map中获取存储的webSocket对象
WebSocketProcess webSocketProcess = concurrentHashMap.get(id);
if (!ObjectUtils.isEmpty(webSocketProcess)) {
//当客户端是Open状态时,才能发送消息
if (webSocketProcess.session.isOpen()) {
webSocketProcess.session.getBasicRemote().sendText(message);
} else {
log.error("websocket session={} is closed ", id);
}
} else {
log.error("websocket session={} is not exit ", id);
}
}
/**
* 发送消息到所有客户端
*
* */
public void sendAllMessage(String msg) throws Exception {
log.info("online client count={}", concurrentHashMap.size());
Set<Map.Entry<Long, WebSocketProcess>> entries = concurrentHashMap.entrySet();
for (Map.Entry<Long, WebSocketProcess> entry : entries) {
Long cid = entry.getKey();
WebSocketProcess webSocketProcess = entry.getValue();
boolean sessionOpen = webSocketProcess.session.isOpen();
if (sessionOpen) {
webSocketProcess.session.getBasicRemote().sendText(msg);
} else {
log.info("cid={} is closed,ignore send text", cid);
}
}
}
}
@ServerEndpoint(value = “/testWebSocket/{id}”)注解,声明并创建了webSocket端点,并且指明了请求路径为 “/testWebSocket/{id}”,id为客户端请求时携带的参数,用于服务端区分客户端使用。
package com.learn.demo.controller;
import com.learn.demo.ws.WebSocketProcess;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/ws")
public class WebSocketController {
/**
*注入WebSocketProcess
**/
@Autowired
private WebSocketProcess webSocketProcess;
/**
* 向指定客户端发消息
* @param id
* @param msg
*/
@PostMapping(value = "sendMsgToClientById")
public void sendMsgToClientById(@RequestParam long id, @RequestParam String text){
try {
webSocketProcess.sendMessage(id,text);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发消息到所有客户端
* @param msg
*/
@PostMapping(value = "sendMsgToAllClient")
public void sendMsgToAllClient( @RequestParam String text){
try {
webSocketProcess.sendAllMessage(text);
} catch (Exception e) {
e.printStackTrace();
}
}
}
HTML5 提供了对websocket的支持,并且提供了相关api,可以直接使用
//url就是服务端的websocket端点路径, protocol 是可选的,指定了可接受的子协议
var Socket = new WebSocket(url, [protocol] );
事件 | 事件处理程序 | 描述 |
---|---|---|
open | Socket.onopen | 连接建立时触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通信发生错误时触发 |
close | Socket.onclose | 连接关闭时触发 |
方法 | 描述 |
---|---|
Socket.send() | 使用连接发送数据 |
Socket.close() | 关闭连接 |
以下是简单的例子,我们使用随机数,模拟客户端ID
<html>
<head>
<meta charset="UTF-8">
<title>websocket测试title>
<script src="http://code.jquery.com/jquery-2.1.1.min.js">script>
head>
<body>
<div id="content">div>
body>
<script type="text/javascript">
$(function(){
var ws;
//检测浏览器是否支持webSocket
if("WebSocket" in window){
$("#content").html("您的浏览器支持webSocket!");
//模拟产生clientID
let clientID = Math.ceil(Math.random()*100);
//创建 WebSocket 对象,注意请求路径!!!!
ws = new WebSocket("ws://127.0.0.1:8080/testWebSocket/"+clientID);
//与服务端建立连接时触发
ws.onopen = function(){
$("#content").append("与服务端建立连接建立成功!您的客户端ID="
+clientID+"");
//模拟发送数据到服务器
ws.send("你好服务端!我是客户端 "+clientID);
}
//接收到服务端消息时触发
ws.onmessage = function (evt) {
let received_msg = evt.data;
$("#content").append("接收到服务端消息:"
+received_msg+"");
};
//服务端关闭连接时触发
ws.onclose = function() {
console.error("连接已经关闭.....")
};
}else{
$("#content").html("您的浏览器不支持webSocket!");
}
})
script>
html>
1.首先启动服务端,springboot默认端口8080,观察是否有报错。
Tomcat started on port(s): 8080 (http) with context path ''
2019-11-10 16:31:35.496 INFO 16412 --- [ restartedMain] com.learn.demo.DemoApplication : Started DemoApplication in 3.828 seconds (JVM running for 6.052)
2019-11-10 16:31:45.006 INFO 16412 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-11-10 16:31:45.006 INFO 16412 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2019-11-10 16:31:45.011 INFO 16412 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
2.打开html界面,这里同时打开了三个界面做模拟使用
服务端可以看到日志,证明websocket连接已经成功建立
[nio-8080-exec-1] com.learn.demo.ws.WebSocketProcess : Open a websocket. id=26
2019-11-10 16:31:45.076 INFO 16412 --- [nio-8080-exec-1] com.learn.demo.ws.WebSocketProcess : Receive a message from client id=26,msg=你好服务端!我是客户端 26
2019-11-10 16:31:46.338 INFO 16412 --- [nio-8080-exec-2] com.learn.demo.ws.WebSocketProcess : Open a websocket. id=62
2019-11-10 16:31:46.338 INFO 16412 --- [nio-8080-exec-2] com.learn.demo.ws.WebSocketProcess : Receive a message from client id=62,msg=你好服务端!我是客户端 62
2019-11-10 16:31:48.052 INFO 16412 --- [nio-8080-exec-3] com.learn.demo.ws.WebSocketProcess : Open a websocket. id=79
2019-11-10 16:31:48.059 INFO 16412 --- [nio-8080-exec-4] com.learn.demo.ws.WebSocketProcess : Receive a message from client id=79,msg=你好服务端!我是客户端 79
3.向所有客户端推送消息,这里使用postman做测试,请求服务端的sendMsgToAllClient接口
可以看到,刚刚打开的三个html界面上,都及时接受到了服务端发送的消息。
3.向指定客户端推送消息,请求服务端的sendMsgToClientById接口
可以看到客户端ID=79的,收到了我们的推送消息,其它的没变化。
每项技术带来优点的同时,同时也会附带缺点,目前来看websocket的一些小问题:
所以,具体看实际场景需求,选择合适方案。