WebRTC的初探之路

image.png

MDN

介紹什麼是WebRTC

WebRTC ( Web Real-Time Communications 網頁即時通訊) 是一項網頁實時通信技術,(以目前所熟知的大多數直播軟件,或是遠程會議,視頻通話類軟件,都是藉助於特定的客戶端作視頻數據的推流工做,客戶端進行拉流播放)WebRTC創建瀏覽器之間點對點(Peer-to-Peer,P2P) 鏈接,實現視頻流和(或)音頻流或者其餘任意數據的傳輸。WebRTC包含的這些標準使用戶在無需安裝任何插件或者第三方的軟件的狀況下,建立點對點(Peer-to-Peer)的數據分享和電話會議成爲可能。而且於 2011年6月1日開源,並在Google、Mozilla、Opera的支持下被歸入萬維網聯盟的W3C推薦標準,它經過簡單的API爲瀏覽器和移動應用程序提供實時通訊(RTC)的功能。javascript

WebRTC目前主要的應用領域以下。

WebRTC項目的原則是API開源、免費、標準化、瀏覽器內置,比現有的技術更高效。WebRTC雖然冠以「Web」之名,但並不受限於傳統互聯網應用或瀏覽器的終端運行環境。實際上,不管終端運行環境是瀏覽器、桌面應用、移動設備(Android或iOS)仍是IoT設備,只要IP鏈接可到達且符合WebRTC規範就能夠互通。這一點釋放了大量智能終端(或運行在智能終端上的App)的實時通訊能力,打開了許多對於實時交互性要求較高的應用場景的想象空間,音視頻會議·在線教育·照相機·音樂播放器·共享遠程桌面·錄製·即時通訊工具·P2P網絡加速·文件傳輸工具·遊戲·實時人臉識別,都是其合適的應用領域,java

WebRTC總體架構

未命名文件 (1).pngWeb應用:Web開發者能夠基於Web API開發基於視頻、音頻的實時通訊應用,如視頻會議、遠程教育、視頻通話、視頻直播、遊戲直播、遠程協做、互動遊戲、實時人臉識別等git

  1. Web API:Web API是面向第三方開發者的WebRTC標準API(JavaScript)經常使用的API以下所示
