Java BIO & Java NIO

Java BIO & Java NIO

在JDK1.4之前,Java的所有socket通信都是基於同步阻塞(BIO)模式,BIO模式在性能和可靠性上存在這巨大的瓶頸,所以在JDK1.4推出了NIO類庫,支持非阻塞IO。

1、BIO

Java BIO通信基於socket + io,其中serverScoket.accept()是一個阻塞方法,會一直等到有客戶端socket連接進入纔會返回。

Client代碼:

package com.xiaohuihui.net.bio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * @Desription:
 * @Author: yangchenhui
 *
 */
public class BioClient {
    private static Charset charset = Charset.forName("UTF-8");

    public static void main(String[] args) throws IOException {
        Socket s = new Socket("localhost", 8080);
        OutputStream out = s.getOutputStream();

        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入:");
        String msg = scanner.nextLine();
        // 阻塞,寫完成
        out.write(msg.getBytes(charset));
        scanner.close();
        s.close();
    }
}

 

Server代碼:

package com.xiaohuihui.net.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @Desription:
 * @Author: yangchenhui
 *
 */
public class BioServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("服務器啓動成功");
        while (!serverSocket.isClosed()) {
            //會阻塞直到有連接連接進入
            Socket request = serverSocket.accept();
            String hostAddress = request.getInetAddress().getHostAddress();
            System.out.println("hostAddress:" + hostAddress);
            System.out.println("收到新連接:" + request.toString());
            try {
                InputStream inputStream = request.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
                String msg;
                while ((msg = bufferedReader.readLine()) != null) {
                    //有數據讀取進來
                    if (msg.length() == 0) {
                        break;
                    }
                    System.out.println(msg);
                }
                System.out.println("收到數據,來自" + request.toString());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    request.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

如果同時有多個socket通信時,第一個連接上的socket連接沒有發送內容,那麼後面進入的連接會一直連不上。

這種BIO方式顯然不能夠滿足高併發的應用服務器開發,雖然可以通過使用一個線程(Acceptor)接收socket連接,多線程處理業務的方式增加socket的處理數量,但是線程作爲操作系統的寶貴資源,大量線程空閒或掛起,會消耗大量系統資源。

還有一種改進的方式,使用線程池 + 任務隊列的方式可以實現僞異步IO(詳見《Netty權威指南》),此時可以通過限制隊列的大小和線程池的最大線程數來保證系統的資源不被耗盡。

2、NIO

NIO又稱New IO,非阻塞IO(Non-block IO),對應BIO中ServerSocket,Socket分別對應的爲ServerSocketChannel,SocketChannel。在NIO中,引入了三個不同於BIO中的概念,下面簡要的介紹下。

2.1、 緩衝區Buffer

Stream只能實現單一功能,分別爲InputStream,OutputStream,但是 Buffer不同,這是一個緩衝區對象,既可以進行讀操作,也可以進行寫操作。

Buffer下有不同的子類,對應操作不同的數據類型,最常用的還是ByteBuffer,同時ByteBuffer提供兩種內存申請模式,一種是jvm內存,一種堆外內存。

1:JVM內存:ByteBuffer.allocate(4);
2:堆外內存:ByteBuffer.allocateDirect(4);

由於JVM的GC,GC之後有可能數據在JVM中的地址就已經發生了變化,所以每次進行網絡IO或者文件IO的時候,都會先把數據從JVM中複製一份到堆外內存中。所以使用堆外內存可以少一次拷貝。

同時堆外內存不受JVM管理,減少了GC的壓力,但是實現了自動管理。Cleaner在被GC之前會執行clean方法,進行堆外內存回收。

堆外內存使用建議:

1、分配給大型、長壽命的場景使用,例如:文件讀寫,網絡傳輸;

2、通過使用JVM參數(MaxDirectMemorySize)控制堆外內存分配,避免整個機器資源被耗盡。

package com.xiaohuihui.net.nio;

import java.nio.ByteBuffer;

/**
 * @Desription:
 * @Author: yangchenhui
 */
public class BufferDemo {

    public static void main(String[] args) {

        // 構建一個byte字節緩衝區,容量是4
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4);
        // 默認寫入模式,查看三個重要的指標
        System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));
        // 寫入2字節的數據
        byteBuffer.put((byte) 1);
        byteBuffer.put((byte) 2);
        byteBuffer.put((byte) 3);
        // 再看數據
        System.out.println(String.format("寫入3字節後,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // 轉換爲讀取模式(不調用flip方法,也是可以讀取數據的,但是position記錄讀取的位置不對)
        System.out.println("#######開始讀取");
        byteBuffer.flip();
        byte a = byteBuffer.get();
        System.out.println(a);
        byte b = byteBuffer.get();
        System.out.println(b);
        System.out.println(String.format("讀取2字節數據後,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // 繼續寫入3字節,此時讀模式下,limit=3,position=2.繼續寫入只能覆蓋寫入一條數據
        // clear()方法清除整個緩衝區。compact()方法僅清除已閱讀的數據。轉爲寫入模式
        byteBuffer.compact(); // buffer : 1 , 3
        byteBuffer.put((byte) 3);
        byteBuffer.put((byte) 4);
        byteBuffer.put((byte) 5);
        System.out.println(String.format("最終的情況,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // rewind() 重置position爲0
        // mark() 標記position的位置
        // reset() 重置position爲上次mark()標記的位置

    }

}

2.2、通道Channel

Channel是雙向的通道,可以通過Channel讀取或者寫入數據。Channel是全雙工的,在UNIX網絡編程模型中,底層操作系統的通道也是全雙工的,所以相對與流操作而言,Channel能夠更好的映射操作系統底層API。

2.3、多路複用器Selector

Selector提供選擇已經就緒的任務的能力,首先Channel可以註冊到Selector上,註冊的時候可以選擇對應感興趣的事件,從源碼中看主要有以下4種:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

當Channel上有對應的tcp連接事件時,Channel就會處於就緒狀態,然後可以使用Selector對應的API可以獲取到就緒Channel的集合,進行IO操作。

基於Selector的這種特性,可以實現一個線程管理多個網絡連接,解決了BIO中最大的短板。同時JDK底層通過epoll()代替傳統的select操作,所以沒有最大句柄數的限制(1024/2048),這就意味着理論上除了操作系統的保留端口,一個Selector可以管理剩餘所有操作系統端口的連接。

2.4、NIO示例

NIO Client:

package com.xiaohuihui.net.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * @Desription:
 * @Author: yangchenhui
 *
 */
public class NioClient {

    public static void main(String[] args) throws IOException {

        SocketChannel socketChannel = SocketChannel.open();
        // 設置爲非阻塞
        socketChannel.configureBlocking(false);
        // 連接服務器
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        while (!socketChannel.finishConnect()) {
            // 沒連接上,則一直等待
            Thread.yield();
        }
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入:");
        // 發送內容
        String msg = scanner.nextLine();
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        while (buffer.hasRemaining()) {
            socketChannel.write(buffer);
        }
        // 讀取響應
        System.out.println("收到服務端響應:");
        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

        while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
            // 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
            if (requestBuffer.position() > 0) {
                break;
            }
        }
        requestBuffer.flip();
        byte[] content = new byte[requestBuffer.limit()];
        requestBuffer.get(content);
        System.out.println(new String(content));
        scanner.close();
        socketChannel.close();


    }

}

NIO Server:

一共有三個版本:

package com.xiaohuihui.net.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * @Desription: 用到了非阻塞的API, 再設計上,和BIO可以有很大的不同
 * 問題: 輪詢通道的方式,低效,浪費CPU
 * @Author: yangchenhui
 * @Date: 2019/4/22 19:49
 */
public class NioServer1 {

    /**
     * 已經建立連接的集合
     */
    private static ArrayList<SocketChannel> channels = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        // 創建網絡服務端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 設置爲非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 綁定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        System.out.println("啓動成功");
        while (true) {
            // 獲取新tcp連接通道
            SocketChannel socketChannel = serverSocketChannel.accept();
            // tcp請求 讀取/響應
            if (socketChannel != null) {
                System.out.println("收到新連接 : " + socketChannel.getRemoteAddress());
                // 默認是阻塞的,一定要設置爲非阻塞
                socketChannel.configureBlocking(false);
                channels.add(socketChannel);
            } else {
                // 沒有新連接的情況下,就去處理現有連接的數據,處理完的就刪除掉
                Iterator<SocketChannel> iterator = channels.iterator();
                while (iterator.hasNext()) {
                    SocketChannel ch = iterator.next();
                    try {
                        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

                        if (ch.read(requestBuffer) == 0) {
                            // 等於0,代表這個通道沒有數據需要處理,那就待會再處理
                            continue;
                        }
                        while (ch.isOpen() && ch.read(requestBuffer) != -1) {
                            // 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
                            if (requestBuffer.position() > 0) {
                                break;
                            }
                        }
                        // 如果沒數據了, 則不繼續後面的處理
                        if (requestBuffer.position() == 0) {
                            continue;
                        }
                        requestBuffer.flip();
                        byte[] content = new byte[requestBuffer.limit()];
                        requestBuffer.get(content);
                        System.out.println(new String(content));
                        System.out.println("收到數據,來自:" + ch.getRemoteAddress());

                        // 響應結果 200
                        String response = "HTTP/1.1 200 OK\r\n" +
                                "Content-Length: 11\r\n\r\n" +
                                "Hello World";
                        ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                        while (buffer.hasRemaining()) {
                            ch.write(buffer);
                        }
                        iterator.remove();
                    } catch (IOException e) {
                        e.printStackTrace();
                        iterator.remove();
                    }
                }
            }
        }

    }

}
package com.xiaohuihui.net.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @Desription: 結合Selector實現的非阻塞服務端(放棄對channel的輪詢, 藉助消息通知機制)
 * @Author: yangchenhui
 * @Date: 2019/4/22 20:24
 *
 */
public class NioServer2 {

    public static void main(String[] args) throws IOException {
        // 1. 創建網絡服務端ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 設置爲非阻塞模式
        serverSocketChannel.configureBlocking(false);

        // 2. 構建一個Selector選擇器,並且將channel註冊上去
        Selector selector = Selector.open();
        // 將serverSocketChannel註冊到selector
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
        // 對serverSocketChannel上面的accept事件感興趣(serverSocketChannel只能支持accept操作)
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        // 3. 綁定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));

        System.out.println("啓動成功");

        while (true) {
            // 不再輪詢通道,改用下面輪詢事件的方式.select方法有阻塞效果,直到有事件通知纔會有返回
            selector.select();
            // 獲取事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍歷查詢結果
            Iterator<SelectionKey> iter = selectionKeys.iterator();
            while (iter.hasNext()) {
                // 被封裝的查詢結果
                SelectionKey key = iter.next();
                iter.remove();
                // 關注 Read 和 Accept兩個事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.attachment();
                    // 將拿到的客戶端連接通道,註冊到selector上面
                    // mainReactor 輪詢accept
                    SocketChannel clientSocketChannel = server.accept();
                    clientSocketChannel.configureBlocking(false);
                    clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel);
                    System.out.println("收到新連接 : " + clientSocketChannel.getRemoteAddress());
                }

                if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.attachment();
                    try {
                        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
                        while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
                            // 長連接情況下 ,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
                            if (requestBuffer.position() > 0) {
                                break;
                            }
                        }
                        // 如果沒數據了, 則不繼續後面的處理
                        if (requestBuffer.position() == 0) continue;
                        requestBuffer.flip();
                        byte[] content = new byte[requestBuffer.limit()];
                        requestBuffer.get(content);
                        System.out.println(new String(content));
                        System.out.println("收到數據,來自:" + socketChannel.getRemoteAddress());
                        // 響應結果 200
                        String response = "HTTP/1.1 200 OK\r\n" +
                                "Content-Length: 11\r\n\r\n" +
                                "Hello World";
                        ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                        while (buffer.hasRemaining()) {
                            socketChannel.write(buffer);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        key.cancel(); // 取消事件訂閱
                    }
                }
            }
            selector.selectNow();
        }
        // 問題: 此處一個selector監聽所有事件,一個線程處理所有請求事件. 會成爲瓶頸! 要有多線程的運用
    }

}

 2.5、Reactor線程模型

