QUIC 協(xié)議初探 - iOS 實(shí)踐

本文來(lái)自于騰訊 Bugly 公眾號(hào)(weixinBugly), 作者:emilymmwang,未經(jīng)作者同意,請(qǐng)勿轉(zhuǎn)載,原文地址:https://mp.weixin.qq.com/s/NbewZ1NU49qSjIcdFrpotw

| 導(dǎo)語(yǔ) 本文主要介紹了QUIC協(xié)議,以及初步研究的過(guò)程,用實(shí)踐證明了QUIC協(xié)議在iOS平臺(tái)的可行性

1、QUIC介紹

(1)QUIC(Quick UDP Internet Connections)協(xié)議

是一種全新的基于UDP的web開(kāi)發(fā)協(xié)議??梢杂靡粋€(gè)公式大致概括:

TCP + TLS + HTTP2 = UDP + QUIC + HTTP2’s API

從公式可看出:QUIC協(xié)議雖然是基于UDP,但它不但具有TCP的可靠性、擁塞控制、流量控制等,且在TCP協(xié)議的基礎(chǔ)上做了一些改進(jìn),比如避免了隊(duì)首阻塞;另外,QUIC協(xié)議具有TLS的安全傳輸特性,實(shí)現(xiàn)了TLS的保密功能,同時(shí)又使用更少的RTT建立安全的會(huì)話(huà)。

(2)QUIC協(xié)議的主要目的

是為了整合TCP協(xié)議的可靠性和UDP協(xié)議的速度和效率。

QUIC的維基百科頁(yè)面的介紹:

QUIC是快速UDP網(wǎng)絡(luò)連接(英語(yǔ):Quick UDP Internet Connections)的縮寫(xiě),這是一種實(shí)驗(yàn)性的傳輸層網(wǎng)絡(luò)傳輸協(xié)議,由Google公司開(kāi)發(fā),在2013年實(shí)現(xiàn)。QUIC使用UDP協(xié)議,它在兩個(gè)端點(diǎn)間創(chuàng)建連接,且支持多路復(fù)用連接。在設(shè)計(jì)之初,QUIC希望能夠提供等同于SSL/TLS層級(jí)的網(wǎng)絡(luò)安全保護(hù),減少數(shù)據(jù)傳輸及創(chuàng)建連接時(shí)的延遲時(shí)間,雙向控制帶寬,以避免網(wǎng)絡(luò)擁塞。Google希望使用這個(gè)協(xié)議來(lái)取代TCP協(xié)議,使網(wǎng)頁(yè)傳輸速度加快,計(jì)劃將QUIC提交至互聯(lián)網(wǎng)工程任務(wù)小組(IETF),讓它成為下一代的正式網(wǎng)絡(luò)規(guī)范。

(3)QUIC的特性

1)低延遲連接的建立 (Connection Establishment Latency)

這對(duì)已建立的連接很有好處。

眾所周知,建立一個(gè)TCP連接需要進(jìn)行三次握手,這意味著每次連接都會(huì)產(chǎn)生額外的RTT,從而給每個(gè)連接增加了顯著的延遲(如下圖1所示)。

另外,如果還需要TLS協(xié)商來(lái)創(chuàng)建一個(gè)安全的、加密的https連接,那么就需要更多的RTT,無(wú)疑會(huì)產(chǎn)生更大的延遲(如下圖所示)。


這里寫(xiě)圖片描述

首次,QUIC協(xié)議可以在1個(gè)RTT中啟動(dòng)一個(gè)連接并且獲取完成握手所需的必要信息。

QUIC 1 RTT

如果連接的是一個(gè)新的服務(wù)器,這時(shí)候client是沒(méi)有server的任何信息的,當(dāng)然也不知道用那種密鑰交換算法,沒(méi)有公鑰信息,就不可能實(shí)現(xiàn)0 RTT握手,所以,對(duì)于新的QUIC連接至少需要1 RTT才能完成握手。

在QUIC中,服務(wù)器的配置是完全靜態(tài)的,而且配置是有過(guò)期時(shí)間的,由于服務(wù)器配置是靜態(tài)的,因而不是每個(gè)連接都需要重新進(jìn)行簽名操作,一個(gè)簽名可以適用于多個(gè)連接。

