国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Node.js - 200 多行代碼實現 Websocket 協議

張巨偉 / 749人閱讀

摘要:預備工作序最近正在研究相關的知識,想著如何能自己實現協議。監聽事件就是協議的抽象,直接在上面監聽已有的事件和事件這兩個事件。表示當前數據幀為消息的最后一個數據幀,此時接收方已經收到完整的消息,可以對消息進行處理。

A、預備工作 1、序

最近正在研究 Websocket 相關的知識,想著如何能自己實現 Websocket 協議。到網上搜羅了一番資料后用 Node.js 實現該協議,倒也沒有想象中那么復雜,除去注釋語句和 console 語句后,大約 200 行代碼左右。本文記錄了實現過程中的經驗和總結。

如果你想要寫一個 WebSocket 服務器,首先需要讀懂對應的網絡協議 RFC6455,不過這對于一般人來說有些 “晦澀”,英文且不說,還得咬文嚼字理解 網絡編程 含義。

好在 WebSocket 技術出現比較早,所以可以搜到 RFC6455 中文版,網上也有很多針對該協議的剖析文章,很多文章里還有現成的實現代碼可以參考,所以說實現一個簡單的 Websocket 服務并非難事。

本文更偏向實戰(in action),會從知識儲備、具體代碼分析以及注意事項角度去講解如何用 Node.js 實現一個簡單的 Websocket 服務,至于 Websocket 概念、定義、解釋和用途等基礎知識不會涉及,因為這些知識在本文所列的參考文章中輕松找到。(也可以自行網上隨便一搜,就能找到很多)

2、知識儲備

如果要自己寫一個 Websocket 服務,主要有兩個難點:

熟練掌握 Websocket 的協議,這個需要多讀現有的解讀類文章;(下面會給出參考文章)

操作二進制數據流,在 Node.js 中需要對 Buffer 這個類稍微熟悉些。

同時還需要具備兩個基礎知識點:

網絡編程中使用 大端次序(Big endian)表示大于一字節的數據,稱之為 網絡字節序 (不曉得大小端的,推薦閱讀 什么是大小端?)

了解最高有效位(MSB, Most Significant Bit),不太清楚的,可以參考 LSB最低有效位和MSB最高有效位

具體的做法如下,推薦先閱讀以下幾篇參考文章:

學習WebSocket協議—從頂層到底層的實現原理(修訂版):作者本身自己就用 Node.js 實現過一遍,知識點講解挺透徹的,適合前端同學優先閱讀

WebSocket詳解(一):初步認識WebSocket技術:是一系列的文章,從淺入深,配有豐富的圖文

WebSocket:5分鐘從入門到精通:全文以 Q&A 的方式組織而成,協議的要點都解讀到了,除此之外還很全面, 涉及了WebSocket如何建立連接、交換數據的細節、數據幀的格式以及網絡安全等。

MDN - Writing WebSocket servers:MDN 官方教程,讀一遍沒啥壞處。

然后開始寫代碼,在實現過程中的大部分代碼可以從下面 3 篇文章中找到并借鑒(copy):

nodejs 實現:簡化版本的從這兒借鑒過來的

學習WebSocket協議—從頂層到底層的實現原理(修訂版)

WebSocket協議解析:雖然是 C++ 寫的,但不影響代碼邏輯的理解

閱讀完上面的文章,你會有發現一個共同點,就是在實現 WebSockets 過程中,最最核心的部分就是 解析 或者 生成 Frame(幀),就是下面這結構:

截圖來自規范Base Framing Protocol

想要理解 frame 各個字段的含義,可參考 WebSocket詳解(三):深入WebSocket通信協議細節,文中作者繪制了一副圖來解釋這個 frame 結構;

而在代碼層面,frame 的解析或生成可以在 RocketEngine - parser 或者 _processBuffer 中找到。

在完成上面幾個方面的知識儲備之后,而且大多有現成的代碼,所以自己邊抄邊寫一個 Websocket 服務器并不算太難。

對于 Websocket 初學者,請務必閱讀以上參考文章,對 Websocket 協議有大概的了解之后再繼續本文剩下部分的閱讀,否則很有可能會覺得我寫得云里霧里,不知所云。