首先由道格·李在《Scalable IO in Java》中提出(http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf),感興趣的朋友可以百度下相關的介紹,最主要的思想就是針對不同的業務壓力,分配不同大小的線程池處理。

package com.xiaohuihui.net.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Desription: NIO selector 多路複用reactor線程模型
 * @Author: yangchenhui
 * @Date: 2019/10/24 22:23
 */
public class NioServer3 {

    /**
     * 處理業務操作的線程
     */
    private static ExecutorService workPool = Executors.newCachedThreadPool();

    /**
     * 封裝了selector.select()等事件輪詢的代碼
     */
    abstract class ReactorThread extends Thread {

        Selector selector;
        LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();

        /**
         * Selector監聽到有事件後,調用這個方法
         */
        public abstract void handler(SelectableChannel channel) throws Exception;

        private ReactorThread() throws IOException {
            selector = Selector.open();
        }

        volatile boolean running = false;

        @Override
        public void run() {
            // 輪詢Selector事件
            while (running) {
                try {
                    // 執行隊列中的任務
                    Runnable task;
                    while ((task = taskQueue.poll()) != null) {
                        task.run();
                    }
                    selector.select(1000);

                    // 獲取查詢結果
                    Set<SelectionKey> selected = selector.selectedKeys();
                    // 遍歷查詢結果
                    Iterator<SelectionKey> iter = selected.iterator();
                    while (iter.hasNext()) {
                        // 被封裝的查詢結果
                        SelectionKey key = iter.next();
                        iter.remove();
                        int readyOps = key.readyOps();
                        // 關注 Read 和 Accept兩個事件
                        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                            try {
                                SelectableChannel channel = (SelectableChannel) key.attachment();
                                channel.configureBlocking(false);
                                handler(channel);
                                if (!channel.isOpen()) {
                                    key.cancel(); // 如果關閉了,就取消這個KEY的訂閱
                                }
                            } catch (Exception ex) {
                                key.cancel(); // 如果有異常,就取消這個KEY的訂閱
                            }
                        }
                    }
                    selector.selectNow();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        private SelectionKey register(SelectableChannel channel) throws Exception {
            // 爲什麼register要以任務提交的形式,讓reactor線程去處理?
            // 因爲線程在執行channel註冊到selector的過程中,會和調用selector.select()方法的線程爭用同一把鎖
            // 而select()方法實在eventLoop中通過while循環調用的,爭搶的可能性很高,爲了讓register能更快的執行,就放到同一個線程來處理
            FutureTask<SelectionKey> futureTask = new FutureTask<>(() -> channel.register(selector, 0, channel));
            taskQueue.add(futureTask);
            return futureTask.get();
        }

        private void doStart() {
            if (!running) {
                running = true;
                start();
            }
        }
    }

    private ServerSocketChannel serverSocketChannel;
    /**
     * 1、創建多個線程 - accept處理reactor線程 (accept線程)
     */
    private ReactorThread[] mainReactorThreads = new ReactorThread[1];
    /**
     * 2、創建多個線程 - io處理reactor線程  (I/O線程)
     */
    private ReactorThread[] subReactorThreads = new ReactorThread[8];

    /**
     * 初始化線程組
     */
    private void newGroup() throws IOException {
        // 創建IO線程,負責處理客戶端連接以後socketChannel的IO讀寫
        for (int i = 0; i < subReactorThreads.length; i++) {
            subReactorThreads[i] = new ReactorThread() {
                @Override
                public void handler(SelectableChannel channel) throws IOException {
                    // work線程只負責處理IO處理,不處理accept事件
                    SocketChannel ch = (SocketChannel) channel;
                    ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
                    while (ch.isOpen() && ch.read(requestBuffer) != -1) {
                        // 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
                        if (requestBuffer.position() > 0) {
                            break;
                        }
                    }
                    if (requestBuffer.position() == 0) {
                        return;
                    }// 如果沒數據了, 則不繼續後面的處理
                    requestBuffer.flip();
                    byte[] content = new byte[requestBuffer.limit()];
                    requestBuffer.get(content);
                    System.out.println(new String(content));
                    System.out.println(Thread.currentThread().getName() + "收到數據,來自:" + ch.getRemoteAddress());

                    // TODO 業務操作 數據庫、接口...
                    workPool.submit(() -> {
                    });

                    // 響應結果 200
                    String response = "HTTP/1.1 200 OK\r\n" +
                            "Content-Length: 11\r\n\r\n" +
                            "Hello World";
                    ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                    while (buffer.hasRemaining()) {
                        ch.write(buffer);
                    }
                }
            };
        }

        // 創建mainReactor線程, 只負責處理serverSocketChannel
        for (int i = 0; i < mainReactorThreads.length; i++) {
            mainReactorThreads[i] = new ReactorThread() {
                AtomicInteger incr = new AtomicInteger(0);

                @Override
                public void handler(SelectableChannel channel) throws Exception {
                    // 只做請求分發,不做具體的數據讀取
                    ServerSocketChannel ch = (ServerSocketChannel) channel;
                    SocketChannel socketChannel = ch.accept();
                    socketChannel.configureBlocking(false);
                    // 收到連接建立的通知之後,分發給I/O線程繼續去讀取數據
                    int index = incr.getAndIncrement() % subReactorThreads.length;
                    ReactorThread workEventLoop = subReactorThreads[index];
                    workEventLoop.doStart();
                    SelectionKey selectionKey = workEventLoop.register(socketChannel);
                    selectionKey.interestOps(SelectionKey.OP_READ);
                    System.out.println(Thread.currentThread().getName() + "收到新連接 : " + socketChannel.getRemoteAddress());
                }
            };
        }


    }

    /**
     * 初始化channel,並且綁定一個eventLoop線程
     *
     * @throws IOException IO異常
     */
    private void initAndRegister() throws Exception {
        // 1、 創建ServerSocketChannel
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        // 2、 將serverSocketChannel註冊到selector
        int index = new Random().nextInt(mainReactorThreads.length);
        mainReactorThreads[index].doStart();
        SelectionKey selectionKey = mainReactorThreads[index].register(serverSocketChannel);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
    }

    /**
     * 綁定端口
     *
     * @throws IOException IO異常
     */
    private void bind() throws IOException {
        //  1、 正式綁定端口,對外服務
        serverSocketChannel.bind(new InetSocketAddress(8080));
        System.out.println("啓動完成,端口8080");
    }

    public static void main(String[] args) throws Exception {
        NioServer3 nioServerV3 = new NioServer3();
        nioServerV3.newGroup(); // 1、 創建main和sub兩組線程
        nioServerV3.initAndRegister(); // 2、 創建serverSocketChannel,註冊到mainReactor線程上的selector上
        nioServerV3.bind(); // 3、 爲serverSocketChannel綁定端口
    }

}

有興趣的可以瞭解下Linux的五種IO模型:

https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect

(PS:未完待續)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章