寫一個類ChatGPT應(yīng)用,前后端數(shù)據(jù)交互有哪幾種
前言
最近,公司有一個AI項目,要做一個文檔問答的AI產(chǎn)品。前端部分呢,還是「友好借鑒」ChatGPT。別問為什么,問就是要站在巨人的肩膀上進行「帶有中國特色」的創(chuàng)新。而后端是接入我們團隊的模型,我咨詢過模型團隊,也是基于開源模型做參數(shù)的微調(diào),這個魔幻的世界真讓人欲罷不能。這就是大概的業(yè)務(wù)背景。
針對前端部分,其實沒啥可聊的,就是接入模型返回的數(shù)據(jù)然后進行展示處理。大家以為這就是一個簡單到令人發(fā)指的功能時。有一個點卻映入眼簾,如何才能實現(xiàn)類似ChatGPT結(jié)果展示效果(逐步輸出結(jié)果,類似打字效果)。也就是在結(jié)果返回的時候,如何做打字效果。
此外,還有一個大背景就是,由于需求是可能要上傳多個文件,并且模型那邊的操作可能對文檔解析有一定的難度。所以,在客戶端發(fā)起請求時,可能投喂給模型的物料有點多,返回的結(jié)果的時間也會很長。也就是如果處理不當?shù)脑?,在結(jié)果沒返回之前或者一股腦把結(jié)果處理完再返回的話,前端會有一段很長的等待時間。
從上面的需求點和解決方案,我們不難看出,其實結(jié)果的展示(打字效果)不是一個難點,我們可以借助簡單的庫或者手搓一個打字效果都是可以的,而是數(shù)據(jù)的獲取制約我們應(yīng)用響應(yīng)。
我們又可以按照數(shù)據(jù)的發(fā)起方是誰(客戶端/服務(wù)端)
- 基于最原始的數(shù)據(jù)獲取方式,客戶端發(fā)起請求,服務(wù)端接入模型數(shù)據(jù)并返回,然后前端一股腦把所以結(jié)果都接入。
- 數(shù)據(jù)的發(fā)起方是服務(wù)端,然后在有合適的數(shù)據(jù)時,就將其發(fā)布給客戶端,前端接收到數(shù)據(jù)后就進行結(jié)果的顯示。此處我們可以按照流式將數(shù)據(jù)返回
所以,這又引起了另外一個問題,前后端數(shù)據(jù)交互我們應(yīng)該采用何種方式。其實針對,后端主動發(fā)起數(shù)據(jù)的方式我們有很多方案
- 長輪詢(Long-Polling)
- WebSockets
- 服務(wù)器發(fā)送事件(Server-Sent Events,SSE)
- WebRTC
- WebTransport
那我們到底用哪種方式亦或者說它們都是個啥,都有啥優(yōu)缺點。所以,今天我們來用一篇文章來講講它們直接的區(qū)別和聯(lián)系。
好了,天不早了,干點正事哇。
我們能所學(xué)到的知識點
- 長輪詢(Long-Polling)
- WebSockets
- 服務(wù)器發(fā)送事件(SSE)
- WebTransport
- WebRTC
- 技術(shù)的限制
- 性能比較
- 適用場景
1. 長輪詢(Long-Polling)
長輪詢可以在瀏覽器上通過 HTTP 啟用一種服務(wù)器-客戶端消息傳遞方法。該技術(shù)通過普通的 XHR 請求模擬了服務(wù)器推送通信。與傳統(tǒng)的輪詢不同,其中客戶端會在「固定的時間間隔內(nèi)重復(fù)向服務(wù)器請求數(shù)據(jù)」,長輪詢建立了一條連接到服務(wù)器的連接,該連接保持打開狀態(tài),直到有新數(shù)據(jù)可用為止。一旦服務(wù)器有了新信息,就會將響應(yīng)發(fā)送給客戶端,并關(guān)閉連接。
在接收到服務(wù)器的響應(yīng)后,客戶端立即發(fā)起新的請求,這個過程會重復(fù)進行。這種方法允許「更即時地更新數(shù)據(jù),并減少不必要的網(wǎng)絡(luò)流量和服務(wù)器負載」。然而,它仍然可能引入通信延遲,并且不如其他實時技術(shù)(如 WebSockets)高效。
function longPoll() {
fetch('http://front789.com/poll')
.then(response => response.json())
.then(data => {
console.log("接收到的數(shù)據(jù):", data);
longPoll(); // 立即發(fā)起新的長輪詢請求
})
.catch(error => {
/**
* 在正常情況下可能會出現(xiàn)錯誤,
* 當連接超時或客戶端離線時。
* 出現(xiàn)錯誤時,我們會在一段延遲后重新啟動輪詢。
*/
setTimeout(longPoll, 10000);
});
}
longPoll(); // 初始化長輪詢
長輪詢解決了在網(wǎng)絡(luò)平臺上構(gòu)建雙向應(yīng)用程序的問題,也就是我們經(jīng)常用的模式- 「客戶端發(fā)出請求,服務(wù)器響應(yīng)」。這是通過顛覆請求-響應(yīng)模型來實現(xiàn)的:
- 客戶端向服務(wù)器發(fā)送 GET 請求:與傳統(tǒng)的 HTTP 請求不同,我們可以將其視為開放式的。它不是請求特定的響應(yīng),而是在準備好時請求任何響應(yīng)。
- 請求時間設(shè)置:HTTP 超時可以使用 Keep-Alive 頭進行調(diào)整。
長輪詢利用此功能,通過設(shè)置非常長或無限期的超時時間,使請求保持打開狀態(tài),即使服務(wù)器沒有立即響應(yīng)。
- 服務(wù)器響應(yīng):當服務(wù)器有要發(fā)送的內(nèi)容時,它會使用響應(yīng)關(guān)閉連接。
返回的數(shù)據(jù)可以是新的聊天消息、體育比分或突發(fā)新聞等。
客戶端發(fā)送新的 GET 請求,循環(huán)重新開始。
圖片
2. WebSockets
WebSockets[1] 是一種實時技術(shù),可通過持久的單套接字(socket)連接在客戶端和服務(wù)器之間實現(xiàn)「雙向全雙工通信」。WebSockets 相對于傳統(tǒng)的 HTTP,代表了一個重大進步,因為一旦建立連接,雙方就可以「獨立發(fā)送數(shù)據(jù)」,這使其非常適合需要低延遲和高頻更新的場景。
WebSocket 技術(shù)由兩個核心構(gòu)建塊組成:
- WebSocket協(xié)議:WebSocket是建立在TCP協(xié)議之上的一種「應(yīng)用層協(xié)議」。該協(xié)議旨在允許客戶端和服務(wù)器「實時通信」,從而在 Web 應(yīng)用程序中實現(xiàn)高效且響應(yīng)迅速的數(shù)據(jù)傳輸。
- WebSocket API:WebSocket API 是一個編程接口,用于創(chuàng)建 WebSocket 連接并管理 Web 應(yīng)用程序中客戶端和服務(wù)器之間的數(shù)據(jù)交換。幾乎所有現(xiàn)代瀏覽器都支持 WebSocket API
圖片
如何工作的
概括地說,使用 WebSockets 涉及三個主要步驟:
- 打開 WebSocket 連接
建立 WebSocket 連接的過程稱為握手,由客戶端和服務(wù)器之間的 HTTP 請求/響應(yīng)交換組成。
- 通過 WebSockets 傳輸數(shù)據(jù)
成功打開握手后,客戶端和服務(wù)器可以通過持久 WebSocket 連接交換消息(幀)。WebSocket 消息可能包含字符串(純文本)或二進制數(shù)據(jù)。
關(guān)閉 WebSocket 連接。
一旦持久的 WebSocket 連接達到其目的,它就可以終止;
客戶端和服務(wù)器都可以通過發(fā)送關(guān)閉消息來啟動關(guān)閉握手。
圖片
// 創(chuàng)建 `WebSocket` 連接
const socket = new WebSocket("ws://localhost:7899");
// 打開鏈接,并發(fā)送信息
socket.addEventListener("open", (event) => {
socket.send("Hello Front789!");
});
// 監(jiān)聽來自服務(wù)端的數(shù)據(jù)
socket.addEventListener("message", (event) => {
console.log("來自服務(wù)端的數(shù)據(jù)", event.data);
});
// 關(guān)閉鏈接
socket.onclose = function(e) {
console.log("關(guān)閉鏈接", e);
};
雖然 WebSocket API 的基礎(chǔ)用法很容易,但在生產(chǎn)環(huán)境中卻相當復(fù)雜。一個 socket 可能會斷開連接,必須相應(yīng)地重新創(chuàng)建。特別是檢測連接是否仍然可用或不可用可能會非常棘手。通常,我們會添加一個 ping-and-pong[2] 心跳以確保打開的連接不會關(guān)閉。我們可以借助類似像 Socket.IO[3] 這樣的庫來處理重連的情況,需要時提供了以「長輪詢」為回退方案。
想了解更多關(guān)于WebSocket可以參考The WebSocket API and protocol explained[4]
3. 服務(wù)器發(fā)送事件(SSE)
服務(wù)器發(fā)送事件(Server-Sent Events,SSE)提供了一種標準方法,通過 HTTP 將服務(wù)器數(shù)據(jù)推送到客戶端。與 WebSockets 不同,SSE 專門設(shè)計用于「服務(wù)器到客戶端的單向通信」,使其非常適用于實時信息的更新或者那些在不向服務(wù)器發(fā)送數(shù)據(jù)的情況下實時更新客戶端的情況。
我們可以將服務(wù)器發(fā)送事件視為單個 HTTP 請求,其中后端不會立即發(fā)送整個主體,而是保持連接打開,并通過每次發(fā)送事件時發(fā)送單個行來逐步傳輸答復(fù)。
圖片
SSE是一個由兩個組件組成的標準:
- 瀏覽器中的 EventSource 接口,允許客戶端訂閱事件:它提供了一種通過抽象較低級別的連接和消息處理來訂閱事件流的便捷方法。
- 事件流協(xié)議:描述服務(wù)器發(fā)送的事件必須遵循的標準純文本格式,以便 EventSource 客戶端理解和傳播它們
在瀏覽器的客戶端上,我們可以使用服務(wù)器端生成事件腳本的 URL 初始化一個 EventSource[5] 實例。
// 連接到服務(wù)器端事件流
const evtSource = new EventSource("https://front789.com/events");
// 處理通用消息事件
evtSource.onmessage = event => {
if(event.data.trim() !== 'undefined'){
const newData = event.data;
// 數(shù)據(jù)追加
setResponse((prevResponse) => prevResponse.concat(newData));
} else{
// 當從服務(wù)端接收到值為`undefined`的數(shù)據(jù)時,關(guān)閉鏈接
setTempPrompt('');
eventSource.close();
}
};
與 WebSockets 不同,EventSource 在連接丟失時會自動重新連接。
在服務(wù)器端,我們的腳本必須將 Content-Type 標頭設(shè)置為 text/event-stream,并根據(jù) SSE 規(guī)范[6]格式化每條消息。這包括指定事件類型、數(shù)據(jù)有效負載和可選字段,如事件 ID。
以下是使用Node.js Express處理SSE的示例:
import express from 'express';
const app = express();
const PORT = process.env.PORT || 7890;
const headers = {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
}
app.get('/events', (req, res) => {
res.writeHead(200, headers);
const sendEvent = (data) => {
// 所有數(shù)據(jù)都必須以'data:'開頭
const formattedData = `data: ${JSON.stringify(data)}\n\n`;
res.write(formattedData);
};
// 每兩秒發(fā)送一個事件
const intervalId = setInterval(() => {
const message = {
time: new Date().toTimeString(),
message: '服務(wù)端產(chǎn)生的數(shù)據(jù)',
};
sendEvent(message);
}, 2000);
// 關(guān)閉輪詢
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
文章最開始,我們不是說想實現(xiàn)實時響應(yīng)后端返回并且逐字顯示聊天機器人回復(fù),我們其實就可以使用SSE的方案。而ChatGPT也是使用這個機制實現(xiàn)的。
4. WebTransport
WebTransport[7] 是一個專為 Web 客戶端和服務(wù)器之間進行高效、低延遲通信而設(shè)計的前沿 API。它利用了 HTTP/3 QUIC 協(xié)議[8],可以實現(xiàn)以可靠和不可靠的方式實現(xiàn)多個流的數(shù)據(jù)傳輸功能,甚至允許數(shù)據(jù)無序發(fā)送。這使得 WebTransport 成為需要高性能網(wǎng)絡(luò)的應(yīng)用程序的強大工具,如實時游戲、直播和協(xié)作平臺。但是,值得注意的是,WebTransport 目前是一個工作草案,尚未被廣泛采用。
圖片
截至目前(2024 年 5 月),WebTransport 仍處于工作草案階段[9],并沒有得到廣泛支持。
圖片
目前還不能在 Safari 瀏覽器中使用 WebTransport,而且 Node.js 也沒有原生支持。這限制了其在不同平臺和環(huán)境中的可用性。
5. WebRTC
網(wǎng)頁實時通信(Web Real-time Communication,WebRTC)[10]是一個增強網(wǎng)頁瀏覽模式。它允許瀏覽器通過安全訪問輸入設(shè)備(如網(wǎng)絡(luò)攝像頭和麥克風),以「點對點的方式直接與其他瀏覽器交換實時媒體數(shù)據(jù)」。
WebRTC 既是 API 又是協(xié)議。
- WebRTC 協(xié)議是一組規(guī)則,供兩個 WebRTC 代理協(xié)商雙向安全實時通信。
- WebRTC API 允許開發(fā)人員使用 WebRTC 協(xié)議。WebRTC API 僅針對 JavaScript。
傳統(tǒng)的網(wǎng)頁架構(gòu)是基于客戶端-服務(wù)器模型,客戶端發(fā)送HTTP請求到服務(wù)器并獲得包含所請求信息的響應(yīng)。與此相對,WebRTC允許N個實體之間交換數(shù)據(jù)。在這種交換中,實體彼此直接通信,而無需中間服務(wù)器。
WebRTC內(nèi)置于HTML 5,因此我們不需要第三方軟件或插件即可使用它,我們可以通過WebRTC API在瀏覽器中訪問它。它支持瀏覽器之間的音頻、視頻和數(shù)據(jù)流交換的點對點連接。WebRTC 設(shè)計用于通過 NAT 和防火墻工作,利用諸如 ICE、STUN 和 TURN 等協(xié)議來建立對等之間的連接。
雖然 WebRTC 是為客戶端-客戶端交互設(shè)計的,但也可以利用它進行服務(wù)器-客戶端通信,其中「服務(wù)器只是模擬成一個客戶端」。這種方法只適用于特定的用例,問題在于,要使 WebRTC 正常工作,我們?nèi)匀恍枰粋€服務(wù)器,這個服務(wù)器會再次通過 WebSockets、SSE 或 WebTransport 運行。這就背離了使用 WebRTC 作為這些技術(shù)的替代方案的初衷。
6. 技術(shù)的限制
雙向發(fā)送數(shù)據(jù)
只有 WebSockets 和 WebTransport 是「雙向全雙工通信」,這樣我們就可以在同一個連接上接收服務(wù)器數(shù)據(jù)并發(fā)送客戶端數(shù)據(jù)。
雖然理論上使用長輪詢也是可能的,但并不建議,因為向現(xiàn)有的長輪詢連接發(fā)送“新”數(shù)據(jù)實際上還是需要額外的 HTTP 請求。因此,我們可以通過額外的 HTTP 請求直接將數(shù)據(jù)從客戶端發(fā)送到服務(wù)器,而不會中斷長輪詢連接。
SSE不支持向服務(wù)器發(fā)送任何附加數(shù)據(jù)。我們只能進行初始請求,即使在原生的 EventSource API 中,默認情況下也無法在 HTTP 主體中發(fā)送類似 POST 的數(shù)據(jù)。相反,我們必須將所有數(shù)據(jù)放在 URL 參數(shù)中,這被認為是一種不安全的做法,因為憑據(jù)可能會泄漏到服務(wù)器日志、代理和緩存中。
每個域的 6 個請求限制
大多數(shù)現(xiàn)代瀏覽器允許「每個域最多六個連接」這限制了服務(wù)器-客戶端消息傳遞方法的可用性。這六個連接的限制甚至在瀏覽器選項卡之間共享,因此當我們在多個選項卡中打開相同的頁面時,它們必須彼此共享六個連接池。
雖然這個策略可以防止D-DOS 攻擊,但當多個連接是為了處理合法的通信時,它可能會造成很大的問題。為了解決這個限制,我們必須使用 HTTP/2 或 HTTP/3,其中瀏覽器為每個域只會打開一個連接,然后使用「多路復(fù)用」來通過單個連接傳輸所有數(shù)據(jù)。雖然這樣可以給我們幾乎無限量的并行連接,但有一個 SETTINGS_MAX_CONCURRENT_STREAMS[11] 設(shè)置,它限制了實際的連接數(shù)量。對于大多數(shù)配置,默認值為 100 個并發(fā)流。
在移動應(yīng)用程序中不保持連接
在 Android 和 iOS 等操作系統(tǒng)上運行的移動應(yīng)用程序中,保持打開連接(例如 WebSockets 和其他連接)會帶來很大的挑戰(zhàn)。移動操作系統(tǒng)被設(shè)計為「在一段時間的不活動后自動將應(yīng)用程序移至后臺,從而有效關(guān)閉任何打開的連接」。這種行為是操作系統(tǒng)資源管理策略的一部分,旨在節(jié)省電池并優(yōu)化性能。因此,我們通常依賴于移動推送通知作為一種高效可靠的方法,以將數(shù)據(jù)從服務(wù)器發(fā)送到客戶端。推送通知允許服務(wù)器提醒應(yīng)用程序有新數(shù)據(jù)到達,促使執(zhí)行某個操作或更新,而無需保持持續(xù)的打開連接。
7. 性能比較
對于一些我們平時可能會用到的技術(shù)例如WebSockets、SSE、長輪詢和 WebTransport 我們可以從延遲、吞吐量、服務(wù)器負載和在不同條件下的可伸縮性的角度來比較。
延遲
- WebSockets:由于其通過單個持久連接進行全雙工通信,提供了最低的延遲。適用于實時應(yīng)用程序,其中立即數(shù)據(jù)交換至關(guān)重要。
- SSE:也提供了低延遲的服務(wù)器到客戶端通信,但不能直接發(fā)送消息回服務(wù)器,需要額外的 HTTP 請求。
- 長輪詢:由于依賴于為每個數(shù)據(jù)傳輸「建立新的 HTTP 連接」,因此產(chǎn)生較高的延遲,使其對實時更新不太有效。此外,當服務(wù)器希望在客戶端仍在打開新連接的過程中發(fā)送事件時,可能會出現(xiàn)延遲顯著較大的情況。
- WebTransport:承諾提供類似于 WebSockets 的低延遲,同時利用 HTTP/3 協(xié)議進行更高效的多路復(fù)用和擁塞控制。
吞吐量
- WebSockets:由于其持久連接,能夠?qū)崿F(xiàn)高吞吐量,但當客戶端無法處理數(shù)據(jù)時,吞吐量可能會受到反壓的影響,反壓[12]是指客戶端無法處理服務(wù)器發(fā)送的數(shù)據(jù)速度。
- SSE:對于向客戶端廣播消息而言,效率高于 WebSockets,開銷較小,因此在單向的服務(wù)器到客戶端通信中可能會實現(xiàn)更高的吞吐量。
- 長輪詢:由于頻繁打開和關(guān)閉連接的開銷較大,通常提供較低的吞吐量,這會「消耗更多的服務(wù)器資源」。
- WebTransport:支持單個連接內(nèi)的雙向和單向數(shù)據(jù)流的高吞吐量,性能優(yōu)于需要多個流的場景下的 WebSockets。
可伸縮性和服務(wù)器負載
- WebSockets:維護大量 WebSocket 連接可能會顯著增加服務(wù)器負載,可能影響具有許多用戶的應(yīng)用程序的可伸縮性。
- SSE:對于主要需要來自服務(wù)器到客戶端的更新的場景,更具可伸縮性,因為與 WebSockets 相比,它使用的連接開銷更小,因為它使用的是常規(guī)的 HTTP 請求,而不是像 WebSockets 那樣需要運行協(xié)議更新的請求。
- 長輪詢:由于頻繁建立連接產(chǎn)生的高服務(wù)器負載,所以是最不可伸縮的,通常僅適用于作為「后備機制」。
- WebTransport:設(shè)計為高度可伸縮,受益于 HTTP/3 在處理連接和流時的高效性,與 WebSockets 和 SSE 相比,可能減少服務(wù)器負載。
8. 適用場景
在服務(wù)器-客戶端通信技術(shù)的領(lǐng)域中,每種技術(shù)都有其獨特的優(yōu)勢和適用用例。SSE是最簡單的實現(xiàn)選項,利用與傳統(tǒng) Web 請求相同的 HTTP/S 協(xié)議,因此可以規(guī)避企業(yè)防火墻限制和其他可能出現(xiàn)的技術(shù)問題。它們很容易集成到 Node.js 和其他服務(wù)器框架中,因此非常適合需要頻繁服務(wù)器到客戶端更新的應(yīng)用程序,如新聞源、股票行情和實時事件流。
另一方面,WebSockets 在需要持續(xù)的雙向通信的場景中表現(xiàn)出色。它們支持連續(xù)互動的能力,使其成為瀏覽器游戲、聊天應(yīng)用程序和實時體育更新的首選。
然而,WebTransport 雖然潛力巨大,但面臨著采用挑戰(zhàn)。它在包括 Node.js 在內(nèi)的服務(wù)器框架中得到的支持不廣泛,并且與 Safari 不兼容。此外,它對 HTTP/3 的依賴進一步限制了其即時適用性,因為許多 Web 服務(wù)器(如 nginx)只有實驗性的 HTTP/3 支持。雖然在支持可靠和不可靠數(shù)據(jù)傳輸?shù)奈磥響?yīng)用程序中有所希望,但在大多數(shù)用例中,WebTransport 還不是一個可行的選擇。
長輪詢曾經(jīng)是一種常見的技術(shù),但由于其效率低下和頻繁建立新的 HTTP 連接的高開銷,現(xiàn)在已經(jīng)大大過時。雖然它可以作為沒有對 WebSockets 或 SSE 進行支持的環(huán)境的后備方案,但由于存在顯著的性能限制,通常不建議使用。