另外,QUIC采用了兩級(jí)密鑰機(jī)制:初始密鑰和會(huì)話(huà)密鑰。QUIC在握手過(guò)程中使用Diffie-Hellman 算法協(xié)商初始密鑰。初始密鑰協(xié)商完畢后,服務(wù)器會(huì)提供一個(gè)臨時(shí)隨機(jī)數(shù),會(huì)馬上再協(xié)商會(huì)話(huà)密鑰,這樣可以保證密鑰的前向安全性,之后可以在通信的過(guò)程中就實(shí)現(xiàn)對(duì)密鑰的更新。接收方意識(shí)到有新的密鑰要更新時(shí),會(huì)嘗試用新舊兩種密鑰對(duì)數(shù)據(jù)進(jìn)行解密,直到成功才會(huì)正式更新密鑰,否則會(huì)一直保留舊密鑰有效。

具體握手過(guò)程如圖(圖片引用daveywu的文章)所示:


這里寫(xiě)圖片描述

QUIC 0 RTT

客戶(hù)端在緩存了ServerConfig的情況下,客戶(hù)端根據(jù)緩存的ServerConifg獲取到密鑰交換算法及公鑰,同時(shí)生成一個(gè)全新的密鑰,直接向服務(wù)器發(fā)送full Client hello消息,開(kāi)始正式握手,消息中包括客戶(hù)端選擇的公開(kāi)數(shù)。服務(wù)器收到full Client hello,不同意回復(fù)REJ;同意連接,則根據(jù)客戶(hù)端的公開(kāi)數(shù)計(jì)算出初始密鑰,回復(fù)SHLO消息。

客戶(hù)端和服務(wù)器根據(jù)臨時(shí)公開(kāi)數(shù)和初始密鑰,各自基于SHA-256算法推導(dǎo)出會(huì)話(huà)密鑰。雙方更換會(huì)話(huà)密鑰通信,初始密鑰已無(wú)用,至此,QUIC握手過(guò)程結(jié)束。

2)改進(jìn)的擁塞控制 (Improved Congestion Control)
QUIC協(xié)議當(dāng)前默認(rèn)使用TCP協(xié)議的Cubic擁塞控制算法。看似QUIC協(xié)議只是吧TCP的擁塞算法重新實(shí)現(xiàn)了一遍,其實(shí)不然。QUIC協(xié)議在TCP擁塞算法基礎(chǔ)上做了些改進(jìn):

1.可插拔

  • 應(yīng)用程序?qū)用婢湍軐?shí)現(xiàn)不同的擁塞控制算法,不需要操作系統(tǒng)或內(nèi)核支持。
  • 單個(gè)應(yīng)用程序的不同連接也能支持配置不同的擁塞控制。
  • 不需要停機(jī)和升級(jí)就能實(shí)現(xiàn)擁塞控制的變更。

2.單調(diào)遞增的Packet Number

  • QUIC并沒(méi)有使用TCP的基于字節(jié)序號(hào)及ACK來(lái)確認(rèn)消息的有序到達(dá),QUIC使用的是Packet Number,每個(gè)Packet Number嚴(yán)格遞增,所以如果Packet N丟失了,重傳Packet N的Packet Number已不是N,而是一個(gè)大于N的值。 這樣就很容易解決TCP的重傳歧義問(wèn)題。

3.更多的ACK塊

  • QUIC ACK幀支持256個(gè)ACK塊,相比TCP的SACK在TCP選項(xiàng)中實(shí)現(xiàn),有長(zhǎng)度限制,最多只支持3個(gè)ACK塊

4.精確計(jì)算RTT時(shí)間

  • QUIC ACK包同時(shí)攜帶了從收到包到回復(fù)ACK的延時(shí),這樣結(jié)合遞增的包序號(hào),能夠精確的計(jì)算RTT。

3)無(wú)隊(duì)頭阻塞的多路復(fù)用 (Multiplexing without head-of-line blocking)

HTTP2的最大特性就是多路復(fù)用,而HTTP2最大的問(wèn)題就是隊(duì)頭阻塞。

