首先介绍下上图的情景:
当我们的前端请求后端子服务的时候,我们的后端子服务去处理相关的逻辑的时候,很有可能因为业务的复杂导致http请求连接超时,而且对于当前时代下,我们都要求高性能系统,用户提交一个请求后,隔了假设20s才响应了,显然也是不合理的。
传统的方式解决上述问题:
前端在发送请求后,我们的子服务可以立即响应返回,但是返回的是请求成功而不是处理成功,然后在后端的业务逻辑处理完毕后,再返回给前端处理成功,这显然符合高性能系统的要求。但是也存在一个问题就是http请求在我们第一次响应后,后端没有能力再去通知前端,而且因为后端业务逻辑处理完成时间的不确定,传统的方式是前端轮询去调这个服务,这样子就会导致服务器的压力大大增加,显然上述的方式虽然能解决,但是我们并不采用。
推介方式解决上述问题:
如上图所示,我们建立一个推送服务(采用websocket协议,有的公司叫做消息中心只是名字的差异),当子服务没有能力将相应的消息推送给前端的时候,可以将推送的消息推送给消息中心,消息中心和前端保持长连接,这样就可以将消息推送给前端。(不同的前端用户我们可以根据用户id和对应的sessionId来建立一种映射关系)
Websocket是一种全双工的协议(服务器可以推送前端,前端也能推送给服务器),只是前端推送后端我们使用的http协议,也就是使用了一半websocket协议。
PS:Websocket协议相关介绍:HTML5 WebSocket | 菜鸟教程
下面使用Java语言在springboot工程中实现:
首先肯定是导入依赖:
<!-- websocket依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
建立后端:
①构建WebSocket
package com.qianfeng.gp09.server;
import com.alibaba.fastjson.JSONObject;
import com.qianfeng.gp09.dao.MessageDao;
import com.qianfeng.gp09.model.Message;
import com.qianfeng.gp09.model.MessageExample;
import com.qianfeng.gp09.util.SpringContextUtil;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* websocket服务器
*
*/
@ServerEndpoint("/webSocket/{uid}")
@Component
public class WebSocketServer {
/**
* 每一个用户都和服务器建立一个会话
* 我们考虑保存会话
* 一个uid对应一个session
* 它应该是一个多线程并发情况下的map
*/
private static ConcurrentHashMap<String,Session> sessionPool=new ConcurrentHashMap<>();
@Resource
private MessageDao messageDao;
/**
* 当建立连接的时候,就运行的方法。
* @param session 会话对象
* @param uid 用户id
*/
@OnOpen
public void onOpen(Session session, @PathParam(value="uid") String uid){
sessionPool.put(uid,session);
//查询uid的消息 state=0 且 overTime是大于当前时间。
MessageExample example=new MessageExample();
MessageExample.Criteria criteria = example.createCriteria();
criteria.andUidEqualTo(uid);
criteria.andStateEqualTo(0);
criteria.andOverTimeGreaterThanOrEqualTo(new Date());
if(messageDao==null){
//手动获取这个对象。
messageDao=SpringContextUtil.getBean(MessageDao.class);
}
List<Message> messages = messageDao.selectByExample(example);
sendMessage(session, JSONObject.toJSONString(messages));
}
/**
* 对方发送给我数据的时候,这个方法就运行。
* @param message 消息内容
*/
@OnMessage
public void onMessage(String message){
//不用。
}
/**
* 当连接关闭时,这个方法就运行
* @param uid 用户id
*/
@OnClose
public void onClose(@PathParam(value="uid")String uid){
sessionPool.remove(uid);
}
/**
* 当连接发生错误时,这个方法就运行
* @param session 会话对象
* @param throwable 错误
*/
@OnError
public void onError(Session session,Throwable throwable){
throwable.printStackTrace();
}
/**
* 根据session发送消息
* @param session 与用户的会话
* @param message 发送给用户的内容
*/
private boolean sendMessage(Session session,String message){
if(session!=null){
synchronized (session){
try{
//根据一个session,发送消息内容给对方。
session.getBasicRemote().sendText(message);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
}
return false;
}
/**
* 指定id给用户发消息
* 业务逻辑的实现
* @param uid 用户id
* @param message 发送给用户的消息内容
* @return 发送是否成功
*/
public boolean sendInfo(String uid,String message){
Session session=sessionPool.get(uid);
if(session==null){
return false;
}
return sendMessage(session,message);
}
}
②构建websockect需要配置如下:
package com.qianfeng.gp09.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 {
/**
* 配置一个serverendpoint
* 用于管理连接服务器的uri
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
③在controller中注入:
package com.qianfeng.gp09.controller;
import com.alibaba.fastjson.JSONObject;
import com.qianfeng.gp09.dto.MessageDTO;
import com.qianfeng.gp09.model.Message;
import com.qianfeng.gp09.model.User;
import com.qianfeng.gp09.result.R;
import com.qianfeng.gp09.server.WebSocketServer;
import com.qianfeng.gp09.service.PushService;
import com.qianfeng.gp09.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* 用户的接口
*
*/
@RestController
@RequestMapping("qf/user")
public class UserController {
@Resource
private WebSocketServer webSocketServer;
@Resource
private PushService pushService;
@PostMapping("pushWeb")
public R pushWeb(@RequestBody MessageDTO messageDTO){
String uid=messageDTO.getUid();
String message=messageDTO.getMessage();
Integer overTime=messageDTO.getOverTime();
boolean flag = webSocketServer.sendInfo(uid, message);
Message msg=new Message();
msg.setUid(uid);
msg.setContent(message);
msg.setSendTime(new Date());
msg.setOverTime(new Date(System.currentTimeMillis()+overTime*1000*60));
if(flag){
msg.setState(1);
pushService.saveMessage(msg);
return R.ok("发送成功");
}else{
msg.setState(0);
pushService.saveMessage(msg);
return R.ok("用户离线,推送任务已经设置。");
}
}
}
建立前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input type="text" id="uid" placeholder="请输入id"/>
<br>
<button onclick="onConnectionClick();">连接服务器</button>
<br>
<div id="messages"></div>
<script>
let socket;
function onConnectionClick(){
if(typeof(WebSocket) == "undefined") {
//此浏览器不支持WebSocket
alert("请使用最新的Google Chrome浏览器");
return ;
}
if(socket!=null){
socket.close();
socket=null;
}
//获取用户输入的id
let uid=document.getElementById("uid").value;
//连接服务器的url
let url="ws://localhost:8080/webSocket/"+uid;
//实例化webSocket
socket=new WebSocket(url);
socket.onopen=function(){
console.info("已经与服务器建立连接");
};
socket.onmessage=function(msg){
let receiveMessage=msg.data;
document.getElementById("messages").innerHTML+="<span>"+receiveMessage+"</span><br>"
};
socket.onclose=function(){
console.info("已经与服务器断开连接");
};
socket.onerror=function(){
console.info("与服务器连接发生错误");
};
}
</script>
</body>
</html>
PS:在验证上述的逻辑中,遇到dao层注入时项目报空指针的问题,这是由于Spring在注入的时候,根据情况和注解,它注入的顺序不同,所以当时直接写了个工具类,可以直接使用注入,不需要关注顺序。工具类如下:
package com.qianfeng.gp09.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* spring上下文的工具类
*
*
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {
/**
* 它是spring的上下文对象,可以使用它获取spring容器中的任意对象
*/
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext=applicationContext;
}
public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}
上面还有种延伸出来的情况,详情如图,不做赘述:
手机端APP推送
首先先描述下上述场景:
当我们的一款手机App想要给手机进行消息的推送,我们使用到的是第三方的极光服务器或者是友盟服务器,解释下原因,如上图,如果不使用三方集成的服务器,针对不同的手机的服务器,我们的App都要分别去对接,并且需要专门的客服去保证这条线路的通畅,所以推介使用的是三方集成平台,由他们帮我们对接所有的手机厂商。