友達がいるよ〜
小さな壊れた Web サイトを構築したので、Web メッセージを Web サイト内にプッシュする機能を実装したいと考えています。はい、それは下の図の小さな赤い点で、非常によく使用される機能です。
しかし、彼はまだその方法を見つけていませんでした。ここで私は、彼がいくつかの解決策を整理し、それらを簡単に実装するのを手伝いました。
プッシュメッセージとは何ですか?
プッシュ シナリオは多数あり、たとえば、誰かが私の公式アカウントをフォローすると、クリックしてアプリを開くように誘導するプッシュ メッセージが届きます。
メッセージ プッシュ ( push
) は通常、Web サイトのオペレーターやその他の担当者が、何らかのツールを介してユーザーの現在の Web ページまたはモバイル デバイス APP にアクティブなメッセージをプッシュすることを指します。
メッセージプッシュは一般にweb端消息推送
とに分けられます移动端消息推送
。
上記のメッセージプッシュはモバイル端末に属するものですが、Web端末での一般的なメッセージプッシュには、サイトメッセージ、未読メール数、監視アラーム数などがあり、これも広く利用されています。
具体的な実装の前に、以前の要件を分析しましょう。実際、この機能は非常に単純です。特定のイベントがトリガーされる限り (リソースをアクティブに共有するか、バックグラウンドでメッセージをアクティブにプッシュする)、Web ページ上の通知の赤い点が表示されます。リアルタイムであること+1
。
通常、サーバー上には、さまざまなイベントをトリガーするユーザーによってプッシュされたさまざまな種類のメッセージを記録するために、サーバー上にいくつかのメッセージ プッシュ テーブルがあり、フロント エンドは、ユーザーからのすべての未読メッセージの数をアクティブにクエリ (プル) するか、受動的に受信 (プッシュ) します。
push
メッセージのプッシュには、プッシュ ( ) とプル ( )の 2 つの形式がありますpull
。以下でそれぞれについて説明します。
ショートポーリング
Polling( ) は、メッセージ プッシュを実装するための最も単純なメソッドである必要があります。ここでは、polling() をsumpolling
に分割します。短轮询
长轮询
ショートポーリングは分かりやすく、指定した時間間隔でブラウザがHTTP
サーバーにリクエストを送信し、サーバーは未読メッセージのデータをリアルタイムでクライアントに返し、ブラウザはそれをレンダリングして表示します。
単純な JS タイマーを使用して、1 秒ごとに未読メッセージ数インターフェイスをリクエストし、返されたデータを表示できます。
setInterval(() => {
// 方法请求
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data
}
})
}, 1000);
効果はまだ良好です。ショート ポーリングは実装が簡単ですが、欠点も明らかです。プッシュ データは頻繁に変更されないため、この時点でバックエンドで新しいメッセージが生成されているかどうかに関係なく、クライアントはリクエストを作成します。必然的にサーバーに多大な問題が発生し、多大なプレッシャーがかかり、帯域幅とサーバー リソースが無駄になります。
ロングポーリング
ロング ポーリングは、上記のショート ポーリングの改良版であり、サーバー リソースの無駄を最小限に抑えながら、メッセージの相対的なリアルタイム性を保証します。Nacos
ロング ポーリングは、構成apollo
センター、メッセージ キューkafka
などのミドルウェアで広く使用されています。RocketMQ
ロング ポーリングはミドルウェアで使用されます。
今回はコンフィグレーションセンターを利用してロングポーリングを実装し、Spring のカプセル化が提供する非同期リクエスト機構であるapollo
class を適用して結果を遅らせることが目的です。DeferredResult
servelet3.0
DeferredResult
これにより、コンテナー スレッドはリクエスト スレッドをブロックすることなく、占有されているリソースを迅速に解放できるため、より多くのリクエストを受け入れてシステム スループットが向上し、非同期ワーカー スレッドを開始して実際のビジネス ロジックを処理し、応答結果を送信する呼び出しを完了します。DeferredResult.setResult(200)
。
次に、ロングポーリングを使用してメッセージプッシュを実装します。
1 つの ID が複数のロング ポーリング リクエストによって監視される可能性があるため、guava
パッケージが提供する構造体を使用してMultimap
ロング ポーリングを保存し、1 つのキーに複数の値を対応させることができます。キーの変更が検出されると、対応するすべてのロング ポーリングが応答します。フロントエンドは非リクエスト タイムアウトのステータス コードを取得し、データの変更を認識し、未読メッセージ カウント インターフェイスに積極的にクエリを実行し、ページ データを更新します。
@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>
必要なラベルは1 つだけです。
<iframe src="/iframe/message" style="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の送信データは2次解析が必要で開発の敷居が高い。
-
SSE はデフォルトで切断と再接続をサポートしていますが、WebSocket は自分で実装する必要があります。
-
SSE はテキスト メッセージのみを送信でき、バイナリ データは送信前にエンコードする必要があります。WebSocket はデフォルトでバイナリ データの送信をサポートします。
SSE と WebSocket のどちらを選択すればよいですか?
「
技術に良い悪いはなく、どちらがより適しているかだけです
SSE は、双方向の全二重通信を実行するためのより豊富なプロトコルを提供する WebSocket の登場のせいもあって、まだあまり知られていないようです。ゲーム、インスタント メッセージング、および双方向でのほぼリアルタイムの更新が必要なシナリオでは、双方向チャネルがある方が魅力的です。
ただし、場合によっては、クライアントからのデータの送信が必要ない場合もあります。必要なのは、サーバー操作のためのいくつかの更新だけです。たとえば、サイト メッセージ、未読メッセージの数、ステータス更新、株価、数量の監視などのシナリオは、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
*/
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
*/
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 プロトコルの具体的な導入と実践についてはここでは詳しく説明しませんが、非常に詳細な私の以前の 2 つの記事を参照してください。
MQTT プロトコルの概要
springboot + Rabbitmq を使用してスマート ホームを作成することがこれほど簡単になるとは予想していませんでした。
MQTT はメッセージ プッシュを実装します
未読メッセージ (小さな赤い点)、フロントエンドと RabbitMQ のリアルタイム メッセージ プッシュの練習、非常に簡単です~
ウェブソケット
websocket
メッセージ プッシュを実装するための誰もがよく知っている方法であるはずであり、上記の SSE について説明したときにも WebSocket と比較しました。
WebSocket は、TCP
接続を介した全二重通信のためのプロトコルであり、クライアントとサーバーの間に通信チャネルを確立します。ブラウザとサーバーはハンドシェイクを 1 回だけ必要とし、双方向データ送信のために両者の間に永続的な接続を直接作成できます。
インターネットからの写真
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 や Jiguang Push と同様、これらは非常に優れたサードパーティ サービス プロバイダーです。
一般的に大企業は自社開発のメッセージプッシュプラットフォームを持っていますが、今回実装したWebメッセージはそのプラットフォーム上のタッチポイントに過ぎず、SMS、電子メール、WeChat公式アカウント、ミニプログラムなどはすべてユーザーにリーチできるチャネルです。 。
写真はインターネットから取得したものです
メッセージ プッシュ システムの内部は、メッセージ コンテンツのメンテナンスとレビュー、プッシュ グループの説明、リーチ フィルタリングとインターセプト (プッシュ ルールの頻度、期間、数量、ブラック リストとホワイト リスト、キーワードなど) など、非常に複雑です。 、プッシュの失敗を補償するためのモジュールが多数あり、大量のデータ量と高い同時実行性のシナリオを技術的に伴うものが数多くあります。したがって、今日の実装は、この巨大なシステムに対する小さな一歩にすぎません。
Githubアドレス
記事内で言及されているすべてのケースを 1 つずつ実装し、順番に並べましたGithub
。役に立ったと思われる場合は、 スター を付けてください。
ポータル: https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-realtime-data