首先了解下為什么會(huì)出現(xiàn)隊(duì)頭阻塞。比如HTTP2在一個(gè)TCP連接上同時(shí)發(fā)送3個(gè)stream,其中第2個(gè)stream丟了一個(gè)Packet,TCP為了保證數(shù)據(jù)可靠性,需要發(fā)送端重傳丟失的數(shù)據(jù)包,雖然這時(shí)候第3個(gè)數(shù)據(jù)包已經(jīng)到達(dá)接收端,但被阻塞了。這就是所謂的隊(duì)頭阻塞。

而QUIC多路復(fù)用可以避免這個(gè)問(wèn)題,因?yàn)镼UIC的丟包、流控都是基于stream的,所有stream是相互獨(dú)立的,一條stream上的丟包,不會(huì)影響其他stream的數(shù)據(jù)傳輸。

4)前向糾錯(cuò) (Forward Error Correction)

QUIC使用了FEC(前向糾錯(cuò)碼)來(lái)恢復(fù)數(shù)據(jù),F(xiàn)EC采用簡(jiǎn)單異或的方式,每發(fā)送一組數(shù)據(jù),包括若干個(gè)數(shù)據(jù)包后,并對(duì)這些數(shù)據(jù)包依次做異或運(yùn)算,最后的結(jié)果作為一個(gè)FEC包再發(fā)送出去。接收方收到一組數(shù)據(jù)后,根據(jù)數(shù)據(jù)包和FEC包即可以進(jìn)行校驗(yàn)和糾錯(cuò)。比如:10個(gè)包,編碼后會(huì)增加2個(gè)包,接收端丟失第2和第3個(gè)包,僅靠剩下的10個(gè)包就可以解出丟失的包,不必重新發(fā)送,但這樣也是有代價(jià)的,每個(gè)UDP數(shù)據(jù)包會(huì)包含比實(shí)際需要更多的有效載荷,增加了冗余和CPU編解碼的消耗。

5)連接遷移 (Connection Migration)
TCP的連接是基于4元組的,而QUIC使用64為的Connection ID進(jìn)行唯一識(shí)別客戶(hù)端和服務(wù)器的邏輯連接,這就意味著如果一個(gè)客戶(hù)端改變IP地址或端口號(hào),TCP連接不再有效,而QUIC層的邏輯連接維持不變,仍然采用老的Connection ID。

2、iOS平臺(tái)QUIC協(xié)議的可行性研究

QUIC協(xié)議在web端的應(yīng)用有不少,比如Chromium項(xiàng)目,但移動(dòng)端支持QUIC還比較少。所以在iOS平臺(tái)上,QUIC協(xié)議的可行性還不太確定。

(1)研究Chromium Projects

Chromium項(xiàng)目是開(kāi)源的, The Chromium Projects(http://dev.chromium.org/chromium-projects) 文檔詳細(xì)介紹了Chromium項(xiàng)目的實(shí)現(xiàn)原理,以及如何獲取源碼并進(jìn)行編譯。

獲取源碼之前,需要先安裝depot_tools

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

然后要配置環(huán)境變量

$ export PATH="$PATH:/path/to/depot_tools"

獲取源碼:

$ mkdir chromium && cd chromium

$ fetch ios

$ cd src

獲取源碼是很漫長(zhǎng)的過(guò)程,Chromium項(xiàng)目的源碼有8G,如果你的電腦剩余存儲(chǔ)空間不足10G,基本就可以放棄了。另外獲取源碼必須要翻墻,在公司的staff-wifi下,足足等了5個(gè)小時(shí)才獲取完源碼。

然后就是編譯了,編譯也是需要很漫長(zhǎng)的等待,不過(guò)可能跟機(jī)器的性能有關(guān)吧,反正我是等了1個(gè)多小時(shí)才編譯好……

首先編譯 ios/build/tools/setup-gn.py,編譯完會(huì)在out 目錄下生成幾個(gè)目錄,同時(shí)會(huì)生成一個(gè)Xcode工程。

到這里,你可以選擇用Xcode編譯工程,或者直接用下面的命令行進(jìn)行編譯

$ ninja -C out/Debug-iphonesimulator gn_all

詳細(xì)的過(guò)程請(qǐng)見(jiàn)Checking out and building Chromium for iOS(https://chromium.googlesource.com/chromium/src/+/master/docs/ios/build_instructions.md)

這里其實(shí)走了不少?gòu)澛罚紫仁蔷W(wǎng)絡(luò)問(wèn)題,必須要翻墻,開(kāi)始是選擇公司dev-wifi,但dev-wifi下,命令行配置了代理仍然不能git clone。然后就想著直接從瀏覽器下載,下載是挺快的,用了不到1個(gè)小時(shí),但編譯的時(shí)候提示沒(méi)有.git,還有各種文件也找不到。。??磥?lái)是必須要git clone才行。 無(wú)奈之下,只好選擇用staff-wifi,但staff-wifi的網(wǎng)絡(luò)很不穩(wěn)定,git clone等待了5個(gè)小時(shí)才搞定。

用Xcode打開(kāi)上面生成的Xcode工程文件,可以很清晰地看到Chromium項(xiàng)目目錄結(jié)構(gòu):

這里寫(xiě)圖片描述
  • base:所有項(xiàng)目共享的代碼,比如字符串操作,工具類(lèi)等。
  • build:編譯相關(guān)的文件
  • cc:chromium compositor(合成器)實(shí)現(xiàn)。
  • chrome:Chromium browser相關(guān)代碼
  • content:包含建立 多進(jìn)程瀏覽器 所需要的核心代碼。這里 描述了為什么要把這塊代碼獨(dú)立出來(lái)。
  • net:網(wǎng)絡(luò)庫(kù)
  • sql:對(duì)sqlite的封裝
  • third_party:一系列第三方庫(kù),比如圖片解碼和壓縮庫(kù), chrome/third_party 包含一些專(zhuān)門(mén)給Chrome用的第三方庫(kù)
  • ui/gfx:共享的繪圖類(lèi),基于Chromium的UI繪圖庫(kù)。
  • ui/views:進(jìn)行 UI 開(kāi)發(fā)的簡(jiǎn)單框架,提供了渲染、布局、事件處理機(jī)制。大部分的瀏覽器 UI 都基于這個(gè)框架來(lái)實(shí)現(xiàn)。
  • url:Google的開(kāi)源URL解析和規(guī)范化庫(kù)。

各個(gè)模塊之間的依賴(lài)關(guān)系如圖所示


這里寫(xiě)圖片描述

(2)Stellite庫(kù)

公司內(nèi)部也有一些使用QUIC協(xié)議的應(yīng)用,比如QQ空間黃鉆頁(yè)面和游戲應(yīng)用頁(yè)面PC端,以及騰訊云移動(dòng)直播都已支持QUIC協(xié)議。這也讓我們有繼續(xù)研究下去的信心。

Line利用Cronet,用C++封裝了一層API,實(shí)現(xiàn)了Stellite,并在Github上進(jìn)行了開(kāi)源。開(kāi)源代碼(https://github.com/line/stellite)

事實(shí)上,騰訊云移動(dòng)直播就是在Stellite基礎(chǔ)上對(duì)代碼進(jìn)行剝離,實(shí)現(xiàn)了自己的SDK。既然有先例,不妨就先用Stellite庫(kù)試下,搞起~

首先是編譯client,很簡(jiǎn)單,Stellite提供了編譯腳本

./tools/build.py --target-platform=ios --target stellite_http_client build

這個(gè)編譯也是很漫長(zhǎng)的,因?yàn)樗鼤?huì)把chromium的源碼先clone下來(lái),然后再編譯。一共花了5個(gè)多小時(shí)才編譯出來(lái),比較坑的是,編譯是完全沒(méi)有l(wèi)og打印出來(lái),一度以為是我的電腦卡住了,ctrl+c停止運(yùn)行,居然打印出來(lái)下面這些log?。 薛洹?很明顯,它是在下載chromium源碼,這下就可以放心了,說(shuō)明它是有在運(yùn)行的。


這里寫(xiě)圖片描述

5個(gè)小時(shí)后,終于編譯結(jié)束,但失敗了,出現(xiàn)下面截圖中的錯(cuò)誤。

這里寫(xiě)圖片描述

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.0.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSUUID.h:26:49: error: nullability specifier ‘_Nullable’ cannot be applied to non-pointer type ‘uuid_t’ (aka ‘unsigned char [16]’)

  • (instancetype)initWithUUIDBytes:(const uuid_t _Nullable)bytes;

