js-EventSource、ajax long connection、サーバー送信イベントチャットサーバー

js-EventSource、ajax long connection、サーバー送信イベントチャットサーバーディレクトリ


序文

  • サーバーサイドプッシュイベントのテクノロジーが登場
  • メッセージを聞く、一方向、Server Sent Event
  • サーバープッシュプロトコル。サーバーによってのみプッシュおよび表示できるもの
  • 一般的なアプリケーション:チャットアプリケーション、株価情報、ニュースフィード
  • メッセージヘッダーは次のように設定する必要があります。Content-Type: text/event-stream
  • ポーリング:クライアントは定期的にサーバーにリクエストを送信します
  • ロングポーリング:クライアントはサーバーにリクエストを送信し、サーバーは返される必要のあるコンテンツが見つかるまでリクエストをブロックしてからクライアントに返します。クライアントはリクエストを受信した後、再度リクエストを送信します。(HTTP要求は、返されるまで維持されます。)
  • サーバー送信イベント:HTTP、ストリーミング、またはロングポーリングに基づく軽量プロトコル
  • Websocket:TCP、全二重通信に基づくH5プロトコルの真の持続的接続、ヘッダーヘッダーを繰り返す必要はなく、ハンドシェイクを確立するときにhttpプロトコルを使用し、将来のデータは必要ありません。
    • WebSocketは、ネットワークプロトコルやAPIなどのTCP接続を使用した双方向通信のメカニズムを提供し、双方向通信にHTTPポーリングを使用するWebページやサーバーを置き換えます。
    • 本質的に、WebSocketはHTTPプロトコルに限定されませんが、既存のHTTPインフラストラクチャ、プロキシ、フィルタリング、ID認証などが大量にあるため、WebSocketはHTTPポートとHTTPSポートを借用します。
    • HTTPポートが使用されるため、TCP接続が確立された後のハンドシェイクメッセージはHTTPに基づいており、サーバーはそれがHTTPプロトコルであるかWebSocketプロトコルであるかを判別します。WebSocket接続の確立と終了の間のハンドシェイクに加えて、データ送信はHTTPとは何の関係もありません。

推奨読書

  • 「JS決定版ガイド」

コード表示

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input id="input" style="width: 100%;">
    <script src="main.js"></script>
</body>
</html>

main.js

window.onload = function() {
    
    
    let nick = prompt("Enter your name:"),
        input = document.getElementById('input');
        input.focus();

    // 注册新消息的通知
    let chat = new EventSource("http://127.0.0.1:8080/");
    // 捕获到一条消息
    chat.onmessage = event => {
    
    
        let msg = event.data,
            node = document.createElement(msg),
            div = document.createElement("div");
        div.appendChild(node);
        document.body.insertBefore(div, input);
        input.scrollIntoView();
    };

    // 发给服务器
    input.onchange = () => {
    
    
        let msg = nick + ":" + input.nodeValue,
            xhr = new XMLHttpRequest();
        xhr.open('POST', "http://127.0.0.1:8080/");
        xhr.setRequestHeader("COntent-type",
                        "text/plain;charset=UTF-8");
        
        xhr.send(msg);
        input.value = "";
    };
}

event.php

//php方式
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$time = date('r');
echo "data: The server time is: {
      
      $time}\n\n";
flush();
?>

間違ったデモンストレーション

ここに画像の説明を挿入

XMLHttpRequestシミュレーションEventSource

/* 在不支持 EventSource的浏览器里进行模拟
需要有一个XMLHttpRequest对象在新数据写到长期存在的HTTP连接中时,
发送readystatechange事件
此API实现的不完整
不支持readyState属性,close()方法,open和error事件
消息事件也是通过onmessage属性注册的,但此版本没有定义add EventListener()
 */