MediaStream:媒體數據流,如音頻流、視頻流等。
RTCPeerConnection:該類很重要,提供了應用層的調用接口。
RTCDataChannel:傳輸非音視頻數據,如文字、圖片等
複製代碼
  1. C++ API 層 有C++語言編寫,使瀏覽器廠商容易實現WebRTC標準的Web API,抽象地對數字信號過程進行處理。如 RTCPeerConnection API是每一個瀏覽器之間點對點鏈接的核心,RTCPeerConnection是WebRTC組件,用於處理點對點間流數據的穩定和有效通訊。github

  2. Session Management 是一個抽象的會話層,提供會話創建和管理功能。該層協議留給應用開發者自定義實現。對於Web應用,建議使用WebSocket技術來管理信令Session, 信令主要用來轉發會話雙方的媒體信息和網絡信息。也是後端開發者須要關注的一層web

  3. Transport 爲WebRTC的傳輸層,涉及音視頻的數據發送、接收、網絡打洞等內容,能夠經過STUN和ICE組件來創建不一樣類型的網絡間的呼叫鏈接 關注與 P2P 這裏算法

  4. VideoEngine是WebRTC視頻處理引擎, 包含一系列視頻處理的總體框架,從攝像頭採集視頻到視頻信息網絡傳輸再到視頻顯示,是一個完整過程的解決方案後端

    • VP8是視頻圖像編解碼器,也是WebRTC視頻引擎默認的編解碼器。VP8適合實時通訊應用場景,由於它主要是針對低延時而設計的編解碼器。VPx編解碼器是Google收購ON2公司後開源的,如今是WebM項目的一部分,而WebM項目是Google致力於推進的HTML5標準之一。
    • Video Jitter Buffer(視頻抖動緩衝器模塊能夠下降因爲視頻抖動和視頻信息包丟失帶來的不良影響。
    • Image Enhancements(圖像質量加強模塊對網絡攝像頭採集到的視頻圖像進行處理,包括明暗度檢測、顏色加強、降噪處理等功能,用來提高視頻質量
  5. VoiceEngine(音頻引擎)是包含一系列音頻多媒體處理的框架,包括從音頻採集到網絡傳輸端等整個解決方案。VoiceEngine是WebRTC極具價值的技術之一,是Google收購GIPS公司後開源的,目前在VoIP技術上處於業界領先地位瀏覽器

    • iSAC 是針對VoIP和音頻流的寬帶和超寬帶音頻編解碼器 是WebRTC音頻引擎的默認編解碼器
    • iLBC是VoIP音頻流的窄帶語音編解碼器
    • NetEQ For Voice是針對音頻軟件實現的語音信號處理元件 可以有效地處理網絡抖動和語音包丟失時對語音質量產生的影響
    • Acoustic Echo Canceler(AEC,回聲消除器)是一個基於軟件的信號處理元件,能實時地去除麥克風採集到的回聲。
    • Noise Reduction(NR,噪聲抑制)也是一個基於軟件的信號處理元件,用於消除與相關VoIP的某些類型的背景噪聲 如嘶嘶聲、風扇噪音等

WebRTC通話原理

MDNWebRTC安全

通話的大體能夠分爲三個步驟 (假定通話的雙方爲Alice和Bob。雙方要創建起通話,主要步驟以下所示)服務器

one: 媒體協商Alice 與 Bob 經過信令服務器進行媒體協商,如雙方使用的音視頻編碼格式。雙方交換的媒體數據由SDP(Session Description Protocol,會話描述協議)描述

two: 網絡協商Alice 與 Bob 經過STUN服務器獲取到各自的網絡信息,如IP和端口。而後經過信令服務器轉發,互相交換各類網絡信息。這樣雙方就知道對方的IP和端口了,即P2P打洞成功創建直連。這個過程涉及NAT及ICE協議。

three: 創建鏈接Alice 與 Bob 若是沒有創建起直連,則經過TURN中轉服務器轉發音視頻數據,最終完成音視頻通話

一、媒體協商

媒體協商就是雙方在創建鏈接以前 必須告訴對方 用什麼樣的媒體格式 ,瞭解對方支持的媒體格式 才能保證後續正確的編解碼 Alice 端可支持VP八、H264多種編碼格式,而 Bob 端支持VP九、H264 經過媒體協商 取他們的交集H264來編解碼視頻。

描述媒體鏈接內容的協議叫(Session Description Protocol) 簡稱 (SDP) 內容 例如分辨率,格式,編碼,加密算法等。因此在數據傳輸時兩端都可以理解彼此的數據。SDP並非一個真正的協議,而是一種數據格式,用於描述在設備之間共享媒體的鏈接的元數據。 那SDP 信息從哪裏來呢?通常來講,在創建鏈接以前,鏈接雙方須要先經過 RTCPeerConnection API來指定本身要傳輸什麼數據(如Audio、Video、DataChannel) 而後經過 CreateOffer() CreateAnswer() 方法建立 SDP 信息

SDP 信息的交換 要藉助於 信令服務器 能夠用來交換雙方的SDP信息,通常是經過建立Socket鏈接進行交互處理。可使用Node.js、Golang或其餘技術,只要能交換雙方的SDP數據便可。

貼一個SDP數據 -- 就下面這玩意

v=0
o=- 7524998691693353763 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:hwkT
a=ice-pwd:tXV1yDOgQpS9bBHqY5w+/oGf
a=ice-options:trickle
a=fingerprint:sha-256 54:9D:F1:8C:46:89:61:24:FC:B1:5C:F6:6E:BF:18:AF:22:CD:A0:37:37:64:37:61:D6:FF:4F:0D:C2:70:7B:A4
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ 6426930d-bb60-4633-b8b5-bb91d19d8430
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
a=ssrc:5150036 cname:+RCf3A8Ya1BflCDM
a=ssrc:5150036 msid:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ 6426930d-bb60-4633-b8b5-bb91d19d8430
a=ssrc:5150036 mslabel:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ
a=ssrc:5150036 label:6426930d-bb60-4633-b8b5-bb91d19d8430
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:hwkT
a=ice-pwd:tXV1yDOgQpS9bBHqY5w+/oGf
a=ice-options:trickle
a=fingerprint:sha-256 54:9D:F1:8C:46:89:61:24:FC:B1:5C:F6:6E:BF:18:AF:22:CD:A0:37:37:64:37:61:D6:FF:4F:0D:C2:70:7B:A4
a=setup:actpass
a=mid:1
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ 1cfb3b88-4ed0-4267-9c1e-c861e2a323cb
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:124 H264/90000
a=rtcp-fb:124 goog-remb
a=rtcp-fb:124 transport-cc
a=rtcp-fb:124 ccm fir
a=rtcp-fb:124 nack
a=rtcp-fb:124 nack pli
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:119 rtx/90000
a=fmtp:119 apt=124
a=rtpmap:123 H264/90000
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 transport-cc
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:118 rtx/90000
a=fmtp:118 apt=123
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
a=ssrc-group:FID 3517908871 1250619161
a=ssrc:3517908871 cname:+RCf3A8Ya1BflCDM
a=ssrc:3517908871 msid:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ 1cfb3b88-4ed0-4267-9c1e-c861e2a323cb
a=ssrc:3517908871 mslabel:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ
a=ssrc:3517908871 label:1cfb3b88-4ed0-4267-9c1e-c861e2a323cb
a=ssrc:1250619161 cname:+RCf3A8Ya1BflCDM
a=ssrc:1250619161 msid:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ 1cfb3b88-4ed0-4267-9c1e-c861e2a323cb
a=ssrc:1250619161 mslabel:kFj1r3NdWzKanYt530Rbg0QQk8DbMwv2eXuJ
a=ssrc:1250619161 label:1cfb3b88-4ed0-4267-9c1e-c861e2a323cb
複製代碼

二、網絡協商

媒體協商須要通訊雙方彼此要了解對方的網絡狀況 。這樣纔有可能找到一條通訊鏈路。須要作如下兩個處理

  • 一、獲取外網IP地址映射
  • 二、經過信令服務器交換 "網絡信息"

理想的網絡狀況是每一個瀏覽器所在的計算機IP都是公網IP,能夠直接進行點對點鏈接 ,可是 理想很豐滿 現實很骨感 實際狀況是咱們的計算機都是在某個局域網中而且有防火牆,須要進行網絡地址轉換(Network Address Translation,NAT)在解決WebRTC使用過程當中的上述問題時,咱們須要用到NAT、STUN和TURN等概念,下面分別介紹。

NAT

NAT 簡單來講,NAT是爲了解決IPv4下的IP地址匱乏而出現的一種技術。例如,一般咱們處在一個路由器之下 路由器的WAN口有一個公網IP,而全部鏈接路由器LAN口的設備會分配一個私有的的地址一般爲 192.168.1.一、192.168.1.2,若是有n個設備,可能分配到192.168.1.n,而這個IP地址顯然只是一個內網的IP地址,這樣一個路由器的公網地址對應了n個內網的地址,這種使用少許的公有IP地址表明較多的私有IP地址的方式,將有助於減緩IP地址空間的枯竭, NAT技術會保護內網地址的安全性,因此這就會引起一個問題,就是當咱們採用P2P中的鏈接方式時,NAT會阻止外網地址的訪問,這時咱們就得采用NAT穿透技術了

能夠藉助一個公網IP服務器,Alice與Bob都往公網IP-PORT 發包,公網服務器就能夠獲知 Alice 與 Bob 的IP/PORT,又因爲Alice與Bob主動給公網IP服務器發包,因此公網服務器能夠穿透NAT- Alice 與 NAT-Bob,併發送包給Alice與Bob。因此只要公網IP將Bob的IP/PORT 發給Alice,將Alice的IP/PORT發給Bob,這樣下次Alice與Bob互相發送消息時,就不會被NAT阻攔了 , WebRTC的防火牆穿透技術就是基於上述思路來實現的。在WebRTC中採用ICE框架來保證RTCPeerConnection能實現NAT穿透。

ICE

交互式鏈接設施Interactive Connectivity Establishment (ICE) 是一個容許你的瀏覽器和對端瀏覽器創建鏈接的協議框架,ICE經過使用如下幾種技術完成上述工做

STUN (Session Traversal Utilities for NAT)

STUN是一種網絡協議,即簡單的用UDP穿透NAT, 它容許位於 NAT(或多重NAT)後的客戶端 [處於局域網中的計算機] 找出本身的公網地址,查出本身位於哪一種類型的NAT以後以及NAT爲某一個本地端口所綁定的Internet端口。這些信息被用來在兩個同時處於NAT路由器以後的主機之間建立UDP通訊 可是 But 經過STUN服務器取得了公網IP位址,也不必定能創建鏈接。 這是 由於不一樣的NAT類型處理傳入的UDP分組的方式是不一樣的,四種主要類型中有三種可使用STUN穿透:徹底圓錐型NAT受限圓錐型NAT端口受限圓錐型NAT。但大型公司網絡中常常採用的對稱型NAT(又稱爲雙向NAT)則不能使用,這類路由器會透過NAT部署所謂的「Symmetric NAT限制」,也就是說,路由器只會接受你以前連線過的節點所創建的連線,這類網絡就須要用到TURN技術

  • Full Cone NAT (徹底錐形NAT)

徹底錐形NAT,全部從同一個內網IP和端口號發送過來的請求都會被映射成同一個外網IP和端口號,而且任何一個外網主機均可以經過這個映射的外網IP和端口號向這臺內網主機發送包。

  • Restricted Cone NAT(限制錐形NAT)

限制錐形NAT,它也是全部從同一個內網IP和端口號發送過來的請求都會被映射成同一個外網IP和端口號。與徹底錐形不一樣的是,外網主機只可以向先前已經向它發送過數據包的內網主機發送包。

  • Port Restricted Cone NAT (端口限制錐形NAT)

端口限制錐形NAT,與限制錐形NAT很類似,只不過它包括端口號。也就是說,一臺IP地址X和端口P的外網主機想給內網主機發送包,必須是這臺內網主機先前已經給這個IP地址X和端口P發送過數據包。

  • Symmetric NAT

對稱NAT,全部從同一個內網IP和端口號發送到一個特定的目的IP和端口號的請求,都會被映射到同一個IP和端口號。若是同一臺主機使用相同的源地址和端口號發送包,可是發往不一樣的目的地,NAT將會使用不一樣的映射。此外,只有收到數據的外網主機才能夠反過來向內網主機發送包

TURN (Traversal Using Relays around NAT (TURN) 

TURN是指使用中繼穿透NAT ,主要添加了中繼功能。若是終端在進行NAT以後,在特定的情景下有可能使得終端沒法和其餘終端進行直接的通訊,這時就須要將公網的服務器做爲一箇中繼,對來往的數據進行轉發。這個轉發採用的協議就是TURN

STUN服務器和TURN服務器咱們使用coturn開源項目來搭建,地址爲github.com/coturn/cotu…。也可使用以Golang技術開發的服務器來搭建,地址爲github.com/pion/turn

信令服務器

信令服務器不僅是交換SDP和Candidate,還有其餘功能,好比房間管理、用戶列表、用戶進入、用戶退出等IM功能

三、鏈接創建

1)鏈接雙方(Peer)經過第三方服務器來交換(signaling)各自的SDP數據。
2)鏈接雙方經過STUN協議從STUN服務器那裏獲取到本身的NAT結構、子網IP和公網IP、端口,即Candidate信息。
3)鏈接雙方經過第三方服務器來交換各自的Candidate,若是鏈接雙方在同一個NAT下,那它們僅經過內網 Candidate就能創建起鏈接;若是它們處於不一樣NAT下,就須要經過STUN服務器識別出的公網Candidate進行通訊。
4)若是僅經過STUN服務器發現的公網Candidate仍然沒法創建鏈接,這就須要尋求TURN服務器提供的轉發服務,而後將轉發形式的Candidate共享給對方。
5)鏈接雙方向目標IP端口發送報文,經過SDP數據中涉及的密鑰以及指望傳輸的內容創建起加密長鏈接。