B、 實戰

實現代碼放在自己的 demos 倉庫的 micro-ws 的目錄 了,git clone 后本地運行,執行

node index.js

將會在 http://127.0.0.1:3000 創建服務。運行服務之后,打開控制臺就能看到效果:

動圖中瀏覽器 console 所執行的 js 代碼步驟如下:

1.先建立連接

var ws = new WebSocket("ws://127.0.0.1:3000");
ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
};

2.然后發送消息:(注意一定要在建立連接之后再執行該語句,否則發不出消息的)

ws.send("hello world"); 

從效果可見,我們已經實現 Websocket 最基本的通訊功能了。

接下來我們詳細看一下具體實現的細節。

1、調用所寫的 Websocket 類

站在使用者的角度,假設我們已經完成 Websocket 類了,那么應該怎么使用?

客戶端通過 HTTP Upgrade 請求,即 101 Switching Protocol 到 HTTP 服務器,然后由服務器進行協議轉換。

在 Node.js 中我們通過 http.createServer 獲取 http.server 實例,然后監聽 upgrade 事件,在處理這個事件:

// HTTP服務器部分
var server = http.createServer(function(req, res) {
  res.end("websocket test
");
});

// Upgrade請求處理
server.on("upgrade", function(req, socket, upgradeHead){
  // 初始化 ws
  var ws = new WebSocket(req, socket, upgradeHead);
  
  // ... ws 監聽 data、error 的邏輯等

});

這里監聽 upgrade 事件的回調函數中第二個參數 socket 是 net.Socket 實例,這個類是 TCP 或 UNIX Socket 的抽象,同時一個 net.Socket 也是一個 duplex stream,所以它能被讀或寫,并且它也是一個 EventEmitter。

我們就利用這個 socket 對象上進行 Websocket 類實例的初始化工作;

2、構造函數

所以不難理解 Websocket 的構造函數就是下面這個樣子:

class WebSocket extends EventEmitter {
  constructor(req, socket, upgradeHead){
    super(); // 調用 EventEmitter 構造函數
    
    // 1. 構造響應頭 resHeaders 部分
    
    // 2. 監聽 socket 的 data 事件,以及 error 事件
    
    // 3. 初始化成員屬性
    
  }
}

注意,我們需要繼承內置的 EventEmitter ,這樣生成的實例才能監聽、綁定事件;

Node.js 采用事件驅動、異步編程,天生就是為了網絡服務而設計的,繼承 EventEmitter 就能享受到非阻塞模式的 IO 處理;

講一下其中 響應頭的構造事件監聽 部分。

2.1、返回響應頭(Response Header)

根據協議規范,我們能寫出響應頭的內容:

Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

通過 SHA1 計算出摘要,并轉成 base64 字符串。

具體代碼如下:

    var resKey = hashWebSocketKey(req.headers["sec-websocket-key"]);

    // 構造響應頭
    var resHeaders = [
      "HTTP/1.1 101 Switching Protocols",
      "Upgrade: websocket",
      "Connection: Upgrade",
      "Sec-WebSocket-Accept: " + resKey
    ]
      .concat("", "")
      .join("
");
    socket.write(resHeaders);

當執行 socket.write(resHeaders); 到后就和客戶端建立起 WebSocket 連接了,剩下去就是數據的處理。

2.2、監聽事件

socket 就是 TCP 協議的抽象,直接在上面監聽已有的 data 事件和 close 事件這兩個事件。

還有其他事件,比如 errorend 等,詳細參考 net.Socket 文檔
    socket.on("data", data => {
      this.buffer = Buffer.concat([this.buffer, data]);
      while (this._processBuffer()) {} // 循環處理返回的 data 數據
    });

    socket.on("close", had_error => {
      if (!this.closed) {
        this.emit("close", 1006);
        this.closed = true;
      }
    });

close 的事件邏輯比較簡單,比較重要的是 data 的事件監聽部分。核心就是 this._processBuffer() 這個方法,用于處理客戶端傳送過來的數據(即 Frame 數據)
。注意該方法是放在 while 循環語句里,處理好邊界情況,防止死循環。

3、Frame 幀數據的處理

WebSocket 客戶端、服務端通信的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)。
this._processBuffer() 部分代碼邏輯就是用來解析幀數據的,所以它是實現 Websocket 代碼的關鍵;(該方法里面用到了大量的位操作符以及 Buffer 類的操作)

幀數據結構詳細定義可參考 RFC6455 5.2節,上面羅列的參考文章都有詳細的解讀,我在這兒也不啰嗦講細節了,直接看代碼比聽我用文字講要好。

這里就其中兩個細節需要鋪墊一下,方便更好地理解代碼。

3.1、操作碼(Opcode)

Opcode操作代碼,Opcode 的值決定了應該如何解析后續的數據載荷(data payload)

根據 Opcode 我們可以大致將數據幀分成兩大類:數據幀控制幀

數據幀:目前只有 3 種,對應的 opcode 是:

0x0:數據延續幀

0x1:utf-8文本

0x2:二進制數據;

0x3 - 0x7:目前保留,用于后續定義的非控制幀。

控制幀:除了上述 3 種數據幀之外,剩下的都是控制幀

0x8:表示連接斷開

0x9:表示 ping 操作

0xA:表示 pong 操作

0xB - 0xF:目前保留,用于后續定義的控制幀

在代碼里,我們會先從幀數據中提取操作碼:

var opcode = byte1 & 0x0f; //截取第一個字節的后 4 位,即 opcode 碼

然后根據協議獲取到真正的數據載荷(data payload),然后將這兩部分傳給 _handleFrame 方法:

this._handleFrame(opcode, payload); // 處理操作碼

該方法會根據不同的 opcode 做出不同的操作:

_handleFrame(opcode, buffer) {
    var payload;
    switch (opcode) {
      case OPCODES.TEXT:
        payload = buffer.toString("utf8"); //如果是文本需要轉化為utf8的編碼
        this.emit("data", opcode, payload); //Buffer.toString()默認utf8 這里是故意指示的
        break;
      case OPCODES.BINARY: //二進制文件直接交付
        payload = buffer;
        this.emit("data", opcode, payload);
        break;
      case OPCODES.PING: // 發送 pong 做響應
        this._doSend(OPCODES.PONG, buffer);
        break;
      case OPCODES.PONG: //不做處理
        console.log("server receive pong");
        break;
      case OPCODES.CLOSE: // close有很多關閉碼
        let code, reason; // 用于獲取關閉碼和關閉原因
        if (buffer.length >= 2) {
          code = buffer.readUInt16BE(0);
          reason = buffer.toString("utf8", 2);
        }
        this.close(code, reason);
        this.emit("close", code, reason);
        break;
      default:
        this.close(1002, "unhandle opcode:" + opcode);
    }
  }
3.2、分片(Fragment)
規范文檔:5.4 -  Fragmentation

一旦 WebSocket 客戶端、服務端建立連接后,后續的操作都是基于數據幀的傳遞。理論上來說,每個幀(Frame)的大小是沒有限制的。

對于大塊的數據,Websocket 協議建議對數據進行分片(Fragment)操作。

分片的意義主要是兩方面:

主要目的是允許當消息開始但不必緩沖該消息時發送一個未知大小的消息。如果消息不能被分片,那么端點將不得不緩沖整個消息以便在首字節發生之前統計出它的長度。對于分片,服務器或中間件可以選擇一個合適大小的緩沖,當緩沖滿時,再寫一個片段到網絡。

另一方面分片傳輸也能更高效地利用多路復用提高帶寬利用率,一個邏輯通道上的一個大消息獨占輸出通道是不可取的,因此多路復用需要可以分割消息為更小的分段來更好的共享輸出通道。參考文檔 I/O多路復用技術(multiplexing)是什么?

WebSocket 協議提供的分片方法,是將原本一個大的幀拆分成數個小的幀。下面是把一個大的Frame分片的圖示:

分片編號 0 1 ... n-2 n-1
FIN 0 0 ... 0 1
opcode !0 0 0 0 0

由圖可知,第一個分片的 FIN 為 0,Opcode 為非0值(0x1 或 0x2),最后一個分片的FIN為1,Opcode為 0。中間分片的 FINopcode 二者均為 0。

根據 FIN 的值來判斷,是否已經收到消息的最后一個數據幀。

FIN=1 表示當前數據幀為消息的最后一個數據幀,此時接收方已經收到完整的消息,可以對消息進行處理。

FIN=0,則接收方還需要繼續監聽接收其余的數據幀。

opcode在數據交換的場景下,表示的是數據的類型。

0x01 表示文本,永遠是 utf8 編碼的

0x02 表示二進制

0x00 比較特殊,表示 延續幀(continuation frame),顧名思義,就是完整消息對應的數據幀還沒接收完。

代碼里,我們需要檢測 FIN 的值,如果為 0 說明有分片,需要記錄第一個 FIN 為 0 時的 opcode 值,緩存到 this.frameOpcode 屬性中,將載荷緩存到 this.frames 屬性中:

    var FIN = byte1 & 0x80; // 如果為0x80,則標志傳輸結束,獲取高位 bit
    // 如果是 0 的話,說明是延續幀,需要保存好 opCode
    if (!FIN) {
      this.frameOpcode = opcode || this.frameOpcode; // 確保不為 0;
    }
    
    //....
    // 有可能是分幀,需要拼接數據
    this.frames = Buffer.concat([this.frames, payload]); // 保存到 frames 中

當接收到最后一個 FIN 幀的時候,就可以組裝后給 _handleFrame 方法:

    if (FIN) {
      payload = this.frames.slice(0); // 獲取所有拼接完整的數據
      opcode = opcode || this.frameOpcode; // 如果是 0 ,則保持獲取之前保存的 code
      this.frames = Buffer.alloc(0); // 清空 frames
      this.frameOpcode = 0; // 清空 opcode
      this._handleFrame(opcode, payload); // 處理操作碼
    }
3.3、發送數據幀

上面講的都是接收并解析來自客戶端的數據幀,當我們想給客戶端發送數據幀的時候,也得按協議來。

這部分操作相當于是上述 _processBuffer 方法的逆向操作,在代碼里我們使用 encodeMessage 方法(為了簡單起見,我們發送給客戶端的數據沒有經過掩碼處理)將發送的數據分裝成數據幀的格式,然后調用 socket.write 方法發送給客戶端;

  _doSend(opcode, payload) {
    // 1. 考慮數據分片
    this.socket.write(
      encodeMessage(count > 0 ? OPCODES.CONTINUE : opcode, payload)
    ); //編碼后直接通過socket發送

為了考慮分片場景,特意設置 MAX_FRAME_SIZE 來對每次發送的數據長度做截斷做分片:

    // ...
    var len = Buffer.byteLength(payload);
    // 分片的距離邏輯
    var count = 0;
    // 這里可以針對 payload 的長度做分片
    while (len > MAX_FRAME_SIZE) {
      var framePayload = payload.slice(0, MAX_FRAME_SIZE);
      payload = payload.slice(MAX_FRAME_SIZE);
      this.socket.write(
        encodeMessage(
          count > 0 ? OPCODES.CONTINUE : opcode,
          framePayload,
          false
        )
      ); //編碼后直接通過socket發送
      count++;
      len = Buffer.byteLength(payload);
    }
  // ...

至此已經實現 Websocket 協議的關鍵部分,所組裝起來的代碼就能和客戶端建立 Websocket 連接并進行數據交互了。

4、Q&A 4.1、字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 怎么來的?

這個標志性字符串是專門標示 Websocket 協議的 UUID;UUID 是長度為 16-byte(128-bit)的ID,一般以形如f81d4fae-7dec-11d0-a765-00a0c91e6bf6的字符串作為 URN(Uniform Resource Name,統一資源名稱)

UUID 可以移步到 UUID原理 和 RFC 4122 獲取更多知識

為啥選擇這個字符串?

在規范的第七頁已經有明確的說明了:

之所以選用這個 UUID ,主要該 ID 極大不太可能被其他不了解 Websocket 協議的網絡終端所使用;

我也不曉得該怎么翻譯。。。總之就說這個 ID 就相當于 Websocket 協議的 “身份證號” 了。

4.2、Websocket 和 HTTP 什么關系?

HTTP、WebSocket 等應用層協議,都是基于 TCP 協議來傳輸數據的,我們可以把這些高級協議理解成對 TCP 的封裝。

既然大家都使用 TCP 協議,那么大家的連接和斷開,都要遵循 TCP 協議中的三次握手和四次握手 ,只是在連接之后發送的內容不同,或者是斷開的時間不同

對于 WebSocket 來說,它必須依賴 HTTP 協議進行一次握手 ,握手成功后,數據就直接從 TCP 通道傳輸,與 HTTP 無關了

4.3、瀏覽器中 Websocket 會自動分片么?

答案是:看具體瀏覽器的實現

WebSocket是一個 message based 的協議,它可以自動將數據分片,并且自動將分片的數據組裝。
每個 message 可以是一個或多個分片。message 不記錄長度,分片才記錄長度;

根據協議 websocket 協議中幀長度上限為 2^63 byte(為 8388608 TB),可以認為沒有限制,很明顯按協議的最大上限來傳輸數據是不靠譜的。所以在實際使用中 websocket 消息長度限制取決于具體的實現。關于哲方面,找了兩篇參考文章:

Websocket需要像TCP Socket那樣進行邏輯數據包的分包與合包嗎?:WebSocket是一個message-based的協議,它可以自動將數據分片,并且自動將分片的數據組裝;;

websocket長文本問題?:這里給出了長文本 ws 傳輸實踐總結。

在文章 WebSocket探秘 中,作者就做了一個實驗,作者發送 27378 個字節,結果被迫分包了;如果是大數據量,就會被socket自動分包發送。

而經過我本人試驗,發現 Chrome 瀏覽器(版本 68.0.3440.106 - 64bit)會針對 131072(=2^17)bytes 大小進行自動分包。我是通過以下測試代碼驗證:

var ws = new WebSocket("ws://127.0.0.1:3000");
ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
};
var myArray = new ArrayBuffer(131072 * 2 + 1);
ws.send(myArray);

服務端日志:

server detect fragment, sizeof payload: 131072
server detect fragment, sizeof payload: 131072
receive data: 2 262145

客戶端日志:

Received Message: good job

截圖如下:

而以同樣的方式去測試一些自己機器上的瀏覽器:

Firefox(62.0,64bit)

safari (11.1.2 - 13605.3.8)

IE 11

這些客戶端上的 Websocket 幾乎沒有大小的分片(隨著數據量增大,發送會減緩,但并沒有發現分片現象)。

5、總結

從剛開始決定閱讀 Websocket 協議,到自己使用 Node.js 實現一套簡單的 Websocket 協議,到這篇文章的產出,前后耗費大約 1 個月時間(拖延癥。。。)。
感謝文中所提及的參考文獻所給予的幫助,讓我實現過程中事半功倍。

之所以能夠使用較少的代碼實現 Websocket,是因為 Node.js 體系本身了很好的基礎,比如其所提供的 EventEmitter 類自帶事件循環,http 模塊讓你直接使用封裝好的 socket 對象,我們只要按照 Websocket 協議實現 Frame(幀)的解析和組裝即可。

在使用 Node.js 實現一遍 Websocket 協議后,就能較為深刻地理解以下知識點(理解起來一切都是那么自然而然):

Websocket 是一種應用層協議,是為了提供 Web 應用程序和服務端全雙工通信而專門制定的;

WebSocket 和 HTTP 都是基于 TCP 協議實現的

WebSocket和 HTTP 的唯一關聯就是 HTTP 服務器需要發送一個 “Upgrade” 請求,即 101 Switching Protocol 到 HTTP 服務器,然后由服務器進行協議轉換。

WebSocket使用 HTTP 來建立連接,但是定義了一系列新的 header 域,這些域在 HTTP 中并不會使用;

WebSocket 可以和 HTTP Server 共享同一 port

WebSocket 的 數據幀有序

...

本文僅僅是協議的簡單實現,對于 Websocket 的其實還有很多事情可以做(比如支持 命名空間流式 API 等),有興趣的可以參考業界流行的 Websocket 倉庫,去練習鍛造一個健壯的 Websocket 工具庫輪子:

socketio/socket.io:43.5k star,不多說,業界權威龍頭老大。(不過這實際上不是一個 WebSocket 庫,而是一個實時 pub/sub 框架。簡單地說,Socket.IO 只是包含 WebSocket 功能的一個框架,如果要使用該庫作為 server 端的服務,則 client 也必須使用該庫,因為它不是標準的 WebSocket 協議,而是基于 WebSocket 再包裝的消息通信協議)

websockets/ws:9k star,強大易用的 websocket 服務端、客戶端實現,還有提供很多強大的特性

uNetworking/uWebSockets:9.5k star,小巧高性能的 websocket實現,C++ 寫的,想更多了解 Websocket 的底層實現,該庫是不錯的案例。

theturtle32/WebSocket-Node:2.3k star,大部分使用 JavaScript,性能關鍵部分使用 C++ node-gyp 實現的庫。其所列的 測試用例 有挺好的參考價值

本文完。

下面的是我的公眾號二維碼圖片,歡迎關注。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108541.html

相關文章

  • 「跨域」利用node.js實踐前端各種跨域方式(下)

    摘要:技術的學習也是如此唯有實踐才能更清楚的明白原理和加深印象,因此本文會利用對前端的各種跨域方式進行實踐,強烈建議一步一步跟著做,相信你肯定會對跨域有更深層次的理解。 前言 常言道,讀萬卷書,不如行萬里路。技術的學習也是如此,唯有實踐才能更清楚的明白原理和加深印象,因此本文會利用node.js對前端的各種跨域方式進行實踐,強烈建議一步一步跟著做,相信你肯定會對跨域有更深層次的理解。而由于篇...

    Jenny_Tong 評論0 收藏0
  • 【跨域】跨域的簡易實現和測試

    摘要:前言由于自己平時只做做,并沒有遇到太多跨域問題,今天通過幾個樣例模擬實現了幾種跨域方式。 前言 由于自己平時只做做demo,并沒有遇到太多跨域問題,今天通過幾個樣例模擬實現了幾種跨域方式。原文地址 傳送門 本文所有樣例靜態服務器基于nodejs實現,代碼親測可用。測試步驟如下: 1.為了實現跨域訪問的效果,需要下載http-server 作為一個服務器 npm install http...

    ninefive 評論0 收藏0
  • 前端常見跨域解決方案(全)

    摘要:需注意的是由于同源策略的限制,所讀取的為跨域請求接口所在域的,而非當前頁。目前,所有瀏覽器都支持該功能需要使用對象來支持,也已經成為主流的跨域解決方案。反向代理接口跨域跨域原理同源策略是瀏覽器的安全策略,不是協議的一部分。 什么是跨域? 跨域是指一個域下的文檔或腳本試圖去請求另一個域下的資源,這里跨域是廣義的。 廣義的跨域: 1.) 資源跳轉: A鏈接、重定向、表單提交 2.) 資源...

    canger 評論0 收藏0
  • 大話javascript 5期:跨域

    摘要:同源策略所謂同源是指協議,域名,端口均相同。同源策略是瀏覽器的一個安全功能,不同源的客戶端腳本在沒有明確授權的情況下,不能讀寫對方資源。需注意的是由于同源策略的限制,所讀取的為跨域請求接口所在域的,而非當前頁。 一、什么是跨域 1.URL解析 URL (Uniform Resource Locator )統一資源定位符(URL)是用于完整地描述Internet上網頁和其他資源的地址的...

    jzzlee 評論0 收藏0
  • 基于socket.io快速實現一個實時通訊應用

    摘要:實時通訊越來越多應用于各個領域。實現原生實現對象一共支持四個消息和。是基于的實時通信庫。服務器應該用包含相同數據的乓包應答客戶端發送探測幀由服務器發送以響應數據包。主要用于在接收到傳入連接時強制輪詢周期。該間隔可通過配置修改。 隨著web技術的發展,使用場景和需求也越來越復雜,客戶端不再滿足于簡單的請求得到狀態的需求。實時通訊越來越多應用于各個領域。 HTTP是最常用的客戶端與服務端的...

    venmos 評論0 收藏0

發表評論

0條評論

張巨偉

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<