NIO基礎-Netty系列-2

概述

初步瞭解了NIO核心組件的API,也大致知道了如何啓動一個網絡IO服務和客戶端後。本篇在此基礎上做一些補充,把一些必須要理解的

正文

ServerSocketChannel的accept方法和Selecor的select

在ServerSocketChannel的API中我們可以通過accept方法監聽傳入的連接,有傳入連接的時候返回一個SocketChannel。也可以註冊ACCEPT事件到Selector上,然後通過Selector的select方法監聽傳入的連接。

需要特別注意的是,accept方法返回的是一個連接,並不是連接集合,而通過Selector的select方法返回的是符合事件要求數量,然後通過selectedKeys獲得SelectoinKey集合。

進行SelectionKey集合遍歷處理的時候,需要刪除元素

注意到通過selectedKeys獲得SelectoinKey集合後,操作完會進行remove,或者遍歷操作後一次性進行clear操作。因爲Selector維護着selectedKeys,所以在操作完後如果不進行刪除,那麼下次輪詢還會存在,而出現混亂,那麼Selector爲什麼不自己刪除呢,這就不對了,畢竟人家是告訴你現在的事件集合讓你處理,你處理是否結束並不清楚,所以由真正處理方來決定刪除最合理了。

selector.select()一直返回大於0問題

在前面的例子的Server端代碼中,當client發起連接然後發送消息後斷開連接,服務端通過read方法獲取數據並且執行selectionKeys.remove()方法,但是在下一次while循環中selector.select()結果還是大於0,debug代碼可以發現此時selectionKey任然是isReadable的,只是read方法返回並不大於0,所以不會進入第二個while循環中,雖然如此,這個時候是一直死循環在執行第一個while循環的,並不是我們預期的阻塞在selector.select()上等待就緒事件的觸發。

客戶端關閉連接的時候,read會返回-1,所以我們可以在第二次循環的時候直接關閉Channel。

ByteBuffer的複用

可以看到在服務端read數據的時候使用ByteBuffer承接,獲得的數據是不會超過ByteBuffer的長度,在使用完後就進行clear操作切換到讀模式進行復用,否則每次讀事件都需要爲ByteBuffer申請新的內存,非常浪費。當再次需要切換到寫模式的時候使用flip方法切換。

Selector.wakeup()

有意思的是Selector採用自己和自己建的TCP連接(Windows)或pipe(Linux)來實現wakeup,因爲只要向這個連接或pipe裏發點數據,就可以喚醒select了。

以下兩個文章透析了這部分的知識:

https://blog.csdn.net/haoel/article/details/2224055

https://blog.csdn.net/haoel/article/details/2224069

SelectionKey.attach(Object ob)

SelectionKey關聯一個對象,這個對象可以通過attachment方法取出,這點在前篇已經提過,這裏再說明一下是要強調一遍通過這個方法,可以比較方便的圍繞selectorKey來進行處理IO流程的拆分工作。比如,我們設計一個接口,所有的綁定對象都實現這個接口,接口中定義一個執行方法,在Accept的SelectorKey上綁定專門處理Accept時間的對象,在READ事件綁定專門處理的對象,如此代碼可以是一致的都是從SelectorKey中拿到對象調用執行方法。

關於拆包粘包

前置說明一下這個概念,首先需要理解的是TCP是流式協議,傳輸內容像流水一樣是沒有明確段落的概念的,而實際使用時我們會把通信的消息進行定義,也就會把一段完整內容定義成有業務含義的消息,那麼天然是需要解決這個矛盾的。這也是接觸Netty時很快會了解解決拆包粘包問題的辦法,這裏只是提一下,瞭解是有這個問題的即可。

代碼

在前面的代碼基礎上,進行了改造,實現一個C/S模型的文件傳輸功能,客戶端將本地文件通過Channel傳輸給服務端,服務端將文件寫到自己本地。

Client代碼:

    public static void start() throws IOException {
        InetSocketAddress inetSocketAddress = new InetSocketAddress(20023);
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(inetSocketAddress);
        ByteBuffer byteBuffer = ByteBuffer.allocate(204800);
        FileChannel fileChannel =  new FileInputStream("/Users/dongchao/test").getChannel();
        if (socketChannel.finishConnect()) {
            while (fileChannel.read(byteBuffer) > 0) {
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                byteBuffer.clear();
            }
        }
        fileChannel.close();
    }

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

Server端代碼:

public class Server {

    public static void start() throws IOException {
        // 開啓Selector
        Selector selector = Selector.open();
        // 開啓一個Server Socket Channel 用於監聽連接Accept事件
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 設置成非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 監聽端口
        serverSocketChannel.bind(new InetSocketAddress(20023));
        // 設置Selector 多路複用監聽Accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 產生一個文件名
        String localFile = getFileName();
        // 獲取一個File Channel
        FileOutputStream fos = new FileOutputStream(localFile);
        FileChannel outFileChannel = fos.getChannel();
        // 記錄文件字節量
        long outLength = 0;
        ByteBuffer byteBuffer = ByteBuffer.allocate(204800);

        // 阻塞選擇準備好進行I/O操作的鍵
        while(selector.select() > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = serverSocketChannel1.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    int length;
                    // 對於Socket Channel來說是從Channel上讀取數據,寫入到ByteBuffer
                    while ((length = socketChannel.read(byteBuffer)) > 0) {
                        // 切換成讀模式
                        byteBuffer.flip();
                        // 對於File Channel來說是讀取ByteBuffer數據,寫入到Channel
                        outFileChannel.write(byteBuffer);
                        outLength = outLength + length;
                        // 切換成寫模式
                        byteBuffer.clear();
                        outFileChannel.force(true);
                    }
                    // read 返回-1 客戶端關閉連接
                    if (length < 0) {
                        socketChannel.close();
                    }
                }
                iterator.remove();
            }
            System.out.print("file length : " + outLength);
        }
        fos.close();
        outFileChannel.close();
    }

    private static String getFileName() {
        long currentTimeMillis = System.currentTimeMillis();
        return "/Users/dongchao/" + currentTimeMillis;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章