websocket
前言:
最近因为一个项目需要:采用websocket进行数据传输,数字大屏上信息进行实时显示。就最近对websocket的学习和使用做记录。
修行之路艰辛,与君共勉。
1.websocket基础
首先是老生常谈的websocket相关的基础知识。
1.1 基础概念
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。
——百度百科
对于一些需要实时数据刷新的应用场景,采用原始的post/get请求,然后在使用ajax进行轮询是能够实现上诉需求的。但是采用轮询的方式会极大的增加服务器的负载。并且会导致过多不必要的请求,浪费流量和服务器资源,每一次请求、应答,都浪费了一定流量在相同的头部信息上。
所有针对上诉问题,websocket横空出世,几乎完美的解决了上诉问题。
websocket的核心功能:即时通讯,替代轮询
1.2 原理
WebSocket同HTTP一样也是应用层的协议,但是它是一种****双向通信协议****,是建立在TCP之上的。
连接过程 —— 握手过程
- 浏览器、服务器建立TCP连接,三次握手。这是通信的基础,传输控制层,若失败后续都不执行。
- TCP连接成功后,浏览器通过HTTP协议向服务器传送WebSocket支持的版本号等信息。(****开始前的HTTP握手****)
- 服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据。
- 当收到了连接成功的消息后,通过TCP通道进行传输通信。
2.基础应用
websocket的基础应用:聊天室的实现
技术实现:websocket+Spring Boot
2.1 添加对应的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.2 创建websocket的配置文件
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.3 创建对应的websocket服务
@ServerEndpoint(value = "/websocket")
@Component
public class WebSocketServer {
// 统计在线人数
private static int onlineCount = 0;
// 用本地线程保存session
private static ThreadLocal<Session> sessions = new ThreadLocal<Session>();
// 保存所有连接上的session
private static Map<String, Session> sessionMap = new ConcurrentHashMap<String, Session>();
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
onlineCount++;
}
public static synchronized void subOnlineCount() {
onlineCount--;
}
// 连接
@OnOpen
public void onOpen(Session session) {
sessions.set(session);
addOnlineCount();
sessionMap.put(session.getId(), session);
System.out.println("【" + session.getId() + "】连接上服务器======当前在线人数【" + getOnlineCount() + "】");
}
// 关闭连接
@OnClose
public void onClose(Session session) {
subOnlineCount();
sessionMap.remove(session.getId());
System.out.println("【" + session.getId() + "】退出了连接======当前在线人数【" + getOnlineCount() + "】");
}
// 接收消息 客户端发送过来的消息
@OnMessage
public void onMessage(String message, Session session) {
//message的格式可以包含sessionid, 格式[SID,内容XXX]:1,内容
System.out.println("【" + session.getId() + "】客户端的发送消息======内容【" + message + "】");
String[] split = message.split(",");
String sessionId = split[0];
Session ss = sessionMap.get(sessionId);
if (ss != null) {
String msgTo = "【" + session.getId() + "】发送给【您】的消息:\n【" + split[1] + "】";
String msgMe = "【我】发送消息给【" + ss.getId() + "】:\n" + split[1];
sendMsg(ss, msgTo);
sendMsg(session, msgMe);
} else {
for (Session s : sessionMap.values()) {
if (!s.getId().equals(session.getId())) {
sendMsg(s, "【" + session.getId() + "】发送给【您】的广播消息:\n【" + message + "】");
} else {
sendMsg(session, "【我】发送广播消息给大家\n" + message);
}
}
}
}
// 异常
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("发生异常!");
throwable.printStackTrace();
}
// 发送消息到指定用户的方法
public synchronized void sendMsg(Session session, String msg) {
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
//广播发送消息
public synchronized void sendMsgToAll(String msg) {
try {
for (Session s : sessionMap.values()) {
s.getBasicRemote().sendText(msg);
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.4 创建对应的controller,用于数据发送
@Controller
public class MyController {
@Autowired
private WebSocketServer webSocketServer;
@RequestMapping("/send.do")
public ModelAndView send(){
ModelAndView modelAndView = new ModelAndView("/send.html");
webSocketServer.sendMsgToAll("Hello World");
return modelAndView;
}
}
2.5 前台采用对应的js代码进行连接
var ws = new WebSocket("你的socket地址");
ws.onopen = function(evt) { //绑定连接事件
console.log("Connection open ...");
ws.send("发送的数据");
};
ws.onmessage = function(evt) {//绑定收到消息事件
console.log( "Received Message: " + evt.data);
};
ws.onclose = function(evt) { //绑定关闭或断开连接事件
console.log("Connection closed.");
};
3.需求应用
核心来了。如何使用websocket完成我的需求:数字大屏的数据实时显示。
首先websocket的工作是建立连接,推送数据。但是数据来源就需要我们的另外一模块:定时任务(qrtz_job)
设计思想:
在服务端创建一个websocket服务。然后客户端进行连接。
1.客户端发起连接请求,服务端接收到连接请求后,解析session,获取对应的sessionId,token(权限控制),API的具体执行参数。
2.服务端根据解析出来的参数,创建对应的定时任务,并且将session、定时任务、API3者的一对一关系存放在数据表中。
3.定时任务创建完成后,启动定时任务,在定时任务的执行函数中,获取到该定时任务的详细信息。包括API的执行信息,session等。
3.根据API的执行信息(SQL语句,数据源类型等),执行该API,获取对应结果,然后调用socket中的特定推送方法,根据sessionId特定推送给客户端。根据时间间隔,定时执行该方法,实现动态推送数据的效果
注:API的调用执行,放在下一次在详细记录。
4.最后,客户端发起断开连接,服务端接收到断开连接请求,根据对应的sessionId,通过数据表查询到对应的定时任务,停止任务,最后删除任务,并且记录连接/断开信息。
3.2 核心源码
核心的websocket源码
public class ExecSocket implements WebSocketHandler {
private static final Logger logger;
@Autowired
private SysWebsocketService sysWebsocketService;
private static final Set<WebSocketSession> sessions;
static {
sessions = new ConcurrentHashSet<>();
logger = LoggerFactory.getLogger(ExecSocket.class);
}
//建立websocket连接时触发
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("建立连接");
//创建对应的定时任务,解析session,获取对应的socketId,以及输入参数params
//解析session,获取参数,调用方法,创建并启动定时器
String url=session.getUri().toString();
Map<String, String> map=new HashMap<>();
map.put("sessionId",session.getId());
//String[]
String[] str=url.split("&");
for(int i=0;i<str.length;i++){
String[] str1=str[i].split("=");
if(str1.length>=2){
map.put(str1[0],str1[1]);
}
}
//该方法就是创建定时任务,并且启动定时任务
sysWebsocketService.startWebSocket(map);
//保存定时器和session一一对应的关系。
sessions.add(session);
}
//接收js侧发送来的用户信息
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> socketMessage) throws Exception {
String message = socketMessage.getPayload().toString();
System.out.println(message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if(session.isOpen()){
session.close();
}
logger.debug("websocket connection closed......");
sessions.remove(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
logger.debug("websocket connection closed......");
//关闭连接,停止/删除定时任务
sysWebsocketService.stopWebSocket(session.getId());
sessions.remove(session);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 给所用户发送消息
*
* @param message
*/
public void sendMessageToAllUsers( String message) {
for (WebSocketSession user : sessions) {
try {
if (user.isOpen()) {
user.sendMessage(new TextMessage(message));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 给特定用户发送消息,并且通过session和定时任务一一对应
* 根据不同任务,将数据分发给不同的session
* @param message
* @param sessionId
*/
public void sendMessageToSpecificUser(String message,String sessionId){
WebSocketSession session = null;
for (WebSocketSession user:sessions){
if(user.getId().equals(sessionId)){
session=user;
}
}
try {
if(session.isOpen()){
session.sendMessage(new TextMessage(message));
}
}catch (IOException e){
e.printStackTrace();
}
}
}
定时任务的核心执行代码
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Map<String,Object> data = (Map<String,Object>)jobExecutionContext.getJobDetail().getJobDataMap().get("data");
System.out.println(data);
//获取对应API信息,执行,返回结果
String sysApiId=(String)data.get("apiId");
SysApi sysApi=sysApiService.queryById(sysApiId);
DataSource dataSource=dataSourceService.get(sysApi.getDsId());
String sql=(String)data.get("sql");
//该方法为核心的API执行方法
Map<String,Object> ret=apiExecService.doExec(dataSource.getEnName(),sql,sysApi.getType());
System.out.println(ret);
String sessionId=(String)data.get("sessionId");
//调用推送给特定用户的方法
execSocket.sendMessageToSpecificUser(ret.toString(),sessionId);
//推送给所有用户
//execSocket.sendMessageToAllUsers(ret.toString());
}