if(window.EventSource === undefined){
    
    
    window.EventSource = function(url) {
    
    
        let xhr,
            evtsrc = this,
            charsReceived = 0,
            type = null,
            data = "",
            eventName = "message",
            lastEventId = "",
            retrydelay = 1000,
            aborted = false;

        xhr = new XMLHttpRequest();

        xhr.onreadystatechange = function() {
    
    
            switch(xhr.readyState){
    
    
                // 数据块到达时
                case 3: processData();  break;
                // 请求关闭时
                case 4: reconnect();    break;
            }
        };

        // 通过connect创建一个长期存在的长连接
        connect();

        // 如果链接正常关闭,等待1秒后,再尝试链接
        function reconnect() {
    
    
            // 在终止链接后不进行重连操作
            if(aborted) return;
            if(xhr.status >= 300)   return;
            // 等待retrydelay秒后进行重连
            setTimeout(connect, retrydelay);
        }

        // 建立一个链接
        function connect() {
    
    
            charsReceived = 0;
            type = null;

            xhr.open("GET", url);
            xhr.setRequestHeader("Cache-control", "no-cache");

            if(lastEventId) xhr.setRequestHeader("Last-Event-ID", lastEventId);
            xhr.send();
        }

        // 每当数据到达的时候,会处理并触发onmessage处理程序
        // 这个函数处理Server-Send Events协议的细节
        function processData() {
    
    
            // 如果没有准备好,先检查响应类型
            if(!type) {
    
    
                type = xhr.getResponseHeader('Content-Type');

                if(type !== "text/event-stream"){
    
    
                    aborted = true;
                    xhr.abort();
                    return;
                }
            }

            // 记录接收的数据
            // 获取响应中未处理的数据
            let chunk = xhr.responseText.substring(charsReceived);
            charsReceived = xhr.responseText.length;

            // 将大块的文本数据分成多行并遍历他们
            let lines = chunk.replace(/(\r\n|\r|\n)$/, "").split(/\r\n|\r|\n/);
            for(let i = 0; i < lines.length; i++) {
    
    
                let line = lines[i],
                    pos = line.indexOf(":"),
                    name,
                    value = "";
                
                // 忽略注释
                if(pos == 0)    continue;
                // 字段名称:值
                if(pos > 0) {
    
    
                    name = line.substring(0, pos);
                    value = line.substring(pos+1);
                    if(value.charAt(0) == " ")  value = value.substring(1);
                }else {
    
    
                    // 只有字段名称
                    name = line;
                }

                switch(name){
    
    
                    case "event":   eventName = value; break;
                    case "data":    data += value + "\n"; break;
                    case "id":  lastEventId = value; break;
                    case "retry": retrydelay = parseInt(value) || 1000; break;
                    default: break;
                }

                // 一个空行意味着发送事件
                if(line === ""){
    
    
                    if(evtsrc.onmessage && data !== ""){
    
    
                        // 如果末尾有新行,就裁剪新行
                        if(data.charAt(data.length-1) == '\n'){
    
    
                            data = data.substring(0, data.length - 1);
                        }

                        // 伪造的事件对象
                        evtsrc.onmessage({
    
    
                            type: eventName,    // 事件类型
                            data: data,         // 事件数据
                            origin: url         // 数据源
                        });
                    }

                    data = "";
                    continue;
                }
            }
        }
    }
}

Server-Sent Eventチャットサーバー

/* 
* 这个例子用的是服务器的Node
* 该聊天室的实现比较简单,而且是完全匿名的
* 将新的消息以 POST 发送到/chat地址, 或者以GET形式从同一个URL获取消息的文本/事件流
* 创建一个GET请求到"/"来返回一个简单的HTML文件
* 这个文件包括客户端聊天UI
*/
const http = require('http');
const {
    
     request } = require('node:http');

// 聊天客户端使用的HTML文件
const clientui  = require('fs').readFileSync("chatclient.html");
const emulation = require('fs').readFileSync("EventSourceEmulation.js");

// ServerResponse对象数组,用于接收发送的事件
let clients = [];

// 每20秒发送一条注释到客户端
// 这样他们就不会关闭链接再重连
setInterval(() => {
    
    
    clients.forEach(client => client.write(":ping?n"));
},20000);

// 创建一个新服务器
let server = new http.Server();

// 当服务器获取到一个新的请求,运行那个回调函数
server.on("request", (request, response) => {
    
    
    // 解析请求的URL
    let url = require("url").parse(request.url);

    // 如果请求是发送到"/", 服务器就发送客户端聊天室UI
    if(url.pathname === '/') {
    
    
        response.writeHead(200, {
    
    "Content-Type":"text/html"});
        response.write(`<script>${
      
      emulation}</script>`);
        response.write(clientui);
        response.end();
        return;
    }else if(url.pathname !== "/chat") {
    
    
        // 如果请求是发送到"/chat"之外的地方,则返回404
        response.writeHead(404);
        response.end();
        return;
    }

    // 如果请求类型是post,那么就有一个客户端发送了一条新的消息
    if(request.method === "POST"){
    
    
        request.setEncoding("utf8");
        let body = "";
        // 再获取到数据之后,将其添加到请求主体中
        request.on("data", chunk => {
    
    
            body += chunk;
        });

        // 当请求完成时,发送一个空响应
        // 并将消息传播到所有处于监听状态的客户端中
        request.on("end", function() {
    
    
            // 响应该请求
            response.writeHead(200);
            response.end();

            // 将消息转换成文本/事件流格式
            // 确保每一行的前缀都是"data:"
            // 并以两个换行符结束
            message = 'data:' + body.replace('\n', '\ndata: ') + "\r\n\r\n";

            // 发送信息给所有监听的客户端
            clients.forEach(client => client.write(message));
        });
    }

    // 此外,客户端正在请求信息流
    else {
    
    
        // 如果不是POST类型的请求,则客户端正在请求一组消息
        response.writeHead(200, {
    
    'Content-Type':"text/event-stream"});
        response.write("data: Connected\n\n");

        // 如果客户端关闭了连接
        // 从活动客户端数组中删除对应的响应对象
        request.connection.on("end", function() {
    
    
            clients.splice(clients.indexOf(response), 1);
            response.end();
        });

        // 记下响应对象, 这样就可以向它发送未来的消息
        clients.push(response);
    }

});

// start server
server.listen(8000);

おすすめ

転載: blog.csdn.net/u013362192/article/details/115008255