image.png

標註的場景是Alice向Bob發起對聊請求
一、Alice 首先建立PeerConnection對象,而後打開本地音視頻設備,將音視頻數據封裝成MediaStream添加到PeerConnection中
二、Alice 調用PeerConnectionCreateOffer方法建立一個用於offer的SDP對象,SDP對象中保存當前音視頻的相關參數。
三、Alice 經過PeerConnectionSetLocalDescription方法將該SDP對象保存起來,並經過信令服務器發送給 Bob
四、Bob接收到Alice發送過的offer SDP 對象,經過PeerConnectionSetRemoteDescription方法將其保存起來、
五、而且Bob調用PeerConnectionCreateAnswer方法建立一個應答的SDP對象,經過PeerConnectionSetLocalDescription的方法保存該應答SDP對象
六、而且Bob須要將建立的應答的SDP對象經過信令服務器發送給Alice
七、Alice 接收到 Bob 發送過來的應答SDP對象,將其經過PeerConnectionSetRemoteDescription方法保存起來
八、在SDP信息的 offer/answer流程中,Alice和Bob已經根據SDP信息建立好相應的音頻l和視頻,而且經過NAT穿透獲取了Candidate數據,Candidate數據 包含了彼此的IP地址信息(本地IP地址、公網IP地址)和端口信息
九、當Alice收集到Candidate信息後,PeerConnection會經過OnIceCandidate接口給Alice發送通知,Alice將收到的Candidate信息經過信令服務器發送給Bob,Bob經過PeerConnectionAddIceCandidate方法保存起來
十、一樣的操做Blice對Alice再來一次。
十一、這樣Alice和Bob就已經創建了音視頻傳輸的P2P通道,Bob接收到Alice傳送過來的音視頻流,會經過PeerConnectionOnAddStream回調接口返回一個標識Alice端音視頻流的MediaStream對象,在Bob端渲染出來便可 -- A、B收到對方的媒體流並播放

相關文章
相關標籤/搜索