摘要:數(shù)據(jù)作為消息通過傳輸,每個(gè)消息由一個(gè)或多個(gè)幀組成,其中包含正在發(fā)送的數(shù)據(jù)有效負(fù)載。幀數(shù)據(jù)如上所述,數(shù)據(jù)可以被分割成多個(gè)幀。但是,規(guī)范希望能夠處理交錯(cuò)的控制幀。
文章底部分享給大家一套 react + socket 實(shí)戰(zhàn)教程
這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第5篇。
想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你!
如果你錯(cuò)過了前面的章節(jié),可以在這里找到它們:
JavaScript是如何工作的:引擎,運(yùn)行時(shí)和調(diào)用堆棧的概述!
JavaScript是如何工作的:深入V8引擎&編寫優(yōu)化代碼的5個(gè)技巧
JavaScript如何工作:內(nèi)存管理+如何處理4個(gè)常見的內(nèi)存泄漏
JavaScript是如何工作的:事件循環(huán)和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
這一次,我們將深入到通信協(xié)議的領(lǐng)域,映射和探討它們的屬性,并在此過程中構(gòu)建部分組件。快速比較WebSockets和 HTTP/2。最后,我們分享一些關(guān)于如何選擇網(wǎng)絡(luò)協(xié)議的方法。
簡介如今,功能豐富、動(dòng)態(tài) ui 的復(fù)雜 web 應(yīng)用程序被認(rèn)為是理所當(dāng)然。這并不奇怪——互聯(lián)網(wǎng)自誕生以來已經(jīng)走過了漫長的道路。
最初,互聯(lián)網(wǎng)并不是為了支持這種動(dòng)態(tài)和復(fù)雜的 web 應(yīng)用程序而構(gòu)建的。它被認(rèn)為是HTML頁面的集合,相互鏈接形成一個(gè)包含信息的 “web” 概念。一切都是圍繞 HTTP 的所謂 請(qǐng)求/響應(yīng) 范式構(gòu)建的。客戶端加載一個(gè)頁面,然后在用戶單擊并導(dǎo)航到下一個(gè)頁面之前什么都不會(huì)發(fā)生。
大約在2005年,AJAX被引入,很多人開始探索在客戶端和服務(wù)器之間建立雙向連接的可能性。盡管如此,所有HTTP 通信都由客戶端引導(dǎo),客戶端需要用戶交互或定期輪詢以從服務(wù)器加載新數(shù)據(jù)。
讓 HTTP 變成“雙向”交互讓服務(wù)器能夠“主動(dòng)”向客戶機(jī)發(fā)送數(shù)據(jù)的技術(shù)已經(jīng)出現(xiàn)了相當(dāng)長的時(shí)間。例如“Push”和“Comet”。
最常見的一種黑客攻擊方法是讓服務(wù)器產(chǎn)生一種需要向客戶端發(fā)送數(shù)據(jù)的錯(cuò)覺,這稱為長輪詢。通過長輪詢,客戶端打開與服務(wù)器的 HTTP 連接,使其保持打開狀態(tài),直到發(fā)送響應(yīng)為止。 每當(dāng)服務(wù)器有新數(shù)據(jù)時(shí)需要發(fā)送時(shí),就會(huì)作為響應(yīng)發(fā)送。
看看一個(gè)非常簡單的長輪詢代碼片段是什么樣的:
(function poll(){ setTimeout(function(){ $.ajax({ url: "https://api.example.com/endpoint", success: function(data) { // Do something with `data` // ... //Setup the next poll recursively poll(); }, dataType: "json" }); }, 10000); })();
這基本上是一個(gè)自執(zhí)行函數(shù),第一次立即運(yùn)行時(shí),它設(shè)置了 10 秒間隔,在對(duì)服務(wù)器的每個(gè)異步Ajax調(diào)用之后,回調(diào)將再次調(diào)用Ajax。
其他技術(shù)涉及 Flash 或 XHR multipart request 和所謂的 htmlfiles 。
但是,所有這些工作區(qū)都有一個(gè)相同的問題:它們都帶有 HTTP 的開銷,這使得它們不適合于低延遲應(yīng)用程序。想想瀏覽器中的多人第一人稱射擊游戲或任何其他帶有實(shí)時(shí)組件的在線游戲。
WebSockets 的引入WebSocket 規(guī)范定義了在 web 瀏覽器和服務(wù)器之間建立“套接字”連接的 API。簡單地說:客戶機(jī)和服務(wù)器之間存在長久連接,雙方可以隨時(shí)開始發(fā)送數(shù)據(jù)。
客戶端通過 WebSocket 握手 過程建立 WebSocket 連接。這個(gè)過程從客戶機(jī)向服務(wù)器發(fā)送一個(gè)常規(guī) HTTP 請(qǐng)求開始,這個(gè)請(qǐng)求中包含一個(gè)升級(jí)頭,它通知服務(wù)器客戶機(jī)希望建立一個(gè) WebSocket 連接。
客戶端建立 WebSocket 連接方式如下:
// Create a new WebSocket with an encrypted connection. var socket = new WebSocket("ws://websocket.example.com")
WebSocket url使用 ws 方案。還有 wss 用于安全的 WebSocket 連接,相當(dāng)于HTTPS。
這個(gè)方案只是打開 websocket.example.com 的 WebSocket 連接的開始。
下面是初始請(qǐng)求頭的一個(gè)簡化示例:
如果服務(wù)器支持 WebSocke t協(xié)議,它將同意升級(jí),并通過響應(yīng)中的升級(jí)頭進(jìn)行通信。
Node.js 的實(shí)現(xiàn)方式:
建立連接后,服務(wù)器通過升級(jí)頭部中內(nèi)容時(shí)行響應(yīng):
一旦建立連接,open 事件將在客戶端 WebSocket 實(shí)例上被觸發(fā):
var socket = new WebSocket("ws://websocket.example.com"); // Show a connected message when the WebSocket is opened. socket.onopen = function(event) { console.log("WebSocket is connected."); };
現(xiàn)在握手已經(jīng)完成,初始 HTTP 連接被使用相同底層 TCP/IP 連接的 WebSocket 連接替換。此時(shí),雙方都可以開始發(fā)送數(shù)據(jù)。
使用 WebSockets,可以傳輸任意數(shù)量的數(shù)據(jù),而不會(huì)產(chǎn)生與傳統(tǒng) HTTP 請(qǐng)求相關(guān)的開銷。數(shù)據(jù)作為消息通過 WebSocket 傳輸,每個(gè)消息由一個(gè)或多個(gè)幀組成,其中包含正在發(fā)送的數(shù)據(jù)(有效負(fù)載)。為了確保消息在到達(dá)客戶端時(shí)能夠正確地進(jìn)行重構(gòu),每一幀都以負(fù)載的4-12字節(jié)數(shù)據(jù)為前綴, 使用這種基于幀的消息傳遞系統(tǒng)有助于減少傳輸?shù)姆怯行ж?fù)載數(shù)據(jù)量,從而大大的減少延遲。
注意:值得注意的是,只有在接收到所有幀并重構(gòu)了原始消息負(fù)載之后,客戶機(jī)才會(huì)收到關(guān)于新消息的通知。WebSocket URLs
之前簡要提到過 WebSockets 引入了一個(gè)新的URL方案。實(shí)際上,他們引入了兩個(gè)新的方案:ws:// 和wss://。
url 具有特定方案的語法。WebSocket url 的特殊之處在于它們不支持錨點(diǎn)(#sample_anchor)。
同樣的規(guī)則適用于 WebSocket 風(fēng)格的url和 HTTP 風(fēng)格的 url。ws 是未加密的,默認(rèn)端口為80,而 wss 需要TLS加密,默認(rèn)端口為 443。
幀協(xié)議更深入地了解幀協(xié)議,這是 RFC 為我們提供的:
在RFC 指定的 WebSocket 版本中,每個(gè)包前面只有一個(gè)報(bào)頭。然而,這是一個(gè)相當(dāng)復(fù)雜的報(bào)頭。以下是它的構(gòu)建模塊:
FIN :1bit ,表示是消息的最后一幀,如果消息只有一幀那么第一幀也就是最后一幀,F(xiàn)irefox 在 32K 之后創(chuàng)建了第二個(gè)幀。
RSV1,RSV2,RSV3:每個(gè)1bit,必須是0,除非擴(kuò)展定義為非零。如果接受到的是非零值但是擴(kuò)展沒有定義,則需要關(guān)閉連接。
Opcode:4bit,解釋 Payload 數(shù)據(jù),規(guī)定有以下不同的狀態(tài),如果是未知的,接收方必須馬上關(guān)閉連接。狀態(tài)如下:
0x00: 附加數(shù)據(jù)幀
0x01:文本數(shù)據(jù)幀 ?
0x02:二進(jìn)制數(shù)據(jù)幀 ? ?
0x3-7:保留為之后非控制幀使用
0x8:關(guān)閉連接幀
0x9:ping
0xA:pong
0xB-F(保留為后面的控制幀使用) ? ? ?
??
Mask:1bit,掩碼,定義payload數(shù)據(jù)是否進(jìn)行了掩碼處理,如果是1表示進(jìn)行了掩碼處理。
Masking-key:域的數(shù)據(jù)即是掩碼密鑰,用于解碼PayloadData。客戶端發(fā)出的數(shù)據(jù)幀需要進(jìn)行掩碼處理,所以此位是1。
Payload_len:7位,7 + 16位,7+64位,payload數(shù)據(jù)的長度,如果是0-125,就是真實(shí)的payload長度,如果是126,那么接著后面的2個(gè)字節(jié)對(duì)應(yīng)的16位無符號(hào)整數(shù)就是payload數(shù)據(jù)長度;如果是127,那么接著后面的8個(gè)字節(jié)對(duì)應(yīng)的64位無符號(hào)整數(shù)就是payload數(shù)據(jù)的長度。
Masking-key:0到4字節(jié),如果MASK位設(shè)為1則有4個(gè)字節(jié)的掩碼解密密鑰,否則就沒有。
Payload data:任意長度數(shù)據(jù)。包含有擴(kuò)展定義數(shù)據(jù)和應(yīng)用數(shù)據(jù),如果沒有定義擴(kuò)展則沒有此項(xiàng),僅含有應(yīng)用數(shù)據(jù)。
為什么 WebSocket 是基于幀而不是基于流?我不知道,就像你一樣,我很想了解更多,所以如果你有想法,請(qǐng)隨時(shí)在下面的回復(fù)中添加評(píng)論和資源。另外,關(guān)于這個(gè)主題的討論可以在 HackerNews 上找到。
幀數(shù)據(jù)如上所述,數(shù)據(jù)可以被分割成多個(gè)幀。 傳輸數(shù)據(jù)的第一幀有一個(gè)操作碼,表示正在傳輸什么類型的數(shù)據(jù)。 這是必要的,因?yàn)?JavaScript 在開始規(guī)范時(shí)幾乎不存在對(duì)二進(jìn)制數(shù)據(jù)的支持。 0x01 表示 utf-8 編碼的文本數(shù)據(jù),0x02 是二進(jìn)制數(shù)據(jù)。大多數(shù)人會(huì)發(fā)送 JSON ,在這種情況下,你可能要選擇文本操作碼。 當(dāng)你發(fā)送二進(jìn)制數(shù)據(jù)時(shí),它將在瀏覽器特定的 Blob 中表示。
通過 WebSocket 發(fā)送數(shù)據(jù)的API非常簡單:
var socket = new WebSocket("ws://websocket.example.com"); socket.onopen = function(event) { socket.send("Some message"); // Sends data to server. };
當(dāng) WebSocket 接收數(shù)據(jù)時(shí)(在客戶端),會(huì)觸發(fā)一個(gè)消息事件。此事件包括一個(gè)名為data的屬性,可用于訪問消息的內(nèi)容。
// Handle messages sent by the server. socket.onmessage = function(event) { var message = event.data; console.log(message); };
在Chrome開發(fā)工具:可以很容易地觀察 WebSocket 連接中每個(gè)幀中的數(shù)據(jù):
消息分片有效載荷數(shù)據(jù)可以分成多個(gè)多帶帶的幀。接收端應(yīng)該對(duì)它們進(jìn)行緩沖,直到設(shè)置好 fin 位。因此,可以將字符串“Hello World”發(fā)送到11個(gè)包中,每個(gè)包的長度為6(報(bào)頭長度)+ 1字節(jié)。控件包不允許分片。但是,規(guī)范希望能夠處理交錯(cuò)的控制幀。這是TCP包以任意順序到達(dá)的情況。
連接幀的邏輯大致如下:
接收第一幀
記住操作碼
將幀有效負(fù)載連接在一起,直到 fin 位被設(shè)置
斷言每個(gè)包的操作碼是零
分片目的是發(fā)送長度未知的消息。如果不分片發(fā)送,即一幀,就需要緩存整個(gè)消息,計(jì)算其長度,構(gòu)建frame并發(fā)送;使用分片的話,可使用一個(gè)大小合適的buffer,用消息內(nèi)容填充buffer,填滿即發(fā)送出去。
什么是跳動(dòng)檢測?主要目的是保障客戶端 websocket 與服務(wù)端連接狀態(tài),該程序有心跳檢測及自動(dòng)重連機(jī)制,當(dāng)網(wǎng)絡(luò)斷開或者后端服務(wù)問題造成客戶端websocket斷開,程序會(huì)自動(dòng)嘗試重新連接直到再次連接成功。
在使用原生websocket的時(shí)候,如果設(shè)備網(wǎng)絡(luò)斷開,不會(huì)觸發(fā)任何函數(shù),前端程序無法得知當(dāng)前連接已經(jīng)斷開。這個(gè)時(shí)候如果調(diào)用 websocket.send 方法,瀏覽器就會(huì)發(fā)現(xiàn)消息發(fā)不出去,便會(huì)立刻或者一定短時(shí)間后(不同瀏覽器或者瀏覽器版本可能表現(xiàn)不同)觸發(fā) onclose 函數(shù)。
后端 websocket 服務(wù)也可能出現(xiàn)異常,連接斷開后前端也并沒有收到通知,因此需要前端定時(shí)發(fā)送心跳消息 ping,后端收到 ping 類型的消息,立馬返回 pong 消息,告知前端連接正常。如果一定時(shí)間沒收到pong消息,就說明連接不正常,前端便會(huì)執(zhí)行重連。
為了解決以上兩個(gè)問題,以前端作為主動(dòng)方,定時(shí)發(fā)送 ping 消息,用于檢測網(wǎng)絡(luò)和前后端連接問題。一旦發(fā)現(xiàn)異常,前端持續(xù)執(zhí)行重連邏輯,直到重連成功。
錯(cuò)誤處理以通過監(jiān)聽 error 事件來處理所有錯(cuò)誤:
var socket = new WebSocket("ws://websocket.example.com"); // Handle any error that occurs. socket.onerror = function(error) { console.log("WebSocket Error: " + error); };關(guān)閉連接
要關(guān)閉連接,客戶機(jī)或服務(wù)器都應(yīng)該發(fā)送包含操作碼0x8的數(shù)據(jù)的控制幀。當(dāng)接收到這樣一個(gè)幀時(shí),另一個(gè)對(duì)等點(diǎn)發(fā)送一個(gè)關(guān)閉幀作為響應(yīng),然后第一個(gè)對(duì)等點(diǎn)關(guān)閉連接,關(guān)閉連接后接收到的任何其他數(shù)據(jù)都將被丟棄:
// Close if the connection is open. if (socket.readyState === WebSocket.OPEN) { socket.close(); }
另外,為了在完成關(guān)閉之后執(zhí)行其他清理,可以將事件偵聽器附加到關(guān)閉事件:
// Do necessary clean up. socket.onclose = function(event) { console.log("Disconnected from WebSocket."); };
服務(wù)器必須監(jiān)聽關(guān)閉事件以便在需要時(shí)處理它:
connection.on("close", function(reasonCode, description) { // The connection is getting closed. });WebSockets和HTTP/2 比較
雖然HTTP/2提供了很多功能,但它并沒有完全滿足對(duì)現(xiàn)有推送/流技術(shù)的需求。
關(guān)于 HTTP/2 的第一個(gè)重要的事情是它并不能替代所有的 HTTP 。verb、狀態(tài)碼和大部分頭信息將保持與目前版本一致。HTTP/2 是意在提升數(shù)據(jù)在線路上傳輸?shù)男省?/p>
比較HTTP/2和WebSocket,可以看到很多相似之處:
正如我們在上面看到的,HTTP/2引入了 Server Push,它使服務(wù)器能夠主動(dòng)地將資源發(fā)送到客戶機(jī)緩存。但是,它不允許將數(shù)據(jù)下推到客戶機(jī)應(yīng)用程序本身,服務(wù)器推送只由瀏覽器處理,不會(huì)在應(yīng)用程序代碼中彈出,這意味著應(yīng)用程序沒有API來獲取這些事件的通知。
這就是服務(wù)器發(fā)送事件(SSE)變得非常有用的地方。SSE 是一種機(jī)制,它允許服務(wù)器在建立客戶機(jī)-服務(wù)器連接之后異步地將數(shù)據(jù)推送到客戶機(jī)。然后,只要有新的“數(shù)據(jù)塊”可用,服務(wù)器就可以決定發(fā)送數(shù)據(jù)。它可以看作是單向發(fā)布-訂閱模式。它還提供了一個(gè)名為 EventSource API 的標(biāo)準(zhǔn)JavaScript,作為W3C HTML5標(biāo)準(zhǔn)的一部分,在大多數(shù)現(xiàn)代瀏覽器中實(shí)現(xiàn)。不支持 EventSource API 的瀏覽器可以輕松地使用 polyfilled 方案來解決。
由于 SSE 基于 HTTP ,因此它與 HTTP/2 非常合適,可以結(jié)合使用以實(shí)現(xiàn)最佳效果:HTTP/2 處理基于多路復(fù)用流的高效傳輸層,SSE 將 API 提供給應(yīng)用以啟用數(shù)據(jù)推送。
為了理解 Streams 和 Multiplexing 是什么,首先看一下`IETF定義:“stream”是在HTTP/2 連接中客戶機(jī)和服務(wù)器之間交換的獨(dú)立的、雙向的幀序列。它的一個(gè)主要特征是,一個(gè)HTTP/2 連接可以包含多個(gè)并發(fā)打開的流,任何一個(gè)端點(diǎn)都可以從多個(gè)流中交錯(cuò)幀。
SSE 是基于 HTTP 的,這說明在 HTTP/2 中,不僅可以將多個(gè) SSE 流交織到單個(gè) TCP 連接上,而且還可以通過多個(gè) SSE 流(服務(wù)器到客戶端的推送)和多個(gè)客戶端請(qǐng)求(客戶端到服務(wù)器)。因?yàn)橛?HTTP/2 和 SSE 的存在,現(xiàn)在有一個(gè)純粹的 HTTP 雙向連接和一個(gè)簡單的 API 就可以讓應(yīng)用程序代碼注冊到服務(wù)器推送服務(wù)上。在比較 SSE 和 WebSocket 時(shí),缺乏雙向能力往往被認(rèn)為是一個(gè)主要的缺陷。有了 HTTP/2,不再有這種情況。這樣就可以跳過 WebSocket ,而堅(jiān)持使用基于 HTTP 的信號(hào)機(jī)制。
如何選擇WebSocket和HTTP/2?WebSockets 會(huì)在 HTTP/2 + SSE 的領(lǐng)域中生存下來,主要是因?yàn)樗且环N已經(jīng)被很好地應(yīng)用的技術(shù),并且在非常具體的使用情況下,它比 HTTP/2 更具優(yōu)勢,因?yàn)樗呀?jīng)被構(gòu)建用于具有較少開銷(如報(bào)頭)的雙向功能。
假設(shè)建立一個(gè)大型多人在線游戲,需要來自連接兩端的大量消息。在這種情況下,WebSockets 的性能會(huì)好很多。
一般情況下,只要需要客戶端和服務(wù)器之間的真正低延遲,接近實(shí)時(shí)的連接,就使用 WebSocket ,這可能需要重新考慮如何構(gòu)建服務(wù)器端應(yīng)用程序,以及將焦點(diǎn)轉(zhuǎn)移到隊(duì)列事件等技術(shù)上。
使用的方案需要顯示實(shí)時(shí)的市場消息,市場數(shù)據(jù),聊天應(yīng)用程序等,依靠 HTTP/2 + SSE 將為你提供高效的雙向通信渠道,同時(shí)獲得留在 HTTP 領(lǐng)域的各種好處:
當(dāng)考慮到與現(xiàn)有 Web 基礎(chǔ)設(shè)施的兼容性時(shí),WebSocket 通常會(huì)變成一個(gè)痛苦的源頭,因?yàn)樗鼘?HTTP 連接升級(jí)到完全不同于 HTTP 的協(xié)議。
規(guī)模和安全性:Web 組件(防火墻,入侵檢測,負(fù)載均衡)是以 HTTP 為基礎(chǔ)構(gòu)建,維護(hù)和配置的,這是大型/關(guān)鍵應(yīng)用程序在彈性,安全性和可伸縮性方面更偏向的環(huán)境。
原文:https://blog.sessionstack.com...
編輯中可能存在的bug沒法實(shí)時(shí)知道,事后為了解決這些bug,花了大量的時(shí)間進(jìn)行l(wèi)og 調(diào)試,這邊順便給大家推薦一個(gè)好用的BUG監(jiān)控工具Fundebug。
老鐵福利:Redux+React+Express+Socket.io構(gòu)建實(shí)時(shí)聊天應(yīng)用教程
你的點(diǎn)贊是我持續(xù)分享好東西的動(dòng)力,歡迎點(diǎn)贊!
歡迎加入前端大家庭,里面會(huì)經(jīng)常分享一些技術(shù)資源。文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/100203.html
摘要:注意值得注意的是,一旦接收到所有幀并且原始消息有效載荷已被重建,客戶端將僅被通知關(guān)于新消息。實(shí)驗(yàn)表明,在之后創(chuàng)建了第二個(gè)幀。以下值目前正在使用中代表繼續(xù)幀。 這一次,我們將深入到通信協(xié)議的世界中,對(duì)比并討論它們的屬性并構(gòu)建部件。我們將提供WebSockets和HTTP / 2的快速比較。 最后,我們分享一些關(guān)于如何選擇網(wǎng)絡(luò)協(xié)議。 概述 如今,擁有豐富動(dòng)態(tài)用戶界面的復(fù)雜網(wǎng)絡(luò)應(yīng)用程序被視為...
摘要:下面我們從前端基礎(chǔ)和底層原理開始講起。對(duì)于和這三個(gè)對(duì)應(yīng)于矢量圖位圖和圖的渲染來說,給前端開發(fā)帶來了重武器,很多小游戲也因此蓬勃發(fā)展。這篇文章受眾之大,后來被人重新整理并發(fā)布為,其中還包括中文版。 showImg(https://segmentfault.com/img/bVbjM5r?w=1142&h=640); 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 這...
摘要:幀協(xié)議讓我們深入了解下幀協(xié)議。目前可用的值該幀接續(xù)前面一幀的有效載荷。該幀包含二進(jìn)制數(shù)據(jù)。幀有以下幾類長度表示有效載荷的長度。數(shù)據(jù)分片有效載荷數(shù)據(jù)可以被分成多個(gè)獨(dú)立的幀。接收端會(huì)緩沖這些幀直到位有值。 原文請(qǐng)查閱這里,略有改動(dòng),本文采用知識(shí)共享署名 3.0 中國大陸許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原...
摘要:為了方便大家共同學(xué)習(xí),整理了之前博客系列的文章,目前已整理是如何工作這個(gè)系列,可以請(qǐng)猛戳博客查看。以下列出該系列目錄,歡迎點(diǎn)個(gè)星星,我將更友動(dòng)力整理理優(yōu)質(zhì)的文章,一起學(xué)習(xí)。 為了方便大家共同學(xué)習(xí),整理了之前博客系列的文章,目前已整理 JavaScript 是如何工作這個(gè)系列,可以請(qǐng)猛戳GitHub博客查看。 以下列出該系列目錄,歡迎點(diǎn)個(gè)星星,我將更友動(dòng)力整理理優(yōu)質(zhì)的文章,一起學(xué)習(xí)。 J...
閱讀 2808·2021-10-08 10:04
閱讀 3198·2021-09-10 11:20
閱讀 521·2019-08-30 10:54
閱讀 3305·2019-08-29 17:25
閱讀 2301·2019-08-29 16:24
閱讀 882·2019-08-29 12:26
閱讀 1445·2019-08-23 18:35
閱讀 1928·2019-08-23 17:53