技术交流,大众号:程序员小富
我们好,我是小富~
我有一个朋友~
做了一个小破站,现在要完结一个站内信web音讯推送的功用,对,便是下图这个小红点,一个很常用的功用。
不过他还没想好用什么办法做,这里我帮他收拾了一下几种计划,并简略做了完结。
案例下载,记住Star 哦
什么是音讯推送(push)
推送的场景比较多,比方有人关注我的大众号,这时我就会收到一条推送音讯,以此来招引我点击翻开运用。
音讯推送(push
)通常是指网站的运营作业等人员,经过某种东西对用户当时网页或移动设备APP进行的主动音讯推送。
音讯推送一般又分为web端音讯推送
和移动端音讯推送
。
上边的这种归于移动端音讯推送,web端音讯推送常见的比如站内信、未读邮件数量、监控报警数量等,运用的也十分广泛。
在详细完结之前,咱们再来分析一下前边的需求,其实功用很简略,只需触发某个事情(主动分享了资源或许后台主动推送音讯),web页面的告诉小红点就会实时的+1
就能够了。
通常在服务端会有若干张音讯推送表,用来记录用户触发不同事情所推送不同类型的音讯,前端主动查询(拉)或许被动接纳(推)用户一切未读的音讯数。
音讯推送无非是推(push
)和拉(pull
)两种办法,下边咱们逐个了解下。
短轮询
轮询(polling
)应该是完结音讯推送计划中最简略的一种,这里咱们暂时将轮询分为短轮询
和长轮询
。
短轮询很好了解,指定的时刻间隔,由浏览器向服务器宣布HTTP
恳求,服务器实时返回未读音讯数据给客户端,浏览器再做渲染显示。
一个简略的JS定时器就能够搞定,每秒钟恳求一次未读音讯数接口,返回的数据展现即可。
setInterval(() => {
// 办法恳求
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data
}
})
}, 1000);
效果仍是能够的,短轮询完结固然简略,缺点也是清楚明了,因为推送数据并不会频频改变,无论后端此刻是否有新的音讯发生,客户端都会进行恳求,必然会对服务端形成很大压力,浪费带宽和服务器资源。
长轮询
长轮询是对上边短轮询的一种改进版别,在尽或许减少对服务器资源浪费的一起,确保音讯的相对实时性。长轮询在中间件中运用的很广泛,比方Nacos
和apollo
配置中心,音讯行列kafka
、RocketMQ
中都有用到长轮询。
Nacos配置中心交互模型是push仍是pull?一文中我详细介绍过Nacos
长轮询的完结原理,感兴趣的小伙伴能够瞅瞅。
这次我运用apollo
配置中心完结长轮询的办法,运用了一个类DeferredResult
,它是在servelet3.0
后经过Spring封装供给的一种异步恳求机制,直意便是延迟结果。
DeferredResult
能够答应容器线程快速开释占用的资源,不阻塞恳求线程,以此承受更多的恳求提升体系的吞吐量,然后发动异步作业线程处理真实的事务逻辑,处理完结调用DeferredResult.setResult(200)
提交呼应结果。
下边咱们用长轮询来完结音讯推送。
因为一个ID或许会被多个长轮询恳求监听,所以我采用了guava
包供给的Multimap
结构寄存长轮询,一个key能够对应多个value。一旦监听到key发生变化,对应的一切长轮询都会呼应。前端得到非恳求超时的状况码,知晓数据改变,主动查询未读音讯数接口,更新页面数据。
@Controller
@RequestMapping("/polling")
public class PollingController {
// 寄存监听某个Id的长轮询集合
// 线程同步结构
public static Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());
/**
* 大众号:程序员小富
* 设置监听
*/
@GetMapping(path = "watch/{id}")
@ResponseBody
public DeferredResult<String> watch(@PathVariable String id) {
// 延迟对象设置超时时刻
DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT);
// 异步恳求完结时移除 key,防止内存溢出
deferredResult.onCompletion(() -> {
watchRequests.remove(id, deferredResult);
});
// 注册长轮询恳求
watchRequests.put(id, deferredResult);
return deferredResult;
}
/**
* 大众号:程序员小富
* 改变数据
*/
@GetMapping(path = "publish/{id}")
@ResponseBody
public String publish(@PathVariable String id) {
// 数据改变 取出监听ID的一切长轮询恳求,并逐个呼应处理
if (watchRequests.containsKey(id)) {
Collection<DeferredResult<String>> deferredResults = watchRequests.get(id);
for (DeferredResult<String> deferredResult : deferredResults) {
deferredResult.setResult("我更新了" + new Date());
}
}
return "success";
}
当恳求超过设置的超时时刻,会抛出AsyncRequestTimeoutException
反常,这里直接用@ControllerAdvice
全局捕获统一返回即可,前端获取约定好的状况码后再次建议长轮询恳求,如此往复调用。
@ControllerAdvice
public class AsyncRequestTimeoutHandler {
@ResponseStatus(HttpStatus.NOT_MODIFIED)
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)
public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
System.out.println("异步恳求超时");
return "304";
}
}
咱们来测验一下,首要页面建议长轮询恳求/polling/watch/10086
监听音讯更变,恳求被挂起,不改变数据直至超时,再次建议了长轮询恳求;紧接着手动改变数据/polling/publish/10086
,长轮询得到呼应,前端处理事务逻辑完结后再次建议恳求,如此循环往复。
长轮询比较于短轮询在性能上提升了许多,但依然会发生较多的恳求,这是它的一点不完美的当地。
iframe流
iframe流便是在页面中刺进一个躲藏的<iframe>
标签,经过在src
中恳求音讯数量API接口,由此在服务端和客户端之间创立一条长衔接,服务端继续向iframe
传输数据。
传输的数据通常是
HTML
、或是内嵌的javascript
脚本,来到达实时更新页面的效果。
这种办法完结简略,前端只需一个<iframe>
标签搞定了
<iframe src="/iframe/message"hljs-attribute">display:none"></iframe>
服务端直接拼装html、js脚本数据向response
写入就行了
@Controller
@RequestMapping("/iframe")
public class IframeController {
@GetMapping(path = "message")
public void message(HttpServletResponse response) throws IOException, InterruptedException {
while (true) {
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print(" <script type="text/javascript">n" +
"parent.document.getElementById('clock').innerHTML = "" + count.get() + "";" +
"parent.document.getElementById('count').innerHTML = "" + count.get() + "";" +
"</script>");
}
}
}
但我个人不引荐,因为它在浏览器上会显示恳求未加载完,图标会不断旋转,简直是强迫症杀手。
SSE (我的办法)
许多人或许不知道,服务端向客户端推送音讯,其实除了能够用WebSocket
这种耳熟能详的机制外,还有一种服务器发送事情(Server-sent events
),简称SSE
。
SSE
它是根据HTTP
协议的,咱们知道一般意义上的HTTP协议是无法做到服务端主动向客户端推送音讯的,但SSE是个例外,它变换了一种思路。
SSE在服务器和客户端之间翻开一个单向通道,服务端呼应的不再是一次性的数据包而是text/event-stream
类型的数据流信息,在有数据改变时从服务器流式传输到客户端。
整体的完结思路有点类似于在线视频播映,视频流会接二连三的推送到浏览器,你也能够了解成,客户端在完结一次用时很长(网络不畅)的下载。
SSE
与WebSocket
作用相似,都能够树立服务端与浏览器之间的通讯,完结服务端向客户端推送音讯,但仍是有少许不同:
- SSE 是根据HTTP协议的,它们不需求特殊的协议或服务器完结即可作业;
WebSocket
需独自服务器来处理协议。 - SSE 单向通讯,只能由服务端向客户端单向通讯;webSocket全双工通讯,即通讯的两边能够一起发送和承受信息。
- SSE 完结简略开发本钱低,无需引入其他组件;WebSocket传输数据需做二次解析,开发门槛高一些。
- SSE 默许支撑断线重连;WebSocket则需求自己完结。
- SSE 只能传送文本音讯,二进制数据需求经过编码后传送;WebSocket默许支撑传送二进制数据。
SSE 与 WebSocket 该怎么挑选?
技术并没有好坏之分,只有哪个更合适
SSE好像一向不被我们所熟知,一部分原因是呈现了WebSockets,这个供给了更丰厚的协议来履行双向、全双工通讯。关于游戏、即时通讯以及需求双向近乎实时更新的场景,拥有双向通道更具招引力。
可是,在某些情况下,不需求从客户端发送数据。而你只需求一些服务器操作的更新。比方:站内信、未读音讯数、状况更新、股票行情、监控数量等场景,SEE
不管是从完结的难易和本钱上都愈加有优势。此外,SSE 具有WebSockets
在规划上缺少的多种功用,例如:主动重新衔接
、事情ID
和发送恣意事情
的能力。
前端只需进行一次HTTP恳求,带上唯一ID,翻开事情流,监听服务端推送的事情就能够了
<script>
let source = null;
let userId = 7777
if (window.EventSource) {
// 树立衔接
source = new EventSource('http://localhost:7777/sse/sub/'+userId);
setMessageInnerHTML("衔接用户=" + userId);
/**
* 衔接一旦树立,就会触发open事情
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("树立衔接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("你的浏览器不支撑SSE");
}
</script>
服务端的完结更简略,创立一个SseEmitter
对象放入sseEmitterMap
进行管理
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
/**
* 创立衔接
*
* @date: 2022/7/12 14:51
* @auther: 大众号:程序员小富
*/
public static SseEmitter connect(String userId) {
try {
// 设置超时时刻,0表示不过期。默许30秒
SseEmitter sseEmitter = new SseEmitter(0L);
// 注册回调
sseEmitter.onCompletion(completionCallBack(userId));
sseEmitter.onError(errorCallBack(userId));
sseEmitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId, sseEmitter);
count.getAndIncrement();
return sseEmitter;
} catch (Exception e) {
log.info("创立新的sse衔接反常,当时用户:{}", userId);
}
return null;
}
/**
* 给指定用户发送音讯
*
* @date: 2022/7/12 14:51
* @auther: 大众号:程序员小富
*/
public static void sendMessage(String userId, String message) {
if (sseEmitterMap.containsKey(userId)) {
try {
sseEmitterMap.get(userId).send(message);
} catch (IOException e) {
log.error("用户[{}]推送反常:{}", userId, e.getMessage());
removeUser(userId);
}
}
}
咱们模拟服务端推送音讯,看下客户端收到了音讯,和咱们预期的效果一致。
注意: SSE不支撑IE
浏览器,对其他主流浏览器兼容性做的还不错。
MQTT
什么是 MQTT协议?
MQTT
全称(Message Queue Telemetry Transport):一种根据发布/订阅(publish
/subscribe
)模式的轻量级
通讯协议,经过订阅相应的主题来获取音讯,是物联网(Internet of Thing
)中的一个规范传输协议。
该协议将音讯的发布者(publisher
)与订阅者(subscriber
)进行别离,因此能够在不牢靠的网络环境中,为远程衔接的设备供给牢靠的音讯服务,运用办法与传统的MQ有点类似。
TCP
协议坐落传输层,MQTT
协议坐落运用层,MQTT
协议构建于TCP/IP
协议上,也便是说只需支撑TCP/IP
协议栈的当地,都能够运用MQTT
协议。
为什么要用 MQTT协议?
MQTT
协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比方咱们更为了解的 HTTP
协议呢?
-
首要
HTTP
协议它是一种同步协议,客户端恳求后需求等候服务器的呼应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比方带宽低、网络延迟高、网络通讯不稳定等,明显异步音讯协议更为合适IOT
运用程序。 -
HTTP
是单向的,假如要获取音讯客户端必须建议衔接,而在物联网(IOT)运用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接纳来自网络的指令。 -
通常需求将一条指令或许音讯,发送到网络上的一切设备上。
HTTP
要完结这样的功用不但很困难,而且本钱极高。
详细的MQTT协议介绍和实践,这里我就不再赘述了,我们能够参考我之前的两篇文章,里边写的也都很详细了。
MQTT协议的介绍
我也没想到 springboot + rabbitmq 做智能家居,会这么简略
MQTT完结音讯推送
未读音讯(小红点),前端 与 RabbitMQ 实时音讯推送实践,贼简略~
Websocket
websocket
应该是我们都比较了解的一种完结音讯推送的办法,上边咱们在讲SSE的时分也和websocket进行过比较。
WebSocket是一种在TCP
衔接上进行全双工通讯的协议,树立客户端和服务器之间的通讯途径。浏览器和服务器仅需一次握手,两者之间就直接能够创立持久性的衔接,并进行双向数据传输。
springboot整合websocket,先引入websocket
相关的东西包,和SSE比较额外的开发本钱。
<!-- 引入websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
服务端运用@ServerEndpoint
注解标注当时类为一个websocket服务器,客户端能够经过ws://localhost:7777/webSocket/10086
来衔接到WebSocket服务器端。
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {
//与某个客户端的衔接会话,需求经过它来给客户端发送数据
private Session session;
private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
// 用来存在线衔接数
private static final Map<String, Session> sessionPool = new HashMap<String, Session>();
/**
* 大众号:程序员小富
* 链接成功调用的办法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
try {
this.session = session;
webSockets.add(this);
sessionPool.put(userId, session);
log.info("websocket音讯: 有新的衔接,总数为:" + webSockets.size());
} catch (Exception e) {
}
}
/**
* 大众号:程序员小富
* 收到客户端音讯后调用的办法
*/
@OnMessage
public void onMessage(String message) {
log.info("websocket音讯: 收到客户端音讯:" + message);
}
/**
* 大众号:程序员小富
* 此为单点音讯
*/
public void sendOneMessage(String userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("websocket消: 单点音讯:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
前端初始化翻开WebSocket衔接,并监听衔接状况,接纳服务端数据或向服务端发送数据。
<script>
var ws = new WebSocket('ws://localhost:7777/webSocket/10086');
// 获取衔接状况
console.log('ws衔接状况:' + ws.readyState);
//监听是否衔接成功
ws.onopen = function () {
console.log('ws衔接状况:' + ws.readyState);
//衔接成功则发送一个数据
ws.send('test1');
}
// 接听服务器发回的信息并处理展现
ws.onmessage = function (data) {
console.log('接纳到来自服务器的音讯:');
console.log(data);
//完结通讯后封闭WebSocket衔接
ws.close();
}
// 监听衔接封闭事情
ws.onclose = function () {
// 监听整个过程中websocket的状况
console.log('ws衔接状况:' + ws.readyState);
}
// 监听并处理error事情
ws.onerror = function (error) {
console.log(error);
}
function sendMessage() {
var content = $("#message").val();
$.ajax({
url: '/socket/publish?userId=10086&message=' + content,
type: 'GET',
data: { "id": "7777", "content": content },
success: function (data) {
console.log(data)
}
})
}
</script>
页面初始化树立websocket衔接,之后就能够进行双向通讯了,效果还不错
自定义推送
上边咱们给我出了6种计划的原理和代码完结,但在实践事务开发过程中,不能盲目的直接拿过来用,仍是要结合自身体系事务的特色和实践场景来挑选合适的计划。
推送最直接的办法便是运用第三推送渠道,毕竟钱能解决的需求都不是问题,无需杂乱的开发运维,直接能够运用,省时、省力、省心,像goEasy、极光推送都是很不错的三方服务商。
一般大型公司都有自研的音讯推送渠道,像咱们本次完结的web站内信只是渠道上的一个触点而已,短信、邮件、微信大众号、小程序但凡能够触到达用户的途径都能够接入进来。
音讯推送体系内部是适当杂乱的,比如音讯内容的维护审阅、圈定推送人群、触达过滤阻拦(推送的规则频次、时段、数量、是非名单、关键词等等)、推送失败补偿十分多的模块,技术上涉及到大数据量、高并发的场景也许多。所以咱们今日的完结办法在这个巨大的体系面前只是小打小闹。
Github地址
文中所说到的案例我都逐个的做了完结,收拾放在了Github
上,觉得有用就 Star 一下吧!
传送门:github.com/chengxy-nds…