NIO網絡編程中重復觸發讀(寫)事件


一、前言

  公司最近要基於Netty構建一個TCP通訊框架, 因Netty是基於NIO的,為了更好的學習和使用Netty,特意去翻了之前記錄的NIO的資料,以及重新實現了一遍NIO的網絡通訊,不試不知道,一試發現好多細節沒注意,導致客戶端和服務端通訊的時候出現了一些非常莫名其妙的問題,這邊我記錄下耗了我一晚上的問題~

二、正文

  廢話不多說,先上問題代碼~

  服務端:

package com.nio.server;

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;

public class NIOServer {
    private static Selector selector;
    private static ServerSocketChannel serverSocketChannel;
    private static ByteBuffer bf = ByteBuffer.allocate(1024);
    public static void main(String[] args) throws Exception{
        init();
        while(true){
            selector.select();
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while(it.hasNext()){
                SelectionKey key = it.next();
                if(key.isAcceptable()){
                    System.out.println("連接准備就緒");
                    ServerSocketChannel server = (ServerSocketChannel)key.channel();
                    System.out.println("等待客戶端連接中........................");
                    SocketChannel channel = server.accept();
                    channel.configureBlocking(false);
                    channel.register(selector,SelectionKey.OP_READ);
                }
                else if(key.isReadable()){
                    System.out.println("讀准備就緒,開始讀.......................");
                    SocketChannel channel = (SocketChannel)key.channel();
                    System.out.println("客戶端的數據如下:");
                    
                    int readLen = 0;
                    bf.clear();
                    StringBuffer sb = new StringBuffer();
                    while((readLen=channel.read(bf))>0){
                        sb.append(new String(bf.array()));
                        bf.clear();
                    }
                    if(-1==readLen){
                        channel.close();
                    }
                    channel.write(ByteBuffer.wrap(("客戶端,你傳過來的數據是:"+sb.toString()).getBytes()));
                }
                it.remove();
            }
        }
    }
    private static void init() throws Exception{
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }
}

  客戶端:

package com.nio.client;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOClient {
    private static Selector selector;
    public static void main(String[]args) throws Exception{
        selector = Selector.open();
        SocketChannel sc = SocketChannel.open();
        sc.configureBlocking(false);
        sc.connect(new InetSocketAddress("127.0.0.1",8080));
        sc.register(selector,SelectionKey.OP_READ);
        
        ByteBuffer bf = ByteBuffer.allocate(1024);
        bf.put("Hi,server,i'm client".getBytes());
        
        
        if(sc.finishConnect()){
            bf.flip();
            while(bf.hasRemaining()){
                sc.write(bf);
            }
            
            while(true){
                selector.select();
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while(it.hasNext()){
                    SelectionKey key = it.next();
                    
                    if(key.isReadable()){
                        bf.clear();
                        SocketChannel othersc  = (SocketChannel)key.channel();
                        othersc.read(bf);
                        System.out.println("服務端返回的數據:"+new String(bf.array()));
                    }
                }
                selector.selectedKeys().clear();
            }
        }
    }
}

  服務端運行結果:

 

   客戶端運行結果:

  這邊我們可以看到,客戶端輸出了兩次,筆者調試的時候發現,服務端只往客戶端寫過一次數據,但是客戶端卻打印了兩次數據,而且兩次的數據不一樣,挺詭異的!然后我就各種查,折磨了我一夜,今早一來,又想着怎么解決問題,不經意間發現了一篇文章:java nio使用的是水平觸發還是邊緣觸發?,文章中指出Nio的Selector.select()是“水平觸發”(也叫“條件觸發”),只要條件一直滿足,那么就會一直觸發,至此我如醍醐灌頂:是不是我通道里面的數據第一次沒有讀取干凈?導致客戶端觸發了多次讀取?后來驗證之后,發現確實是這個問題,讀者可以看我服務器端的代碼,我返回的是數據字節數是:"服務端返回的數據:".length()+bf.array().length=26+1024,而客戶端只是將這個數據讀入1024大小的ByteBuffer中,還有26字節沒有讀取干凈,所以就觸發了第二次的讀事件!!!

  既然問題找到了,現在就是要解決如何將通道內的數據讀取干凈了,修改之后的代碼如下, 特別注意紅色部分:

  服務端:

  

package com.nio.server;

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;

public class NIOServer {
    private static Selector selector;
    private static ServerSocketChannel serverSocketChannel;
    private static ByteBuffer bf = ByteBuffer.allocate(1024);
    public static void main(String[] args) throws Exception{
        init();
        while(true){
            selector.select();
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while(it.hasNext()){
                SelectionKey key = it.next();
                if(key.isAcceptable()){
                    System.out.println("連接准備就緒");
                    ServerSocketChannel server = (ServerSocketChannel)key.channel();
                    System.out.println("等待客戶端連接中........................");
                    SocketChannel channel = server.accept();
                    channel.configureBlocking(false);
                    channel.register(selector,SelectionKey.OP_READ);
                }
                else if(key.isReadable()){
                    System.out.println("讀准備就緒,開始讀.......................");
                    SocketChannel channel = (SocketChannel)key.channel();
                    System.out.println("客戶端的數據如下:");
                    
                    int readLen = 0;
                    bf.clear();
                    StringBuffer sb = new StringBuffer();
                    while((readLen=channel.read(bf))>0){
                       bf.flip(); byte [] temp = new byte[readLen];
                        bf.get(temp,0,readLen);
                        sb.append(new String(temp)); bf.clear();
                    }
                    if(-1==readLen){
                        channel.close();
                    }
            System.out.println(sb.toString()); channel.write(ByteBuffer.wrap((
"客戶端,你傳過來的數據是:"+sb.toString()).getBytes())); } it.remove(); } } } private static void init() throws Exception{ selector = Selector.open(); serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(8080)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } }

  客戶端:

  

package com.nio.client;

import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOClient {
    private static Selector selector;
    public static void main(String[]args) throws Exception{
        selector = Selector.open();
        SocketChannel sc = SocketChannel.open();
        sc.configureBlocking(false);
        sc.connect(new InetSocketAddress("127.0.0.1",8080));
        sc.register(selector,SelectionKey.OP_READ);
        
        ByteBuffer bf = ByteBuffer.allocate(1024);
        bf.put("Hi,server,i'm client".getBytes());
        
        
        if(sc.finishConnect()){
            bf.flip();
            while(bf.hasRemaining()){
                sc.write(bf);
            }
            
            while(true){
                selector.select();
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while(it.hasNext()){
                    SelectionKey key = it.next();
                    
                    
                    if(key.isReadable()){
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        bf.clear();
                        SocketChannel othersc  = (SocketChannel)key.channel();
                        while(othersc.read(bf)>0){
                            bf.flip();
                            while(bf.hasRemaining()){ bos.write(bf.get()); } bf.clear(); };
                        System.out.println("服務端返回的數據:"+bos.toString());
                    }
                }
                selector.selectedKeys().clear();
            }
        }
    }
}

  客戶端的輸出:

   

三、參考鏈接

       https://www.zhihu.com/question/22524908

四、聯系本人

  為方便沒有博客園賬號的讀者交流,特意建立一個企鵝群(純公益,非利益相關),讀者如果有對博文不明之處,歡迎加群交流:261746360,小杜比亞-博客園。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM