Java的IO模型、Netty原理解析
1.什么是IO
雖然作為Java開(kāi)發(fā)程序員,很多都聽(tīng)過(guò)IO、NIO這些,但是很多人都沒(méi)深入去了解這些內(nèi)容。
- Java的I/O是以流的方式進(jìn)行數(shù)據(jù)輸入輸出的,Java的類(lèi)庫(kù)涉及很多領(lǐng)域的IO內(nèi)容:標(biāo)準(zhǔn)的輸入輸出,文件的操作、網(wǎng)絡(luò)上的數(shù)據(jù)傳輸流、字符串流、對(duì)象流等
2.同步與異步、阻塞與非阻塞
- 同步:一個(gè)任務(wù)完成之前不能做其他操作,必須等待。
- 異步:一個(gè)任務(wù)完成之前,可以進(jìn)行其他操作
- 阻塞:相對(duì)于CPU來(lái)說(shuō),掛起當(dāng)前線(xiàn)程,不能做其他操作只能等待
- 非阻塞:CPU無(wú)需掛起當(dāng)前線(xiàn)程,可以執(zhí)行其他操作
3.三種IO模型
BIO(Blocking I/O)
同步并阻塞模式,調(diào)用方在發(fā)起IO操作時(shí)會(huì)被阻塞,直到操作完成才能繼續(xù)執(zhí)行,適用于連接數(shù)較少的場(chǎng)景。
例如:服務(wù)端通過(guò)ServerSocket監(jiān)聽(tīng)端口,accept()阻塞等待客戶(hù)端連接。
優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單
- 缺點(diǎn):線(xiàn)程資源開(kāi)銷(xiāo)大,連接數(shù)多時(shí),每個(gè)線(xiàn)程都要占用CPU資源,容易出現(xiàn)性能瓶頸
適用于低并發(fā)、短連接的場(chǎng)景,如傳統(tǒng)的HTTP服務(wù)
NIO(Non-blocking I/O)
同步非阻塞模型,客戶(hù)端發(fā)送的連接請(qǐng)求都會(huì)注冊(cè)到Selector多路復(fù)用器上,服務(wù)器端通過(guò)Selector管理多個(gè)通道Channel,Selector會(huì)輪詢(xún)這些連接,當(dāng)輪詢(xún)到連接上有IO活動(dòng)就進(jìn)行處理。
NIO基于 Channel 和 Buffer 進(jìn)行操作,數(shù)據(jù)總是從通道讀取到緩沖區(qū)或者從緩沖區(qū)寫(xiě)入到通道。Selector 用于監(jiān)聽(tīng)多個(gè)通道上的事件(比如收到連接請(qǐng)求、數(shù)據(jù)達(dá)到等等),因此使用單個(gè)線(xiàn)程就可以監(jiān)聽(tīng)多個(gè)客戶(hù)端通道。
IO多路復(fù)用:一個(gè)線(xiàn)程可對(duì)應(yīng)多個(gè)連接,不用為每個(gè)連接都創(chuàng)建一個(gè)線(xiàn)程
核心組件:
- Channel:雙向通信通道(如SocketChannel),數(shù)據(jù)可流入流出
- Buffer:數(shù)據(jù)緩沖區(qū),是雙向的,可讀可寫(xiě)
- Selector:一個(gè)Selector對(duì)應(yīng)一個(gè)線(xiàn)程,一個(gè)Selector上可注冊(cè)多個(gè)Channel,并輪詢(xún)多個(gè)Channel的就緒事件
優(yōu)缺點(diǎn):
- 可以減少線(xiàn)程數(shù)量,降低線(xiàn)程切換的開(kāi)銷(xiāo),適用于需要處理大量并發(fā)連接的場(chǎng)景
- 缺點(diǎn):實(shí)現(xiàn)復(fù)雜度高
使用于高并發(fā)、長(zhǎng)連接的場(chǎng)景,如即時(shí)通訊場(chǎng)景
AIO(Asynchronous I/O)
異步非阻塞模型,基于事件回調(diào)或Future機(jī)制
- 調(diào)用方發(fā)起IO請(qǐng)求后,無(wú)需等待操作完成,可繼續(xù)執(zhí)行其他任務(wù)。操作系統(tǒng)在IO操作完成后,通過(guò)回調(diào)或事件通知的方式告知調(diào)用方
- Java中
AsynchronousSocketChannel
是AIO的代表類(lèi),通過(guò)回調(diào)函數(shù)處理讀寫(xiě)操作完成后的結(jié)果
優(yōu)缺點(diǎn):
- IO密集型的應(yīng)用,AIO提供更高的并發(fā)和低延遲,因?yàn)檎{(diào)用方在等待IO時(shí)不會(huì)被阻塞
- 缺點(diǎn):實(shí)現(xiàn)復(fù)雜
適用于高吞吐、低延遲的場(chǎng)景,如日志批量寫(xiě)入
4.什么是Netty
說(shuō)起Java的IO模型,繞不開(kāi)的就是Netty框架了,那什么是Netty,為什么Netty的性能這么高呢?
- Netty是由JBOSS提供的一個(gè)Java開(kāi)源框架。提供異步的、事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用程序框架和工具,用以快速開(kāi)發(fā)高性能、高可靠性的網(wǎng)絡(luò)服務(wù)器
- Netty的原理就是NIO,是基于NIO的完美封裝
很多中間件的底層通信框架用的都是它,比如:RocketMQ、Dubbo、Elasticsearch
4.1 Netty的核心要點(diǎn)
核心特點(diǎn):
- 高并發(fā):通過(guò)多路復(fù)用Selector實(shí)現(xiàn)單線(xiàn)程管理大量連接,減少線(xiàn)程開(kāi)銷(xiāo)
- 傳輸快:零拷貝技術(shù),減少內(nèi)存拷貝次數(shù)
- 封裝性:簡(jiǎn)化NIO的復(fù)雜API,提供鏈?zhǔn)教幚恚–hannelPipeline)和可擴(kuò)展的編解碼能力(如Protobuf支持)
高性能的核心原因:
- 主從Reactor線(xiàn)程模型,無(wú)鎖化設(shè)計(jì),減少線(xiàn)程競(jìng)爭(zhēng)
- 零拷貝技術(shù),堆外內(nèi)存直接操作
- 高效內(nèi)存管理,對(duì)象池技術(shù),預(yù)分配內(nèi)存塊并復(fù)用,對(duì)象復(fù)用機(jī)制
- 基于Selector的I/O多路復(fù)用,異步事件驅(qū)動(dòng)機(jī)制
- Selector空輪詢(xún)問(wèn)題修復(fù)
4.2 零拷貝技術(shù)
Netty的零拷貝體現(xiàn)在操作數(shù)據(jù)時(shí), 不需要將數(shù)據(jù) buffer從 一個(gè)內(nèi)存區(qū)域拷貝到另一個(gè)內(nèi)存區(qū)域。少了一次內(nèi)存的拷貝,CPU 效率就得到的提升。
4.2.1 Linux系統(tǒng)的文件從本地磁盤(pán)發(fā)送到網(wǎng)絡(luò)中的零拷貝技術(shù)
- 內(nèi)核緩沖區(qū)是 Linux 系統(tǒng)的 Page Cahe。為了加快磁盤(pán)的 IO,Linux 系統(tǒng)會(huì)把磁盤(pán)上的數(shù)據(jù)以 Page 為單位緩存在操作系統(tǒng)的內(nèi)存里
- 內(nèi)核緩沖區(qū)到 Socket 緩沖區(qū)之間并沒(méi)有做數(shù)據(jù)的拷貝,只是一個(gè)地址的映射,底層的網(wǎng)卡驅(qū)動(dòng)程序要讀取數(shù)據(jù)并發(fā)送到網(wǎng)絡(luò)上的時(shí)候,看似讀取的是 Socket 的緩沖區(qū)中的數(shù)據(jù),其實(shí)直接讀的是內(nèi)核緩沖區(qū)中的數(shù)據(jù)。
- 零拷貝中所謂的“零”指的是內(nèi)存中數(shù)據(jù)拷貝的次數(shù)為 0
4.2.2 Netty零拷貝技術(shù)
- 使用了堆外內(nèi)存進(jìn)行Socket讀寫(xiě),避免JVM堆內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝
- 提供了CompositeByteBuf合并對(duì)象,可以組合多個(gè)Buffer對(duì)象合并成一個(gè)邏輯上的對(duì)象,用戶(hù)可以像操作一個(gè)Buffer那樣對(duì)組合Buffer進(jìn)行操作,避免傳統(tǒng)內(nèi)存拷貝合并
- 文件傳輸使用FileRegion,封裝FileChannel#transferTo()方法,將文件緩沖區(qū)的內(nèi)容直接傳輸?shù)侥繕?biāo)Channel,避免內(nèi)核緩沖區(qū)和用戶(hù)態(tài)緩沖區(qū)間的數(shù)據(jù)拷貝
4.2.3 Netty和操作系統(tǒng)的零拷貝的區(qū)別?
Netty 的 Zero-copy 完全是在用戶(hù)態(tài)(Java 應(yīng)用層)的, 更多的偏向于優(yōu)化數(shù)據(jù)操作。而在 OS 層面上的 Zero-copy 通常指避免在用戶(hù)態(tài)(User-space)與內(nèi)核態(tài)(Kernel-space)之間來(lái)回拷貝數(shù)據(jù)
4.3 Reactor模式
- 基于IO多路復(fù)用技術(shù),多個(gè)連接共用一個(gè)多路復(fù)用器,程序只需要阻塞等待多路復(fù)用器即可
- 基于線(xiàn)程池技術(shù)復(fù)用線(xiàn)程資源,程序?qū)⑦B接上的任務(wù)分配給線(xiàn)程池中線(xiàn)程處理,不用為每個(gè)連接單獨(dú)創(chuàng)建線(xiàn)程
- Reactor是圖中的ServiceHandler,在一個(gè)單獨(dú)線(xiàn)程中運(yùn)行,負(fù)責(zé)監(jiān)聽(tīng)和分發(fā)事件
Reactor可以分為單Reactor單線(xiàn)程模式、單Reactor多線(xiàn)程模型,主從Reactor多線(xiàn)程模型
4.3.1 單Reactor單線(xiàn)程模式
- Reactor通過(guò)select監(jiān)聽(tīng)客戶(hù)端請(qǐng)求事件,收到事件后通過(guò)dispatch分發(fā)
該模式簡(jiǎn)單,所有操作都由1個(gè)IO線(xiàn)程處理,缺點(diǎn)是存在性能瓶頸,只有1個(gè)線(xiàn)程工作,無(wú)法發(fā)揮多核CPU的性能。
4.3.2 單Reactor多線(xiàn)程模式
- Reactor主線(xiàn)程負(fù)責(zé)接收建立連接事件和后續(xù)的IO處理,Worker線(xiàn)程池處理具體業(yè)務(wù)邏輯
充分發(fā)揮了多核CPU的處理能力,缺點(diǎn)是用一個(gè)線(xiàn)程接收事件和響應(yīng),高并發(fā)時(shí)仍然會(huì)有性能瓶頸
4.3.3 主從Reactor多線(xiàn)程模式
- Reactor主線(xiàn)程負(fù)責(zé)通過(guò)select監(jiān)聽(tīng)連接事件,通過(guò)acceptor處理連接事件
- Reactor從線(xiàn)程負(fù)責(zé)處理建立連接后的IO處理事件
- worker線(xiàn)程池負(fù)責(zé)業(yè)務(wù)邏輯處理,并將結(jié)果返回給Handler
該模式優(yōu)點(diǎn)是主從線(xiàn)程分工明確,能應(yīng)對(duì)更高的并發(fā)。缺點(diǎn)是編程復(fù)雜度較高。
應(yīng)用該模式的中間件有:Dubbo、RocketMQ、Zookeeper等
小結(jié)
Reactor模式的核心在于用一個(gè)或少量線(xiàn)程來(lái)監(jiān)聽(tīng)多個(gè)連接上的事件,根據(jù)事件類(lèi)型分發(fā)調(diào)用相應(yīng)處理邏輯,從而避免為每個(gè)連接都分配一個(gè)線(xiàn)程
4.4 Netty的線(xiàn)程模型
- BossGroup:boss線(xiàn)程組,負(fù)責(zé)接收客戶(hù)端的連接請(qǐng)求,連接來(lái)了之后,將其注冊(cè)到Worker線(xiàn)程組的NioEventLoop中
- WorkerGroup:Worker線(xiàn)程組,每個(gè)線(xiàn)程都是一個(gè)NioEventLoop,負(fù)責(zé)和處理一個(gè)或多個(gè)Channel的I/O讀寫(xiě)操作。處理邏輯通常是通過(guò)ChannelPipeline中的各個(gè)ChannelHandler來(lái)完成
- 業(yè)務(wù)線(xiàn)程組(可選):還可以引入一個(gè)業(yè)務(wù)線(xiàn)程組來(lái)處理業(yè)務(wù)邏輯,避免阻塞Worker線(xiàn)程
簡(jiǎn)單理解:Boss線(xiàn)程是老板,Worker線(xiàn)程是員工,老板負(fù)責(zé)接收處理的事件請(qǐng)求,Worker負(fù)責(zé)工作,處理請(qǐng)求的I/O事件,并交給對(duì)應(yīng)的Handler處理
本質(zhì)是將線(xiàn)程連接和具體的業(yè)務(wù)處理分開(kāi)
5.多路復(fù)用I/O的3種機(jī)制
5.1 select
這三種都是操作系統(tǒng)中的多路復(fù)用I/O機(jī)制
輪詢(xún)機(jī)制:select使用一個(gè)固定大小的位圖來(lái)表示文件描述符集,將文件描述符的狀態(tài)(如可讀、可寫(xiě))存儲(chǔ)在一個(gè)數(shù)組中,調(diào)用select時(shí),每次需將完整的位圖從用戶(hù)空間拷貝到內(nèi)核空間,內(nèi)核遍歷所有描述符,檢查就緒狀態(tài)
局限:
- 文件描述符限制通常為1024,限制了并發(fā)處理數(shù)
- 性能低:搞并發(fā)場(chǎng)景,每次都要遍歷整個(gè)位圖,性能開(kāi)銷(xiāo)大,時(shí)間負(fù)責(zé)度為O(N)
5.2 poll
poll使用了動(dòng)態(tài)數(shù)組來(lái)替代位圖,使用pollfd結(jié)構(gòu)數(shù)組存儲(chǔ)文件描述符和事件,無(wú)數(shù)量限制
工作機(jī)制:每次調(diào)用時(shí)仍然需要遍歷所有描述符,即使只有少量描述符修改了,仍然要檢查整個(gè)數(shù)組,時(shí)間復(fù)雜度為O(N)
5.3 epoll
1)事件驅(qū)動(dòng)模型:epoll使用紅黑樹(shù)來(lái)存儲(chǔ)和管理注冊(cè)的文件描述符,使用就緒事件鏈表來(lái)存儲(chǔ)觸發(fā)的事件。當(dāng)某個(gè)文件描述符上的事件就緒時(shí),epoll會(huì)將該文件描述符添加到就緒鏈表中。
2)觸發(fā)模式:支持水平觸發(fā)(LT)和邊緣觸發(fā)(ET),ET模式下事件僅通知一次
- 水平觸發(fā)(Level Triggered),默認(rèn)模式,只要文件描述符上有未處理的數(shù)據(jù),每次調(diào)用epoll_wait都會(huì)返回該文件描述符
- 邊緣觸發(fā)(Edge Triggered),僅在狀態(tài)發(fā)生變化時(shí)通知一次,減少重復(fù)事件的通知次數(shù)
3)工作流程:
epoll_create
創(chuàng)建實(shí)例:分配相應(yīng)數(shù)據(jù)結(jié)構(gòu),并返回一個(gè)epoll文件描述符。內(nèi)核分配一棵紅黑樹(shù)管理文件描述符,以及一個(gè)就緒事件的鏈表epoll_ctl
注冊(cè)、修改、刪除事件:epoll_ctl是用于管理文件描述符與事件關(guān)系的接口epoll_wait
等待事件:epoll會(huì)檢查就緒事件鏈表,將鏈表中所有就緒的文件描述符返回給用戶(hù)空間。epoll_wait高效體現(xiàn)在它返回的是已經(jīng)發(fā)生事件的文件描述符,而不是遍歷所有注冊(cè)的文件描述符
優(yōu)點(diǎn)是時(shí)間復(fù)雜度O(1),僅處理活躍連接,性能和連接數(shù)無(wú)關(guān)
4)零拷貝機(jī)制:
- 通過(guò)內(nèi)存映射mmap減少了在內(nèi)核和用戶(hù)空間之間的數(shù)據(jù)復(fù)制,進(jìn)一步提高了性能
總結(jié):epoll每次只傳遞發(fā)生的事件,不需要傳遞所有文件描述符,所以提高了效率
6. Netty如何解決JDK NIO空輪詢(xún)bug的?
Java NIO在Linux系統(tǒng)下默認(rèn)是epoll機(jī)制,理論上無(wú)客戶(hù)端連接時(shí)Selector.select()方法是會(huì)阻塞的。
發(fā)生空輪詢(xún)bug表現(xiàn)時(shí),即時(shí)select輪詢(xún)事件返回?cái)?shù)量是0,Select.select()方法也不會(huì)被阻塞,NIO就會(huì)一直處于while死循環(huán)中,不斷向CPU申請(qǐng)資源導(dǎo)致CPU 100%
底層原因:
- Linux內(nèi)核在某些情況下會(huì)錯(cuò)誤地將Selector的EPOLLUP(連接掛起)和EPOLLERR(錯(cuò)誤)事件標(biāo)記為就緒狀態(tài),JDK中的NIO實(shí)現(xiàn)未正確處理這些事件,導(dǎo)致select()方法誤判事件存在而提前返回
6.1 Netty的解決方式
Netty并沒(méi)有解決這個(gè)bug,而是繞開(kāi)了這個(gè)錯(cuò)誤,具體如下:
1)統(tǒng)計(jì)空輪詢(xún)次數(shù):通過(guò)selectCnt計(jì)數(shù)器來(lái)統(tǒng)計(jì)連續(xù)空輪詢(xún)的次數(shù),每次執(zhí)行Selector.select()方法后,如果發(fā)現(xiàn)沒(méi)有IO事件,selectCnt就會(huì)遞增
2)設(shè)置閾值:定義了一個(gè)閾值,默認(rèn)為512,當(dāng)空輪詢(xún)達(dá)到這個(gè)閾值時(shí),Netty就會(huì)觸發(fā)重建Selector的操作
3)重建Selector:Netty新建一個(gè)Selector,并將所有注冊(cè)的Channel從舊的Selector轉(zhuǎn)移到新的Selector上,過(guò)程涉及取消舊Selector上的注冊(cè),以及新Selector上重新注冊(cè)
4)關(guān)閉舊的Selector:重建Selector并將Channel重新注冊(cè)后,Netty關(guān)閉舊的Selector
總結(jié):通過(guò)SelectCnt統(tǒng)計(jì)沒(méi)有IO事件的次數(shù),來(lái)判斷當(dāng)前是否發(fā)生了空輪詢(xún),如果發(fā)生了,就重建一個(gè)Selector來(lái)替換之前出問(wèn)題的Selector
核心代碼如下:
long time = System.nanoTime(); //調(diào)用select方法,阻塞時(shí)間為上面算出的最近一個(gè)將要超時(shí)的定時(shí)任務(wù)時(shí)間 int selectedKeys = selector.select(timeoutMillis); //計(jì)數(shù)器加1 ++selectCnt; if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) { //進(jìn)入這個(gè)分支,表示正常場(chǎng)景 //selectedKeys != 0: selectedKeys個(gè)數(shù)不為0, 有io事件發(fā)生 //oldWakenUp:表示進(jìn)來(lái)時(shí),已經(jīng)有其他地方對(duì)selector進(jìn)行了喚醒操作 //wakenUp.get():也表示selector被喚醒 //hasTasks() || hasScheduledTasks():表示有任務(wù)或定時(shí)任務(wù)要執(zhí)行 //發(fā)生以上幾種情況任一種則直接返回 break; } //此處的邏輯就是: 當(dāng)前時(shí)間 - 循環(huán)開(kāi)始時(shí)間 >= 定時(shí)select的時(shí)間timeoutMillis,說(shuō)明已經(jīng)執(zhí)行過(guò)一次阻塞select(), 有效的select if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) { //進(jìn)入這個(gè)分支,表示超時(shí),屬于正常的場(chǎng)景 //說(shuō)明發(fā)生過(guò)一次阻塞式輪詢(xún), 并且超時(shí) selectCnt = 1; } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) { //進(jìn)入這個(gè)分支,表示沒(méi)有超時(shí),同時(shí) selectedKeys==0 //屬于異常場(chǎng)景 //表示啟用了select bug修復(fù)機(jī)制, //即配置的io.netty.selectorAutoRebuildThreshold //參數(shù)大于3,且上面select方法提前返回次數(shù)已經(jīng)大于 //配置的閾值,則會(huì)觸發(fā)selector重建 //進(jìn)行selector重建 //重建完之后,嘗試調(diào)用非阻塞版本select一次,并直接返回 selector = this.selectRebuildSelector(selectCnt); selectCnt = 1; break; } currentTimeNanos = time;
到此這篇關(guān)于Java的IO模型、Netty原理詳解的文章就介紹到這了,更多相關(guān)Java IO模型、Netty原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java集成開(kāi)發(fā)SpringBoot生成接口文檔示例實(shí)現(xiàn)
這篇文章主要為大家介紹了java集成開(kāi)發(fā)SpringBoot如何生成接口文檔的示例實(shí)現(xiàn)過(guò)程,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10java實(shí)現(xiàn)動(dòng)態(tài)上傳多個(gè)文件并解決文件重名問(wèn)題
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)動(dòng)態(tài)上傳多個(gè)文件,并解決文件重名問(wèn)題的方法,感興趣的小伙伴們可以參考一下2016-03-03一文詳解Java項(xiàng)目中如何優(yōu)雅的使用枚舉類(lèi)型
枚舉類(lèi)型在開(kāi)發(fā)中是很常見(jiàn)的,有非常多的應(yīng)用場(chǎng)景,這篇文章我們就來(lái)學(xué)習(xí)一下項(xiàng)目中如何優(yōu)雅的使用枚舉類(lèi)型,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03Triple協(xié)議支持Java異?;貍髟O(shè)計(jì)實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Triple協(xié)議支持Java異?;貍髟O(shè)計(jì)實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Java中@JSONField和@JsonProperty注解的用法及區(qū)別詳解
@JsonProperty和@JSONField注解都是為了解決obj轉(zhuǎn)json字符串的時(shí)候,將java bean的屬性名替換成目標(biāo)屬性名,下面這篇文章主要給大家介紹了關(guān)于Java中@JSONField和@JsonProperty注解的用法及區(qū)別的相關(guān)資料,需要的朋友可以參考下2024-06-06Java使用POI-TL實(shí)現(xiàn)生成有個(gè)性的簡(jiǎn)歷
POI-TL?是一個(gè)基于?Apache?POI?的?Java?庫(kù),專(zhuān)注于在?Microsoft?Word?文檔(.docx?格式)中進(jìn)行模板填充和動(dòng)態(tài)內(nèi)容生成,下面我們看看如何使用POI-TL生成有個(gè)性的簡(jiǎn)歷吧2024-11-11基于java Files類(lèi)和Paths類(lèi)的用法(詳解)
下面小編就為大家分享一篇基于java Files類(lèi)和Paths類(lèi)的用法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-11-11