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