IO&NIO

1.Socket模擬通信

通過一個案例來實現Socket模擬通信

Service

public class ServerSocketClass {
    // 啓動一個服務端
    public static void main(String[] args) {
        final int DEFAULT_PORT = 8080;
        ServerSocket serverSocket = null;
        // 綁定一個監聽端口
        try {
            serverSocket = new ServerSocket(DEFAULT_PORT);
            // 阻塞操作。等待客戶端的鏈接
            Socket socket = serverSocket.accept();
            System.out.println("客戶端:"+socket.getPort()+"已連接");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String clientStr = bufferedReader.readLine();// 獲得客戶端輸入的信息
            System.out.println("收到客戶端請求消息:"+clientStr);
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("服務端已收到消息\n");
            bufferedWriter.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client

public class ClientSocketClass {

    public static void main(String[] args) {
        final int DEFAULT_PORT = 8080;
        try {
            Socket socket = new Socket("localhost", DEFAULT_PORT);
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("客戶端發送消息:client-01\n");
            bufferedWriter.flush();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String serviceStr = bufferedReader.readLine();
            System.out.println("收到服務端消息:"+serviceStr);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

先運行Service,再運行Client

在這裏插入圖片描述
在這裏插入圖片描述

2.Socket和ServerSocket

通信流程如下:
在這裏插入圖片描述

2.1 Socket類

套接字是網絡連接的一個端點。套接字使得一個應用可以從網絡中讀取和寫入數據。放在兩個不同計算機上的兩個應用可以通過連接發送和接受字節流。爲了從你的應用發送一條信息到另一個應用,你需要知道另一個應用的IP地址和套接字端口。在Java裏邊,套接字指的是java.net.Socket類。要創建一個套接字,你可以使用Socket類衆多構造方法中的一個。其中一個接收主機名稱和端口號:

public Socket (java.lang.String host, int port)

一旦你成功創建了一個Socket類的實例,你可以使用它來發送和接受字節流。要發送字節流,你首先必須調用Socket類的getOutputStream方法來獲取一個java.io.OutputStream對象。要發送文本到一個遠程應用,你經常要從返回的OutputStream對象中構造一個java.io.PrintWriter對象。要從連接的另一端接受字節流,你可以調用Socket類的getInputStream方法用來返回一個java.io.InputStream對象。

2.2 ServerSocket類

Socket類代表一個客戶端套接字,即任何時候你想連接到一個遠程服務器應用的時候你構造的套接字,現在,假如你想實施一個服務器應用,例如一個HTTP服務器或者FTP服務器,你需要一種不同的做法。這是因爲你的服務器必須隨時待命,因爲它不知道一個客戶端應用什麼時候會嘗試去連接它。爲了讓你的應用能隨時待命,你需要使用java.net.ServerSocket類。這是服務器套接字的實現。
ServerSocket和Socket不同,服務器套接字的角色是等待來自客戶端的連接請求。一旦服務器套接字獲得一個連接請求,它創建一個Socket實例來與客戶端進行通信。
要創建一個服務器套接字,你需要使用ServerSocket類提供的四個構造方法中的一個。你需要指定IP地址和服務器套接字將要進行監聽的端口號。通常,IP地址將會是127.0.0.1,也就是說,服務器套接字將會監聽本地機器。服務器套接字正在監聽的IP地址被稱爲是綁定地址。服務器套接字的另一個重要的屬性是backlog,這是服務器套接字開始拒絕傳入的請求之前,傳入的連接請求的最大隊列長度。
其中一個ServerSocket類的構造方法如下所示:

public ServerSocket(int port, int backLog, InetAddress bindingAddress);

對於這個構造方法,綁定地址必須是java.net.InetAddress的一個實例。一種構造InetAddress對象的簡單的方法是調用它的靜態方法getByName,傳入一個包含主機名稱的字符串,就像下面的代碼一樣。

InetAddress.getByName("127.0.0.1");

下面一行代碼構造了一個監聽的本地機器8080端口的ServerSocket,它的backlog爲1。

new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));

一旦你有一個ServerSocket實例,你可以讓它在綁定地址和服務器套接字正在監聽的端口上等待傳入的連接請求。你可以通過調用ServerSocket類的accept方法做到這點。這個方法只會在有連接請求時纔會返回,並且返回值是一個Socket類的實例。Socket對象接下去可以發送字節流並從客戶端應用中接受字節流,就像前一節"Socket類"解釋的那樣。

3.網絡通信協議分析

3.1 網絡分層模型

在這裏插入圖片描述

3.2 OSI開放式互聯網參考模型

3.2.1 第一層:物理層

機械、電子、定時接口通信信道上的原始bit流傳輸
定義硬件設備標準,用於計算機之間的數據傳輸,傳輸bit流。

解決兩臺物理機之間的通信需求,具體就是機器A往機器B發送bit流,機器B能收到這些比特流,這就是物理層要做的事情。物理層主要定義了物理設備的標準,如網線的類型,光纖的接口類型,各種傳輸介質的傳輸速率,主要作用是傳輸bit流,即我們所謂的0101二進制數據。將他們轉換爲電流強弱來進行傳輸,到達目的後再轉換爲0101的機器碼,也就是我們常說的數模轉換與模數轉換。(數模轉換就是將離散的數字量轉換爲連接變化的模擬量。與數模轉換相對應的就是模數轉換,模數轉換是數模轉換的逆過程。)這一層的數據叫做比特,網卡就是工作在這一層的。

3.2.2 第二層:數據鏈路層

物理尋址,同時將原始比特流轉變爲邏輯傳輸線路。
數據幀,對bit數據格式化,校驗。目的是保障數據的可靠性。
在傳輸比特流的過程中,會產生錯傳,數據傳輸不完整的可能,因此數據鏈路層應運而生,數據鏈路層定義瞭如何格式化數據以進行傳輸,以及如何控制對物理介質的訪問。這一層通常還提供錯誤檢測和糾正以確保數據傳輸的可靠性。本層將比特數據組成了幀,其中交換機工作在這一層面。對幀解碼,並根據幀中包含的信息把數據發送到正確的接收方,那隨着網絡節點的不斷增加,點對點通信的時候是需要多個節點的,那麼如何找到目標節點,如何選擇最佳路徑便成爲了首要需求,此時便有了網絡層。

3.2.3 第三層:網絡層

控制子網的運行,如邏輯編輯、分組傳輸、路由選擇。
IP尋址,通過IP連接網絡上的計算機。
其主要功能是將網絡地址翻譯成對應的物理地址,並決定如何將數據從發送方路由到接收方。網絡層通過綜合考慮,發送優先權。通過網絡擁塞層度,服務質量以及可選路由的花費來決定從一個網絡中節點A到另一個節點B的最佳路徑。由於網絡層處理並智能指導數據傳送路由器連接網絡各段,所以路由器屬於網絡層。此層的數據,我們稱之爲數據包。本層我們需要關注的協議爲TCP/IP協議裏面的IP協議。隨着網絡通信需求的進一步擴大,通信過程中需要發送大量的數據,如海量文件傳輸的,可能需要很長時間,而網絡在通信的過程當中,會中斷好多次,此時爲了保證傳輸大量文件時的準確性,需要對發出去的數據進行切分,切割爲一個一個的段落。

3.2.4 第四層:傳輸層

接收上一層數據,在必要的時候把數據進行分割,並將這些數據較爲網絡層,且保證這些數據段有效到達對端。建立主機端對端的鏈接。
傳輸層解決了主機間的數據傳輸,數據的傳輸可以是不同網絡的,並且傳輸層解決了傳輸質量的問題,該層稱之爲OSI模型中最重要的一層。傳輸協議同時進行流量控制,或是基於可接收方接收數據的快慢程度規定適當的發送數率。除此之外,傳輸層按照網絡能處理的最大尺寸,將較長的數據包進行強制分割,例如:以太坊無法接收大於1500字節的數據包。發送方節點的傳輸層將數據分割成較小的數據片,同時對每一數據片安排一個序列號,以便數據到達接收方的傳輸層時,能以正確的順序重組。該過程即稱爲排序。傳輸層中需要我們關注的協議有TCP/IP的TCP協議和UDP協議。

3.2.5 第五層:會話層

不同機器(如windows linux)上的用戶之間建立及管理會話。管理不同設備之間的通信

3.2.6 第六層:表示層

對應用層數據編碼和數據格式轉換,保障不同設備之間的通信。
信息的語法語義以及它們的關聯,如加密解密、轉換翻譯,壓縮解壓縮。

3.2.7 第七層:應用層

提供應用接口,爲用戶直接提供各種網絡服務

4. 請求發送過程

在這裏插入圖片描述

5.請求接收過程

在這裏插入圖片描述

6.深入分析NIO

6.1 阻塞和非阻塞

同步: 客戶端發起一個請求,這個請求需要同步等待結果,在結果返回之前,這個客戶端一直處於阻塞狀態。
阻塞: 同步代表一個通信機制,阻塞指的是在同步機制下結果返回之前客戶端的狀態。

在這裏插入圖片描述

異步: 客戶端發起一個請求後,不需要一直等待,可以繼續其他操作,然後服務端異步返回請求結果。
非阻塞: 異步代表一個通信機制,非阻塞指的是在異步機制下結果返回之前客戶端的狀態。
在這裏插入圖片描述

6.2 深入分析5種IO模型

6.2.1 阻塞IO

在這裏插入圖片描述

6.2.2 非阻塞IO

在這裏插入圖片描述

6.2.3 IO複用

在這裏插入圖片描述

6.2.4 信號驅動

在這裏插入圖片描述

6.2.5異步IO

在這裏插入圖片描述

6.3 NIO的概述及應用

6.3.1 NIO的新特性

在這裏插入圖片描述

6.3.2 核心組件

  • 通道(Channel): Java NIO數據來源,可以是網絡,也可以是本地磁盤
  • 緩衝區(Buffer):數據讀寫的中轉區,後續會單獨講
  • 選擇器(Selectors):異步IO的核心類,可以實現異步非阻塞IO,一個selectors可以管理多個通道Channel

6.3.3 IO和NIO的區別

類型 面向操作區域 處理數據(字節流&字符流) IO阻塞/非阻塞
Java IO 直接面向最初的數據源 每次讀取時 = 讀取所有字節/字符,無緩存;無法前後移動讀取流中的數據 當一個線程在讀/寫時:當數據被完全讀取/寫入完畢前&數據未準備好時,線程不能做其他任務,只能一直等待,直到數據準備好後繼續讀取/寫入,即阻塞。;當線程處於活躍狀態時&外部未準備好時,則阻塞。
Java NIO 面向緩衝區 先將數據讀取到緩存區;可在緩衝區前後移動流數據 當一個線程向某通道發送請求要求讀/寫時,當數據被完全讀取/寫入完畢前&數據未準備好時,線程可以做其他任務(控制其他通道),直到數據準備好後再切換回該通道,繼續讀取/寫入,即選擇器(selector)的使用;外部 準備好時才喚醒線程 ,則不會阻塞。

6.3.4 Demo

	public static void main(String[] args) {
        // 實現一個文件複製
        try {
            FileInputStream fis = new FileInputStream(new File("G:/test.txt"));
            FileOutputStream fos = new FileOutputStream(new File("G:/test-10.txt"));

            FileChannel fin = fis.getChannel();
            FileChannel fout = fos.getChannel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);
            fin.read(buffer); // 讀取數據到緩衝區
            buffer.flip(); // 表示從讀轉化爲寫
            fout.write(buffer);
            buffer.clear(); // 重置緩衝區

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

6.3.4 NIO詳解Channel和Buffer

6.3.4.1 什麼是Channel

什麼是Channel?由java.nio.channels包定義的,Channel表示IO源與目標打開的連接,Channel類似於傳統的“流”,只不過Channel本身不能直接訪問數據,Channel只能與Buffer進行交互。通道主要用於傳輸數據,從緩衝區的一側傳到另一側的實體(如文件、套接字…),反之亦然;通道是訪問IO服務的導管,通過通道,我們可以以最小的開銷來訪問操作系統的I/O服務;順便說下,緩衝區是通道內部發送數據和接收數據的端點。

在這裏插入圖片描述

6.3.4.2 Channel的實現

  • F i l e C h a n n e l: 從文件中讀寫數據
  • D a t a g r a m C h a n n e l: 通過U D P協議讀寫網絡中的數據
  • S o c k e t C h a n n e l: 通過T C P協議讀寫網絡中的數據
  • S e r v e r S o c k e t C h a n n e l: 監聽一個T C P連接,對於每一個新的客戶端連接都會創建一個S o c k e t C h a n n e l。

6.3.4.3 什麼是Buffer

b u f f e r是一個對象,它包含了需要寫入或者剛讀出的數據,最常用的緩衝區類型是 B y t e B u f f e r

在這裏插入圖片描述

6.3.5 NIO Read Demo

	public static void main(String[] args) {
        try(FileInputStream inputStream = new FileInputStream("G:/test.txt");){
            // 針對本地磁盤的文件進行操作
            FileChannel fileChannel = inputStream.getChannel();
            // 讀取數據,分配緩衝區大小 與下面這種寫法效果一樣
            // byte[] bytes = new byte[1024];
            // ByteBuffer buffer = ByteBuffer.wrap(bytes);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int read = fileChannel.read(buffer);
            System.out.println(new String(buffer.array()));

        }catch (Exception e){
            e.printStackTrace();
        }
    }

6.3.6 NIO Write Demo

	public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("G:/test-nio-write.txt")){
            FileChannel channel = fos.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 往buffer寫數據
            buffer.put("NIO Wirte Test Demo".getBytes()); // 往緩衝區寫數據
            buffer.flip(); // 讀模式轉換爲寫模式
            channel.write(buffer);// 讀取buffer數據

        }catch (Exception e){
            e.printStackTrace();
        }
    }

6.3.7 Buffer的本質

緩衝區本質上是一塊可以寫入數據,以及從中讀取數據的內存,實際上也是一個byte[]數據,只是在NIO中被封裝成了NIO Buffer對象,並提供了一組方法來訪問這個內存塊,要理解buffer的工作原理,需要知道幾個屬性

  • capacity // ByteBuffer.allocate(10);
  • position
  • limit

初始狀態下,limit和capacity都是8,而position=0,如果當前是從通道讀取數據到緩衝區,那麼下一個讀取的數據就會存儲到0位置。
在這裏插入圖片描述

第一次讀取數據,讀了四個字節,position指向4

在這裏插入圖片描述

第二次讀取了兩個字節,position指向6

在這裏插入圖片描述

讀完數據之後,調用flip方法切換爲寫模式。調用之後,如下圖:

在這裏插入圖片描述

數據寫出,寫完之後,position會不斷的移動

在這裏插入圖片描述

當執行完clear方法,緩衝區又回到了初始的狀態。

6.3.8 零拷貝

IO的通信原理:

在這裏插入圖片描述

IO流程:

  • 內核給磁盤控制器發命令說:我要讀磁盤上的某某塊磁盤塊上的數據
  • 在DMA的控制下,把磁盤上的數據讀入到內核緩衝區
  • 內核把數據從內核緩衝區複製到用戶緩衝區
byte[] b = new byte[1024];

while((read = inputStream.read(b))>=0){
	total = total + read;
	// TODO
}

mmap原理:
在這裏插入圖片描述

	public static void main(String[] args) throws IOException {
        FileChannel inChannel = FileChannel.open(Paths.get("G:/test.txt"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("G:/test-mmap.txt"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMap = outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
        byte[] bytes = new byte[inMap.limit()];
        inMap.get(bytes);
        outMap.put(bytes);
        inChannel.close();
        outChannel.close();
    }

IO拷貝流程:

在這裏插入圖片描述

零拷貝流程:

在這裏插入圖片描述

Demo:

public class ZeroCopyServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            SocketChannel socketChannel = serverSocketChannel.accept();
            ByteBuffer buffer = ByteBuffer.allocate(2048);
            int r = 0;
            FileChannel fileChannel = new FileOutputStream("G:/02-技術分析之Spring框架的概述_copy.flv").getChannel();
            while (r != -1) {
                r = socketChannel.read(buffer);
                buffer.flip();
                fileChannel.write(buffer);
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {

        }
    }
}
public class ZeroCopyClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            FileChannel fileChannel = new FileInputStream("G:/02-技術分析之Spring框架的概述_.flv").getChannel();
            // tf表示總的字節數
            int position = 0;
            long size = fileChannel.size();
            while (size > 0) {
                long tf = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
                if (tf > 0) {
                    position += tf;
                    size -= tf;
                }
            }
            System.out.println("總的數據傳輸字節數:" + position);
            socketChannel.close();
            fileChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6.3.9 SocketChannel和ServeSocketChannel

demo:

public class NIOSocketServer01 {
    public static void main(String[] args) {
        try {
            // 可以支持兩種模式:阻塞,非阻塞
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //  修改阻塞模式 start
            serverSocketChannel.configureBlocking(false); // 默認是true
            //  修改阻塞模式 end
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));

            while (true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel != null) {
                    // 如果代碼進入這個位置,說明有連接進來
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer);
                    System.out.println(new String(buffer.array()));
                    // 再把消息寫回客戶端
                    Thread.sleep(10000);
                    buffer.flip();
                    socketChannel.write(buffer);
                }else{
                    Thread.sleep(1000);
                    System.out.println("沒有客戶端連接進來");
                }

            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class NIOSocketClient01 {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false); // 把客戶端設置爲非阻塞
            // 非阻塞模式下,後面的代碼並不一定是等到建立連接之後再往下執行
            socketChannel.connect(new InetSocketAddress("localhost",8080));
            if(socketChannel.isConnectionPending()){
                socketChannel.finishConnect();
            }
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("Hello,I'm SockectChannel Client01".getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            // 讀取服務端返回的數據
            buffer.clear();
            int r = socketChannel.read(buffer); //非阻塞模式下,這裏不阻塞
            if(r>0) {
                System.out.println("收到服務端的消息:" + new String(buffer.array()));
            }else{
                System.out.println("服務端的數據還未返回");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6.3.10 選擇器 Selector

Selector(選擇器,多路複用器)是Java NIO中能夠檢測一到多個NIO通道,是否爲諸如讀寫事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接。

在這裏插入圖片描述

Demo:

public class NIOSelectorServerDemo {

    static Selector selector; // 多路複用器

    public static void main(String[] args) {
        try {
            selector = Selector.open(); // 創建一個多路複用器
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false); // 多路複用器下,這個必須設置爲非阻塞
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            // 監聽連接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove(); // 避免重複處理
                    if (selectionKey.isAcceptable()) {
                        handleAccept(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        handleRead(selectionKey);
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleAccept(SelectionKey selectionKey) {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.write(ByteBuffer.wrap("Hello Client , I'm NIO Server With Selector".getBytes()));
            socketChannel.register(selector,SelectionKey.OP_READ);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleRead(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        socketChannel.read(buffer);
        System.out.println("server receive Msg:"+new String(buffer.array()));

    }
}
public class NIOSelectorClientDemo {

    static Selector selector;

    public static void main(String[] args) {
        try {
            selector = Selector.open();
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isConnectable()) {
                        handleConnect(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        handleRead(selectionKey);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void handleConnect(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        if (socketChannel.isConnectionPending()){
            socketChannel.finishConnect();
        }
        socketChannel.configureBlocking(false);
        socketChannel.write(ByteBuffer.wrap("Hello Server ,I'm NIO Client".getBytes()));
        socketChannel.register(selector,SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        socketChannel.read(buffer);
        System.out.println("Server receive Msg:"+new String(buffer.array()));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章