Java NIO

每一種技術的出現(xiàn)汁针,都是為了解決某一個或者某一類問題。讓我們先來了解問題的產生粹胯。

問題
使用socket通信實現(xiàn)如下:
1.client連接server
2.client發(fā)送"Hi Server,I am client."
3.server收到消息在控制臺的打印下隧,并回復"Hi client,I am Server."
4.client收到消息在控制臺打印。
5.client斷開連接。

1.Simple Solution(方式一)

直接貼代碼了

/**
 * @description: SimpleSolution server 
 * @author: sanjin
 * @date: 2019/7/8 11:33
 */
public class Server {
    public static void main(String[] args) {
        // 服務端占用端口
        int port = 8000;
        // 創(chuàng)建 serversocker
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (serverSocket != null) {
            while (true) {
                InputStream is = null;
                OutputStream os = null;
                Socket client = null;
                try {
                    // accept()方法會阻塞殷勘,直到有client連接后才會執(zhí)行后面的代碼
                    client = serverSocket.accept();
                    is = client.getInputStream();
                    os = client.getOutputStream();

                    // 3.server收到消息在控制臺的打印此再,并回復"Hi client,I am Server."
                    byte[] buffer = new byte[5];
                    int len = 0;
                    // 使用ByteArrayOutputStream,避免緩沖區(qū)過小導致中文亂碼
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    while ((len = is.read(buffer)) != -1) {
                        baos.write(buffer,0,len);
                    }
                    System.out.println(baos.toString());
                    // 服務端回復客戶端消息
                    os.write("Hi client,I am Server.".getBytes());
                    os.flush(); // 刷新緩存玲销,避免消息沒有發(fā)送出去
                    client.shutdownOutput();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    // 程序異呈淠矗或者執(zhí)行完成,關閉流贤斜,防止占用資源
                    if (client != null) {
                        try {
                            client.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (is != null) {
                        try {
                            is.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (os != null) {
                        try {
                            os.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }

            }
        }


    }
}


/**
 * @description: SimpleSolution client
 * @author: sanjin
 * @date: 2019/7/8 11:33
 */
public class Client {
    public static void main(String[] args) {
        int port = 8000;
        Socket client = null;
        InputStream is = null;
        OutputStream os = null;
        try {
            // 1.client連接server
            client = new Socket("localhost", port);
            is = client.getInputStream();
            os = client.getOutputStream();

            // 2.client發(fā)送"Hi Server,I am client."
            os.write("Hi Server,I am client.".getBytes());
            os.flush();
            // 調用shutdownOutput()方法表示客戶端傳輸完了數(shù)據(jù)策吠,否則服務端的
            // read()方法會一直阻塞
            // (你可能會問我這不是寫了 read()!=-1, -1表示的文本文件的結尾字符串,而對于字節(jié)流數(shù)據(jù),
            // 是沒有 -1 標識的,這就會使服務端無法判斷客戶端是否發(fā)送完成仰泻,導致read()方法一直阻塞)
            client.shutdownOutput();

            // 4.client收到消息在控制臺打印埃唯。
            int len = 0;
            byte[] buffer = new byte[5];
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer,0,len);
            }
            System.out.println(baos.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 程序異常或者執(zhí)行完成,關閉流,防止占用資源
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (os != null) {
                    os.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (client != null) {
                    client.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


    }
}

程序描述圖:


1.png

順便說一下,ProcessOn真的很好用??

關于Socket編程跋理,有幾個注意點:

  1. 注意使用流時一定要用try-catch-finally,雖然代碼確實有點繁瑣。
    2.客戶端如果發(fā)送的使中文恬总,在服務端接收數(shù)據(jù)時候前普,要注意接收方式:
// 接收數(shù)據(jù)方式一
byte[] buffer = new byte[5];
int len = 0;
// 使用ByteArrayOutputStream,避免緩沖區(qū)過小導致中文亂碼
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
    baos.write(buffer, 0, len);
}
System.out.println(baos.toString());

// 接收數(shù)據(jù)方式一
byte[] buffer = new byte[5];
int len = 0;
// 使用ByteArrayOutputStream壹堰,避免緩沖區(qū)過小導致中文亂碼
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
    // 這種方式會導致中文亂碼
    System.out.println(new String(buffer, 0, len));
}

如果客戶端傳輸中文使用方式二會導致中文亂碼汁政,這是因為我們在讀取時候緩沖區(qū)大小設置的是5個字節(jié),此處假設客戶端傳輸“小白兔“三個字缀旁,常用的漢字一般占3個字節(jié)记劈。”小白兔“發(fā)送過來后我們的緩沖區(qū)只有5個字節(jié)并巍,沒辦法一次讀取完目木,所以要分二次讀取,第一次讀取5個字節(jié)懊渡,然后立即進行了打印刽射,漢字”小“會被正常打印,但是漢字”白“只讀取了2個字節(jié)剃执,打印就會產生亂碼誓禁。而使用ByteArrayOutputStream把緩沖區(qū)讀取的字節(jié)全都存放一起,然后一起打印肾档,就不會導致亂碼了摹恰。
3.shutdownOutput()方法辫继。當客戶端傳輸”Hi Server,I am client.“,服務端接收數(shù)據(jù)并打印出來俗慈,然后向客戶端發(fā)送”"Hi client,I am Server."姑宽。如果不使用shutdownOutput()方法會使服務端卡在read()方法。這是因為當客戶端數(shù)據(jù)發(fā)送完成后闺阱,服務端的判斷條件
while ((len = is.read(buffer)) != -1)
不成立炮车,因為只有文本文件的末尾是 -1,而字節(jié)流沒有末尾標識,這就導致服務端不知道客戶端有沒有發(fā)送完成酣溃,使得read()方法阻塞瘦穆。所以客戶端發(fā)送完數(shù)據(jù)后需要發(fā)送一個標識來表示”我已經(jīng)發(fā)送完數(shù)據(jù)了“。而shutdownOutput()方法就是這個標識赊豌。

我們使用socket完成了一個收發(fā)的程序扛或。但是它還存在著問題。

1. 不能同時有多個client連接我們的server
服務端與客戶端連接使用依靠accept()函數(shù)亿絮,而我們的服務端程序是單線程告喊,只能等當前的socket執(zhí)行完成后麸拄,才能接收下一個socket的連接派昧。

假設我們同時又2個client連接server會發(fā)生什么?(因為我們程序簡單拢切,執(zhí)行的很快蒂萎,所以我在server種加了Thread.sleep(50*1000))
現(xiàn)象:第二個client會拋出異常:


1.png

下面我們就用多線程解決這個問題。


2.Multithreading Solution(方式二)

我又新加了一個HandlerClient類淮椰,實現(xiàn)Runnable接口五慈,用于處理client連接,Client類的代碼沒有做修改主穗。


/**
 * @description: MultithreadingSolution client
 * @author: sanjin
 * @date: 2019/7/8 11:33
 */
public class Client {
    public static void main(String[] args) {
        int port = 8000;
        Socket client = null;
        InputStream is = null;
        OutputStream os = null;
        try {
            // 1.client連接server
            client = new Socket("localhost", port);
            is = client.getInputStream();
            os = client.getOutputStream();

            // 2.client發(fā)送"Hi Server,I am client."
            os.write("Hi Server,I am client.".getBytes());
            os.flush();
            // 調用shutdownOutput()方法表示客戶端傳輸完了數(shù)據(jù)泻拦,否則服務端的
            // read()方法會一直阻塞
            // (你可能會問我這不是寫了 read()!=-1, -1表示的文本文件的結尾字符串,而對于字節(jié)流數(shù)據(jù)忽媒,
            // 是沒有 -1 標識的争拐,這就會使服務端無法判斷客戶端是否發(fā)送完成,導致read()方法一直阻塞)
            client.shutdownOutput();

            // 4.client收到消息在控制臺打印晦雨。
            int len = 0;
            byte[] buffer = new byte[5];
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer,0,len);
            }
            System.out.println(baos.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 程序異臣懿埽或者執(zhí)行完成,關閉流闹瞧,防止占用資源
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (os != null) {
                    os.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (client != null) {
                    client.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


    }
}

/**
 * @description: MultithreadingSolution server
 * @author: sanjin
 * @date: 2019/7/8 11:33
 */
public class Server {
    public static void main(String[] args) {
        // 服務端占用端口
        int port = 8000;
        // 創(chuàng)建 serversocker
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (serverSocket != null) {
            while (true) {
                try {
                    Socket client = serverSocket.accept();
                    System.out.println("收到client連接绑雄,client地址:"+client.getInetAddress());
                    new Thread(new HandlerClient(client)).start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * @description: 用于處理client連接
 * @author: sanjin
 * @date: 2019/7/8 16:28
 */
public class HandlerClient implements Runnable {
    private Socket client;

    public HandlerClient(Socket client) {
        this.client = client;
    }

    @Override
    public void run() {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = client.getInputStream();
            os = client.getOutputStream();

            // 3.server收到消息在控制臺的打印,并回復"Hi client,I am Server."
            byte[] buffer = new byte[5];
            int len = 0;
            // 使用ByteArrayOutputStream奥邮,避免緩沖區(qū)過小導致中文亂碼
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = is.read(buffer)) != -1) {
                // 這種方式會導致中文亂碼
                // System.out.println(new String(buffer, 0, len));
                baos.write(buffer, 0, len);
            }
            System.out.println(baos.toString());

            try {
                // 增加任務執(zhí)行時間万牺,用于進行多個client連接測試
                Thread.sleep(20*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 服務端回復客戶端消息
            os.write("Hi client,I am Server.".getBytes());
            os.flush(); // 刷新緩存罗珍,避免消息沒有發(fā)送出去
            client.shutdownOutput();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 程序異常或者執(zhí)行完成杏愤,關閉流靡砌,防止占用資源
            if (client != null) {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

運行結果:


1.png

問題:計算機的CPU資源有限,來一個client就會創(chuàng)建一個線程珊楼,線程完成任務后再進行銷毀通殃,線程的創(chuàng)建、銷毀以及線程上下文的切換會消耗很多CPU的資源厕宗。并且JVM中線程數(shù)過多還有可能拋出內存不足的異常画舌。

所以我們下一步使用線程池來解決這個問題。

程序描述圖:


1.png

3.Thread Pool Solution(方式三)

線程池解決方法思路:


1.png

我們再方式二已經(jīng)完成了多線程方式代碼已慢,將它修改成線程池方式非常簡單曲聂,我們只需要修改Server類就可以了:


/**
 * @description: ThreadPoolSolution server
 * @author: sanjin
 * @date: 2019/7/8 11:33
 */
public class Server {

    // 創(chuàng)建線程池
    private static ThreadPoolExecutor threadPoolExecutor =
            new ThreadPoolExecutor(
                    5, // 核心線程數(shù)
                    10, // 最大線程數(shù)
                    200, // keep alive 時間
                    TimeUnit.HOURS, // keep alive 時間單位
                    new ArrayBlockingQueue<Runnable>(5) // 工作隊列
            );

    public static void main(String[] args) {
        // 服務端占用端口
        int port = 8000;
        // 創(chuàng)建 serversocker
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (serverSocket != null) {
            while (true) {
                try {
                    Socket client = serverSocket.accept();
                    System.out.println("收到client連接,client地址:"+client.getInetAddress());

                    // 多線程方式
                    // new Thread(new HandlerClient(client)).start();

                    // 線程池方式
                    threadPoolExecutor.execute(new HandlerClient(client));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

不知道大家暈了沒佑惠,我已經(jīng)快不行了朋腋,但還是要明白我們使用多線程的目的:
解決多個client同時連接的問題
好了膜楷,下面主角登場旭咽。

4.NIO(方式三)

關于JavaNIO有一個非常好的英文資料:http://tutorials.jenkov.com/java-nio/index.html


/**
 * @description:
 * @author: sanjin
 * @date: 2019/7/8 19:56
 */
public class NIOClient {

    public static void main(String[] args) {
        SocketAddress socketAddress = new InetSocketAddress(8000);
        SocketChannel socketChannel = null;
        try {
            socketChannel = SocketChannel.open(socketAddress);
            socketChannel.configureBlocking(false);
            if (socketChannel.finishConnect()) {

                ByteBuffer buffer = ByteBuffer.allocate(1024);
                // 客戶端發(fā)送數(shù)據(jù) "Hi Server,I am client."
                buffer.clear();
                buffer.put("Hi Server,I am client.".getBytes());
                buffer.flip();
                while (buffer.hasRemaining()) {
                    socketChannel.write(buffer);
                }
                // 客戶端接收服務端數(shù)據(jù)打印在控制臺

                buffer.clear();
                int len = socketChannel.read(buffer);
                while (len > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        System.out.print((char) buffer.get());
                    }
                    System.out.println();
                    buffer.clear();
                    len = socketChannel.read(buffer);
                }
                if (len == -1) {
                    socketChannel.close();
                }
            }


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (socketChannel != null) {
                    socketChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}


/**
 * @description:
 * @author: sanjin
 * @date: 2019/7/8 19:56
 */
public class NIOServer {


    public static void main(String[] args) {
        ServerSocketChannel serverSocketChannel = null;
        Selector selector = null;
        try {
            // 初始化一個 serverSocketChannel
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8000));

            // 設置serverSocketChannel為非阻塞模式
            // 即 select()會立即得到返回
            serverSocketChannel.configureBlocking(false);

            // 初始化一個 selector
            selector = Selector.open();

            // 將 serverSocketChannel 與 selector綁定
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                // 通過操作系統(tǒng)監(jiān)聽變化的socket個數(shù)
                // 在windows平臺通過selector監(jiān)聽(輪詢所有的socket進行判斷,效率低)
                // 在Linux2.6之后通過epool監(jiān)聽(事件驅動方式赌厅,效率高)
                int count = selector.select(3000);
                if (count > 0) {
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();

                        if (key.isAcceptable()) {
                            handleAccept(key);
                        }
                        if (key.isReadable()) {
                            handleRead(key);
                        }
                        if (key.isWritable() && key.isValid()) {
                            handleWrite(key);
                        }
                        if (key.isConnectable()) {
                            System.out.println("isConnectable = true");
                        }
                        iterator.remove();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (serverSocketChannel != null) {
                    serverSocketChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (selector != null) {
                    selector.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static void handleWrite(SelectionKey key) {
        // 獲取 client 的 socket
        SocketChannel clientChannel = (SocketChannel) key.channel();
        // 獲取緩沖區(qū)
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        buffer.put("Hi client,I am Server.".getBytes());
        buffer.flip();
        try {
            while (buffer.hasRemaining()) {
                clientChannel.write(buffer);
            }
            buffer.compact();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleRead(SelectionKey key) {
        // 獲取 readable 的客戶端 socketChannel
        SocketChannel clientChannel = (SocketChannel) key.channel();
        // 讀取客戶端發(fā)送的消息信息,我們已經(jīng)在 acceptable 中設置了緩沖區(qū)
        // 所以直接沖緩沖區(qū)讀取信息
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        // 獲取 client 發(fā)送的消息
        try {
            int len = clientChannel.read(buffer);
            while (len > 0) {
                // 設置 limit 位置
                buffer.flip();
                // 開始讀取數(shù)據(jù)
                while (buffer.hasRemaining()) {
                    byte b = buffer.get();
                    System.out.print((char) b);
                }
                System.out.println();
                // 清除 position 位置
                buffer.clear();
                // 從新讀取 len
                len = clientChannel.read(buffer);
            }
            if (len == -1) {
                clientChannel.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }



    }

    private static void handleAccept(SelectionKey key) {
        // 獲得 serverSocketChannel
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        try {
            // 獲得 socketChannel,就是client的socket
            SocketChannel clientChannel = serverSocketChannel.accept();
            if (clientChannel == null) return;
            // 設置 socketChannel 為無阻塞模式
            clientChannel.configureBlocking(false);
            // 將其注冊到 selector 中穷绵,設置監(jiān)聽其是否可讀,并分配緩沖區(qū)
            clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(512));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末特愿,一起剝皮案震驚了整個濱河市仲墨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌揍障,老刑警劉巖目养,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異毒嫡,居然都是意外死亡癌蚁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門审胚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匈勋,“玉大人,你說我怎么就攤上這事膳叨∏⒔啵” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵菲嘴,是天一觀的道長饿自。 經(jīng)常有香客問我汰翠,道長,這世上最難降的妖魔是什么昭雌? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任复唤,我火速辦了婚禮,結果婚禮上烛卧,老公的妹妹穿的比我還像新娘佛纫。我一直安慰自己,他們只是感情好总放,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布呈宇。 她就那樣靜靜地躺著,像睡著了一般局雄。 火紅的嫁衣襯著肌膚如雪甥啄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天炬搭,我揣著相機與錄音蜈漓,去河邊找鬼。 笑死宫盔,一個胖子當著我的面吹牛融虽,可吹牛的內容都是我干的。 我是一名探鬼主播飘言,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼衣形,長吁一口氣:“原來是場噩夢啊……” “哼驼侠!你這毒婦竟也來了姿鸿?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤倒源,失蹤者是張志新(化名)和其女友劉穎苛预,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笋熬,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡热某,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了胳螟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昔馋。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖糖耸,靈堂內的尸體忽然破棺而出秘遏,到底是詐尸還是另有隱情,我是刑警寧澤嘉竟,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布邦危,位于F島的核電站洋侨,受9級特大地震影響,放射性物質發(fā)生泄漏倦蚪。R本人自食惡果不足惜希坚,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陵且。 院中可真熱鬧裁僧,春花似錦、人聲如沸慕购。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脓钾。三九已至售睹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間可训,已是汗流浹背昌妹。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留握截,地道東北人飞崖。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像谨胞,于是被迫代替她去往敵國和親固歪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容