JavaScript基於ChatGPT實現打字機消息回復
1 背景
在使用 ChatGPT 時,發現輸入 prompt 後,頁面是逐步給出回復的,起初以為使用瞭 WebSckets 持久化連接協議,查看其網絡請求,發現這個接口的通信方式並非傳統的 http 接口或者 WebSockets,而是基於 EventStream 的事件流,像打字機一樣,一段一段的返回答案。
ChatGPT 是一個基於深度學習的大型語言模型,處理自然語言需要大量的計算資源和時間,響應速度肯定比普通的讀數據庫要慢的多,普通 http 接口等待時間過長,顯然並不合適。對於這種單項對話場景,ChagtGPT 將先計算出的數據“推送”給用戶,邊計算邊返回,避免用戶因為等待時間過長關閉頁面。而這,正式采用瞭 SSE 技術。
2 簡介
Server-Sent Events 服務器推送事件,簡稱 SSE,是一種服務端實時主動向瀏覽器推送消息的技術。 SSE 是 HTML5 中一個與通信相關的 API,主要由兩部分組成:服務端與瀏覽器端的通信協議(HTTP
協議)及瀏覽器端可供 JavaScript 使用的 EventSource
對象。
從“服務端主動向瀏覽器實時推送消息”這一點來看,該 API 與 WebSockets API 有一些相似之處。但是,該 API 與 WebSockers API 的不同之處在於:
Server-Sent Events API | WebSockets API |
---|---|
基於 HTTP 協議 | 基於 TCP 協議 |
單工,隻能服務端單向發送消息 | 全雙工,可以同時發送和接收消息 |
輕量級,使用簡單 | 相對復雜 |
內置斷線重連和消息追蹤的功能 | 不在協議范圍內,需手動實現 |
文本或使用 Base64 編碼和 gzip 壓縮的二進制消息 | 類型廣泛 |
支持自定義事件類型 | 不支持自定義事件類型 |
連接數 HTTP/1.1 6 個,HTTP/2 可協商(默認 100) | 連接數無限制 |
3 服務端實現
3.1 協議
SSE 協議非常簡單,本質是瀏覽器發起 http 請求,服務器在收到請求後,返回狀態與數據,並附帶以下 headers: js Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive
– SSE API規定推送事件流的 MIME 類型為 text/event-stream
。 – 必須指定瀏覽器不緩存服務端發送的數據,以確保瀏覽器可以實時顯示服務端發送的數據。 – SSE 是一個一直保持開啟的 TCP 連接,所以 Connection 為 keep-alive。
3.2 消息格式
EventStream(事件流)為 UTF-8
格式編碼的文本
或使用 Base64 編碼和 gzip 壓縮的二進制消息。 每條消息由一行或多行字段(event
、id
、retry
、data
)組成,每個字段組成形式為:字段名:字段值
。字段以行為單位,每行一個(即以 \n
結尾)。以冒號
開頭的行為註釋行,會被瀏覽器忽略。 每次推送,可由多個消息組成,每個消息之間以空行分隔(即最後一個字段以\n\n
結尾)。
📢 註意:
- 除上述四個字段外,其他所有字段都會被忽略。
- 如果一行字段中不包含冒號,則整行文本將被視為字段名,字段值為空。
- 註釋行可以用來防止鏈接超時,服務端可以定期向瀏覽器發送一條消息註釋行,以保持連接不斷。
3.2.1 event
事件類型。如果指定瞭該字段,則在瀏覽器收到該條消息時,會在當前 EventSource
對象(見 4)上觸發一個事件,事件類型就是該字段的字段值。可以使用 addEventListener
方法在當前 EventSource
對象上監聽任意類型的命名事件。 如果該條消息沒有 event
字段,則會觸發 EventSource
對象 onmessage
屬性上的事件處理函數。
3.2.2 id
事件ID。事件的唯一標識符,瀏覽器會跟蹤事件ID,如果發生斷連,瀏覽器會把收到的最後一個事件ID放到 HTTP Header Last-Event-Id
中進行重連,作為一種簡單的同步機制。 例如可以在服務端將每次發送的事件ID值自動加 1,當瀏覽器接收到該事件ID後,下次與服務端建立連接後再請求的 Header 中將同時提交該事件ID,服務端檢查該事件ID是否為上次發送的事件ID,如果與上次發送的事件ID不一致則說明瀏覽器存在與服務器連接失敗的情況,本次需要同時發送前幾次瀏覽器未接收到的數據。
3.2.3 retry
重連時間。整數值,單位 ms,如果與服務器的連接丟失,瀏覽器將等待指定時間,然後嘗試重新連接。如果該字段不是整數值,會被忽略。 當服務端沒有指定瀏覽器的重連時間時,由瀏覽器自行決定每隔多久與服務端建立一次連接(一般為 30s)。
3.2.4 data
消息數據。數據內容隻能以一個字符串的文本形式進行發送,如果需要發送一個對象時,需要將該對象以一個 JSON 格式的字符串的形式進行發送。在瀏覽器接收到該字符串後,再把它還原為一個 JSON 對象。
3.3 示例
如下事件流示例,共發送瞭 4 條消息,每條消息間以一個空行作為分隔符。 第一條僅僅是個註釋,因為它以冒號開頭。 第二條消息隻包含一個 data 字段,值為 'this is second message'。 第三條消息包含兩個 data 字段,其會被解析為一個字段,值為 'this is third message part 1\nthis is third message part 2'。 第四條消息包含完整四個字段,指定瞭事件類型為 'server-time',事件id 為 '1',重連時間為 '30000'ms,消息數據為 JSON
格式的 '{"text": "this is fourth message", "time": "12:00:00"}'。
: this is first message data: this is second message data: this is third message part one data this is third message part two event: server-time id: 1 retry: 30000 data: {"text": "this is fourth message", "time": "2023-04-09 12:00:00"}
4 瀏覽器 API
在瀏覽器端,可以使用 JavaScript 的 EventSource API 創建 EventSource
對象監聽服務器發送的事件。一旦建立連接,服務器就可以使用 HTTP 響應的 'text/event-stream' 內容類型發送事件消息,瀏覽器則可以通過監聽 EventSource 對象的 onmessage
、onopen
和 onerror
事件來處理這些消息。
4.1 建立連接
EventSource 接受兩個參數:URL 和 options。 URL 為 http 事件來源,一旦 EventSource 對象被創建後,瀏覽器立即開始對該 URL 地址發送過來的事件進行監聽。 options 是一個可選的對象,包含 withCredentials 屬性,表示是否發送憑證(cookie、HTTP認證信息等)到服務端,默認為 false。
const eventSource = new EventSource('http_api_url', { withCredentials: true }) 復制代碼
與 XMLHttpRequest 對象類型,EventSource 對象有一個 readyState 屬性值,具體含義如下表:
readyState | 含義 |
---|---|
0 | 瀏覽器與服務端尚未建立連接或連接已被關閉 |
1 | 瀏覽器與服務端已成功連接,瀏覽器正在處理接收到的事件及數據 |
2 | 瀏覽器與服務端建立連接失敗,客戶端不再繼續建立與服務端之間的連接 |
可以使用 EventSource 對象的 close
方法關閉與服務端之間的連接,使瀏覽器不再建立與服務端之間的連接。
// 初始化 eventSource 等省略 // 關閉連接 eventSource.close()
4.2 監聽事件
EventSource 對象本身繼承自 EventTarget 接口,因此可以使用 addEventListener() 方法來監聽事件。EventSource 對象觸發的事件主要包括以下三種:
- open 事件:當成功連接到服務端時觸發。
- message 事件:當接收到服務器發送的消息時觸發。該事件對象的 data 屬性包含瞭服務器發送的消息內容。
- error 事件:當發生錯誤時觸發。該事件對象的 event 屬性包含瞭錯誤信息。
// 初始化 eventSource 等省略 eventSource.addEventListener('open', function(event) { console.log('Connection opened') }) eventSource.addEventListener('message', function(event) { console.log('Received message: ' + event.data); }) // 監聽自定義事件 eventSource.addEventListener('xxx', function(event) { console.log('Received message: ' + event.data); }) eventSource.addEventListener('error', function(event) { console.log('Error occurred: ' + event.event); })
當然,也可以采用屬性監聽(onopen
、onmessage
、onerror
)的形式。
// 初始化 eventSource 等省略 eventSource.onopen = function(event) { console.log('Connection opened') } eventSource.onmessage = function(event) { console.log('Received message: ' + event.data); } eventSource.onerror = function(event) { console.log('Error occurred: ' + event.event) })
📢註意:
EventSource
對象的屬性監聽隻能監聽預定義的事件類型(open
、message
、error
)。不能用於監聽自定義事件類型。如果要實現自定義事件類型的監聽,可以使用addEventListener()
方法。
5 實踐
5.1 服務端
使用 Node.js 實現 SSE 的簡單示例:
const http = require('http'); const fs = require('fs'); http.createServer((req, res) => { if (req.url === '/') { // 如果請求根路徑,返回 index.html 文件 fs.readFile('index.html', (err, data) => { if (err) { res.writeHead(500); res.end('Error loading index.html'); } else { res.writeHead(200, {'Content-Type': 'text/html'}); res.end(data); } }); } else if (req.url === '/events') { // 如果請求 /events 路徑,建立 SSE 連接 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // 每隔 1 秒發送一條消息 let id = 0; const intervalId = setInterval(() => { res.write(`event: customEvent\n`) res.write(`id: ${id}\n`) res.write(`retry: 30000\n`) const data = { id, time: new Date().toISOString()} res.write(`data: ${JSON.stringify(data)}\n\n`); id++ }, 1000); // 當客戶端關閉連接時停止發送消息 req.on('close', () => { clearInterval(intervalId); id = 0 res.end(); }); } else { // 如果請求的路徑無效,返回 404 狀態碼 res.writeHead(404); res.end(); } }).listen(3000); console.log('Server listening on port 3000');
5.2 瀏覽器
<!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>SSE Demo</title> </head> <body> <h1>SSE Demo</h1> <button onclick="connectSSE()">建立 SSE 連接</button> <button onclick="closeSSE()">斷開 SSE 連接</button> <br /> <br /> <div id="message"></div> <script> const messageElement = document.getElementById('message') let eventSource // 建立 SSE 連接 const connectSSE = () => { eventSource = new EventSource('/events') // 監聽消息事件 eventSource.addEventListener('customEvent', (event) => { const data = JSON.parse(event.data) messageElement.innerHTML += `${data.id} --- ${data.time}` + '<br />' }) eventSource.onopen = () => { messageElement.innerHTML += `SSE 連接成功,狀態${eventSource.readyState}<br />` } eventSource.onerror = () => { messageElement.innerHTML += `SSE 連接錯誤,狀態${eventSource.readyState}<br />` } } // 斷開 SSE 連接 const closeSSE = () => { eventSource.close() messageElement.innerHTML += `SSE 連接關閉,狀態${eventSource.readyState}<br />` } </script> </body> </html>
將上面的兩份代碼保存為 server.js
和 index.html
,並在命令行中執行 node server.js
啟動服務端,然後在瀏覽器中打開 http://localhost:3000
即可看到 SSE 效果。
6 兼容性
發展至今,SSE 已具有廣泛的的瀏覽器兼容性,幾乎除 IE 之外的瀏覽器均已支持。
對於不支持 EventSource 的瀏覽器,可以使用 polyfill 實現。判斷瀏覽器是否支持 EventSource:
if(typeof(EventSource) !== “undefined”) { // 支持 } else { // 不支持,使用 polyfill }
7 總結
SSE 技術是一種輕量級的實時通信技術,基於 HTTP 協議,具有服務端推送、斷線重連、簡單輕量等優點。但是,SSE 技術也有一些缺點,如不能進行雙向通信、連接數受限等。
SSE 可以在 Web 應用程序中實現諸如股票在線數據、日志推送、聊天室實時人數等即時數據推送功能。需要註意的是,SSE 並不是適用於所有的實時推送場景。在需要高並發、高吞吐量和低延遲的場景下,WebSockets 可能更加適合。而在需要更輕量級的推送場景下,SSE 可能更加適合。因此,在選擇即時更新方案時,需要根據具體的需求和場景進行選擇。
以上就是基於JavaScript的ChatGPT打字機消息回復實現原理的詳細內容,更多關於ChatGPT打字機消息回復的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- java實現web實時消息推送的七種方案
- Iframe跨窗口通信原理詳解
- 使用postMessage實現iframe跨域通信的示例代碼
- 如何利用js在兩個html窗口間通信
- jQuery中ajax的相關知識點匯總