解決方法是:Xcode的Command Line Tools 選擇Xcode 8.0,猜測(cè)是因?yàn)镾tellite庫(kù)編譯不支持iOS 11模擬器。

改為Xcode 8.0之后,重新編譯,終于在out目錄下看到了期盼已久的libstellite_http_client.a 庫(kù)。_

這里寫(xiě)圖片描述

(3)Cronet庫(kù)

Google Chrome提供了一個(gè)網(wǎng)絡(luò)模塊Cronet SDK,封裝了Chromium net,提供了Java接口和OC接口。業(yè)界也有直接使用Cronet的案例,比如蘑菇街(http://www.infoq.com/cn/articles/mogujie-app-chromium-network-layer

Andorid編譯Cronet庫(kù)是很方便的,而且Google有專(zhuān)門(mén)提供文檔,Checking out and building Cronet for
Android(https://chromium.googlesource.com/chromium/src/+/master/components/cronet/android/build_instructions.md)

相對(duì)來(lái)說(shuō),iOS編譯就比較麻煩了。

首先要將cr_cronet.py link到你的當(dāng)前目錄下,比如src目錄下。這樣用起來(lái)會(huì)比較方便,當(dāng)然你也可以忽略這一步,每次都用cr_cronet.py的完整路徑。。。

~/chromium/src $ ln -s components/cronet/tools/cr_cronet.py somewhere/in/your/path

然后創(chuàng)建編譯文件夾:

~/chromium/src $ python cr_cronet.py gn

之后就可以開(kāi)始編譯了

~/chromium/src $ cr_cronet.py build -d out/Debug-iphonesimulator

如果想deploy到真機(jī),可以用下面的命令行

~/chromium/src $ python cr_cronet.py gn -i

- ~/chromium/src $ python cr_cronet.py build -i -d out/Debug-iphoneos

如果你沒(méi)有安裝最新的JDK,編譯的時(shí)候會(huì)一直提醒你進(jìn)行安裝,所以最好是確保已安裝了最新的JAVA JDK和JRE。

編譯成功后,就可以在out目錄下看到生成的framework,可以直接在Xcode里面打開(kāi)工程。


這里寫(xiě)圖片描述

3、QUIC協(xié)議實(shí)踐

因?yàn)镾tellite 編譯比較簡(jiǎn)單,這里我是直接采用Stellite庫(kù),將Chromium net移植到iOS,測(cè)試QUIC協(xié)議的。

Stellite提供了一些很方便的
API(https://github.com/line/stellite/blob/master/CLIENT_GUIDE.md),但Stellite是C++寫(xiě)的,因?yàn)楹芫脹](méi)寫(xiě)C++了,順便惡補(bǔ)了下語(yǔ)法,哈哈哈哈。。。

Xcode中引入libstellite_http_client.a庫(kù),這個(gè)不贅述了,相信大家都會(huì)。

為了測(cè)試QUIC,以及對(duì)比QUIC和HTTP2的性能,我寫(xiě)了個(gè)初步的Demo,Demo二維碼:

這里寫(xiě)圖片描述

附件中有具體的代碼,有興趣可以看下,或者直接git clone http://git.code.oa.com/emilymmwang/QuicTest.git 查看demo代碼

Demo中使用Stellite庫(kù)提供的API請(qǐng)求url,代碼如下:


- (void)requestUrl:(NSString*)url useQuic:(BOOL)useQuic
{
    if (url.length == 0) {
        return;
    }

    // 設(shè)置header
    stellite::HttpRequestHeader *header = new stellite::HttpRequestHeader;
    header->SetHeader("Q-UA","V1_IPH_SQ_7.3.0_0_HDBM_T");
    stellite::HttpRequest *request = new stellite::HttpRequest;
    request->url = [url UTF8String];
    request->request_type = stellite::HttpRequest::GET;

    // 設(shè)置params
    stellite::HttpClientContext::Params *stParams = new stellite::HttpClientContext::Params;
    if (useQuic) {
        stParams->using_quic = true;
        stParams->using_disk_cache = true;
        std::vector<std::string> strings;
        strings.push_back("https://stellite.io:443");
        stParams->origins_to_force_quic_on = strings;
    } else {
        stParams->using_http2 = true;
        stParams->using_disk_cache = true;
    }

    // 初始化context
    stellite::HttpClientContext *context = new stellite::HttpClientContext(*stParams);
    context->Initialize();

    downloadDuration = CFAbsoluteTimeGetCurrent();
    // 開(kāi)始請(qǐng)求
    MyHttpResponseDelegate *delegate = new MyHttpResponseDelegate;
    stellite::HttpClient *client = context->CreateHttpClient(delegate);
    client->Request(*request);
}

useQuic 為YES表示用QUIC協(xié)議,NO表示用http2協(xié)議

MyHttpResponseDelegate 代碼:

class MyHttpResponseDelegate:public stellite::HttpResponseDelegate
{
public:
    void OnHttpResponse(int request_id, const stellite::HttpResponse& response,
                        const char* body, size_t body_len) {
        if (response.response_code == 200) { // 成功
            downloadDuration = CFAbsoluteTimeGetCurrent() - downloadDuration;
            NSData *data = [NSData dataWithBytes:body length:body_len];
            BOOL useQuic = (response.connection_info == stellite::HttpResponse::CONNECTION_INFO_QUIC1_SDPY3);
            [[libTest instance] saveImage:[UIImage imageWithData:data] downloadDuration:downloadDuration useQuic:useQuic];
            NSLog(@"OnHttpResponse success downloadDuration=%lf data:%s connect_info=%zd",downloadDuration, body, response.connection_info);
        }
    }
    void OnHttpStream(int request_id, const stellite::HttpResponse& response,
                      const char* stream, size_t stream_len,
                      bool is_last){
    }
    // The error code are defined at net/base/net_error_list.h
    void OnHttpError(int request_id, int error_code,
                     const std::string& error_message) {
    }

    virtual void OnHttpHeader(int request_id, const stellite::HttpResponse& response) {
        NSLog(@"OnHttpHeader downloadDuration=%lf", CFAbsoluteTimeGetCurrent() - downloadDuration);
    }
};

為了確保確實(shí)是使用的QUIC協(xié)議,特地抓包看了下:


這里寫(xiě)圖片描述

最終,引入libstellite_http_client.a庫(kù),安裝包增加了3M左右。有經(jīng)驗(yàn)表明可以對(duì)Chromium源代碼進(jìn)行剝離,減少安裝包大小,這個(gè)還待研究

4、QUIC協(xié)議和Http2對(duì)比數(shù)據(jù)

測(cè)試請(qǐng)求圖片url:
https://vip.qzone.qq.com/proxy/domain/qzonestyle.gtimg.cn/qzone/space_item/boss_pic/2472_2017_11/1512034326193_704231.jpg

感謝yippeehuang 提供的圖片,因?yàn)镼Q空間游戲應(yīng)用頁(yè)面現(xiàn)在用的是QUIC協(xié)議,所以該測(cè)試數(shù)據(jù)直接是連接的他們的服務(wù)器。

我用 QUIC 和 HTTP2 分別在 wifi網(wǎng)絡(luò) 和 4G網(wǎng)絡(luò) 請(qǐng)求上面的圖片(圖片大?。?3K),wifi和4G下分別做了10組測(cè)試,具體的下載總耗時(shí)(單位:ms)對(duì)比數(shù)據(jù)如下:

wifi下:


這里寫(xiě)圖片描述

4G網(wǎng)絡(luò)下:


這里寫(xiě)圖片描述

從表格可以看出,wifi網(wǎng)絡(luò)和4G網(wǎng)絡(luò)下,QUIC協(xié)議下載的總耗時(shí)比Http2要小,相對(duì)于Http2,wifi下,QUIC在下載總耗時(shí)上提升了14%左右,4G下提升18%左右。當(dāng)然,這只是針對(duì)一張圖片進(jìn)行的測(cè)試,可能不具有代表性,但可以大致看出QUIC在下載耗時(shí)方面還是有所提升的。

目前只是對(duì)QUIC進(jìn)行初步研究,后續(xù)將會(huì)繼續(xù)熟悉Chromium源代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容