Technical exchange, public account: Programmer Xiaofu
Hello everyone, I'm Xiaofu~
I have a friend~
I made a small broken station, and now I want to implement a function of pushing web messages within the station. Yes, it is the little red dot in the picture below, a very commonly used function.
However, he hasn't figured out how to do it yet. Here I help him sort out several solutions and simply implement them.
Case download , remember Star
What is push message (push)
There are many push scenarios. For example, when someone follows my official account, I will receive a push message to attract me to click to open the app.
Message push ( push
) usually refers to the active push of messages to the user's current web page or mobile device APP through a certain tool by the operation staff of the website.
Message push is generally divided into web端消息推送
and 移动端消息推送
.
The above one belongs to the mobile-side message push, and the web-side message push is common, such as in-site messages, the number of unread emails, the number of monitoring alarms, etc., and it is also widely used.
Before the specific implementation, let's analyze the previous requirements. In fact, the function is very simple. As long as an event is triggered (actively sharing resources or actively pushing messages in the background), the notification red dot on the web page will be real-time +1
's it.
Usually, there are several message push tables on the server side to record different types of messages pushed by users triggering different events. The front end actively queries (pulls) or passively receives (pushes) the number of all unread messages of users.
Message push is nothing more than push ( push
) and pull ( pull
) two forms, let's take a look at them one by one.
short polling
Polling ( polling
) should be the easiest way to implement a message push scheme. Here we temporarily divide polling into 短轮询
and 长轮询
.
Short polling is easy to understand. At the specified time interval, the browser sends a HTTP
request to the server, the server returns the unread message data to the client in real time, and the browser renders and displays it.
A simple JS timer can do it, request the unread message count interface every second, and display the returned data.
setInterval(() => {
// 方法请求
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data
}
})
}, 1000);
The effect is still good. Although the implementation of short polling is simple, the shortcomings are also obvious. Since the push data does not change frequently, no matter whether there is a new message in the backend at this time, the client will make a request, which will inevitably cause a lot to the server. A lot of stress, wasting bandwidth and server resources.
long polling
Long polling is an improved version of the above short polling, which can reduce the waste of server resources as much as possible while ensuring the relative real-time performance of messages.长轮询在中间件中应用的很广泛,比如---af2b9c9974dcb60f03187c4ce150301c Nacos
和---b47fbcae5a13d73e88b40d6c964d380c apollo
中心,消息队列kafka
、 RocketMQ
中都Useful for long polling.
Is the interaction model of the Nacos configuration center push or pull? In this article, I have introduced in detail Nacos
the implementation principle of long polling, and interested friends can take a look.
This time I used the apollo
configuration center to implement long polling, and applied a class DeferredResult
, which is provided by Spring after servelet3.0
Asynchronous request mechanism, the direct meaning is to delay the result.
DeferredResult
It can allow container threads to quickly release occupied resources without blocking the request thread, so as to accept more requests to improve the throughput of the system, and then start the asynchronous worker thread to process the real business logic and process the completed DeferredResult.setResult(200)
Submit the response result.
Below we use long polling to implement message push.
Because an ID may be monitored by multiple long polling requests, I use the guava
package provided by the Multimap
structure to store long polling, and a key can correspond to multiple values. Once the key changes are monitored, all corresponding long polls will respond. The front end gets the status code of the non-request timeout, knows the data change, actively queries the interface for the number of unread messages, and updates the page data.
@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";
}
When the request exceeds the set timeout time, it will throw AsyncRequestTimeoutException
exception, here directly use @ControllerAdvice
the global capture and return can be unified, the front end obtains the agreed status code and initiates long polling again request, and so on and so forth.
@ControllerAdvice
public class AsyncRequestTimeoutHandler {
@ResponseStatus(HttpStatus.NOT_MODIFIED)
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)
public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
System.out.println("异步请求超时");
return "304";
}
}
Let's test it. First, the page initiates a long polling request /polling/watch/10086
to monitor the message change, the request is suspended, the data is not changed until the timeout, and the long polling request is initiated again; then the data is manually /polling/publish/10086
, the long polling is responded, and the front-end processing business logic is completed and the request is initiated again, and so on.
Compared with short polling, long polling has improved performance a lot, but it still generates more requests, which is a little imperfect.
iframe stream
The iframe flow is to insert a hidden <iframe>
tag into the page, and by requesting the number of messages API interface in src
, a long connection is created between the server and the client, The server continues to transmit data to iframe
.
The transmitted data is usuallyHTML
, or the embeddedjavascript
script to achieve the effect of updating the page in real time.
This method is simple to implement, and the front end only needs one <iframe>
label to get it
<iframe src="/iframe/message" style="display:none"></iframe>
The server directly assembles the html and js script data to response
and writes it
@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>");
}
}
}
But I personally don't recommend it, because it will show on the browser that the request has not been loaded, and the icon will keep spinning, which is an obsessive-compulsive disorder killer.
SSE (my way)
Many people may not know that the server pushes messages to the client. In fact, in addition to the familiar mechanism of WebSocket
, there is also a server-sent event ( Server-sent events
), referred to SSE
.
SSE
It is based on the HTTP
protocol. We know that the HTTP protocol in the general sense cannot make the server actively push messages to the client, but SSE is an exception, which transforms an idea.
SSE opens a one-way channel between the server and the client. The server responds with a one-time data packet instead of text/event-stream
type data flow information, which is streamed from the server when there is data change transmitted to the client.
The overall implementation idea is a bit similar to online video playback. The video stream will be continuously pushed to the browser. You can also understand that the client completes a download that takes a long time (the network is not smooth).
SSE
and WebSocket
are similar in function, both can establish communication between the server and the browser, so that the server can push messages to the client, but there are some differences:
- SSE is based on the HTTP protocol, and they do not require a special protocol or server implementation to work;
WebSocket
need a separate server to handle the protocol. - SSE one-way communication, only one-way communication from the server to the client; webSocket full-duplex communication, that is, both parties of the communication can send and receive information at the same time.
- SSE is simple to implement and has low development cost, without the need to introduce other components; WebSocket data transmission requires secondary analysis, and the development threshold is higher.
- SSE supports disconnection and reconnection by default; WebSocket needs to be implemented by itself.
- SSE can only transmit text messages, and binary data needs to be encoded and transmitted; WebSocket supports the transmission of binary data by default.
How to choose between SSE and WebSocket?
Technology is not good or bad, only which is more suitable
SSE seems to have been unknown to everyone, in part because of the advent of WebSockets, which provide a richer protocol to perform two-way, full-duplex communication. For gaming, instant messaging, and scenarios that require near real-time updates in both directions, having a two-way channel is more attractive.
However, in some cases, it is not necessary to send data from the client. And you just need some updates for server operations. For example, in-site messages, unread messages, status updates, stock quotes, monitoring numbers, etc., SEE
is more advantageous in terms of ease of implementation and cost. In addition, SSE has several features that ---f2d27080f5c8aa80a8aedc9ded5f6c36 WebSockets
lacks by design, such as: 自动重新连接
, 事件ID
and 发送任意事件
.
The front end only needs to make an HTTP request, bring a unique ID, open the event stream, and listen to the events pushed by the server.
<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>
The implementation of the server is simpler, create a SseEmitter
object and put it in sseEmitterMap
for management
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);
}
}
}
We simulate the server to push the message, and see that the client receives the message, which is consistent with our expected effect.
Note: SSE does not support the IE
browser, and it does a good job of compatibility with other mainstream browsers.
MQTT
What is the MQTT protocol?
MQTT
(Message Queue Telemetry Transport):一种基于发布/订阅( publish
/ subscribe
)模式的轻量级
通讯协议, Getting messages by subscribing to the corresponding topic is a standard transport protocol in the Internet of Things ( Internet of Thing
).
This protocol separates the message publisher ( publisher
) from the subscriber ( subscriber
), so it can provide reliable messages to remotely connected devices in an unreliable network environment Services, which are used in a similar way to traditional MQ.
TCP
protocol is located at the transport layer, MQTT
protocol is located at the application layer, MQTT
TCP/IP
is built on the TCP/IP
protocol as long as it supports TCP/IP
protocol stack can use the MQTT
protocol.
Why use the MQTT protocol?
MQTT
Why are protocols so favored in the Internet of Things (IOT)? Instead of other protocols, such as the more familiar HTTP
protocol?
- First of all
HTTP
protocol It is a synchronous protocol, and the client needs to wait for the server's response after requesting. In the Internet of Things (IOT) environment, the device will be very affected by the environment, such as low bandwidth, high network latency, unstable network communication, etc. Obviously, the asynchronous message protocol is more suitable forIOT
applications. -
HTTP
is one-way, if you want to get a message the client must initiate a connection, and in Internet of Things (IOT) applications, devices or sensors are often clients, which means they cannot passively receive commands from the network. - Usually a command or message needs to be sent to all devices on the network.
HTTP
To achieve such a function is not only difficult, but also extremely expensive.
The specific MQTT protocol introduction and practice, I will not repeat them here, you can refer to my two previous articles, which are also very detailed.
Introduction to the MQTT protocol
I didn't expect springboot + rabbitmq to do smart home, it would be so simple
MQTT implements message push
Websocket
websocket
should be a method that everyone is familiar with to implement message push. We also compared it with websocket when we talked about SSE.
WebSocket is a protocol for full-duplex communication over TCP
connections, establishing a communication channel between client and server. The browser and the server only need one handshake, and a persistent connection can be created directly between the two, and two-way data transmission can be performed.
Springboot integrates websocket, first introduces websocket
related toolkit, which costs extra development compared to SSE.
<!-- 引入websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
The server uses the @ServerEndpoint
annotation to mark the current class as a websocket server, and the client can connect to the WebSocket server through ws://localhost:7777/webSocket/10086
.
@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();
}
}
}
}
The front-end initializes to open the WebSocket connection, monitors the connection status, receives server data or sends data to the server.
<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>
The page is initialized to establish a websocket connection, and then two-way communication can be performed, and the effect is not bad.
custom push
Above, we gave me the principles and code implementations of 6 solutions. However, in the actual business development process, we cannot blindly use them directly. We should choose the appropriate solution based on the characteristics of our own system business and actual scenarios.
The most direct way to push is to use the third push platform. After all , the demand that money can solve is not a problem . It can be used directly without complicated development, operation and maintenance. It saves time, effort and worry. Like goEasy and Jiguang push are very good. third-party service provider.
Generally, large companies have self-developed message push platforms. For example, the web site message we implemented this time is just a contact point on the platform. SMS, email, WeChat public account, and small programs can be accessed through any channel that can reach users. come in.
The inside of the message push system is quite complicated, such as the maintenance and review of message content, delineating the push crowd, reaching filtering and intercepting (the frequency of push rules, time period, quantity, black and white lists, keywords, etc.), and modules with many push failure compensations , There are also many scenarios that technically involve large amounts of data and high concurrency. So the way we implement it today is just a small game in front of this huge system.
Github address
I have implemented all the cases mentioned in the article, and put them in Github
. If you think it is useful, just star it!
Portal: https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-realtime-data
Whether you are just entering the industry or a programmer with several years of experience, I believe this interview outline will give you a lot of help, pay attention to the public account " Programmer Xiaofu ", reply " offer " to get it by yourself, I wish you all the offer Soft hands!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。