Java NIO詳解

    從事網絡編程的應該都知道傳輸層的主要協議是TCP/UDP,關於二者的區別網絡上有好多資料這裏就很少說介紹,然而數據的傳輸過程大都有個IO操做,所以就衍生出了BIO,NIO,AIO三大模型,關於這三者的區別本系列博客有介紹,歡迎你們參考並指正,本篇主要寫基於Java實現的NIO編程模型的一些使用細節,歡迎正在使用NIO編程的朋友們出來討論,但願起到一個拋磚引玉的效果。java


    最近一直在看mina與netty的源代碼,從中學習到了好多編程技巧與編程方式,因而花了點時間研究了NIO的Java層面的調用,本篇主要以代碼爲主:編程

首先我須要實現的是:服務器

1:服務端啓動監聽socket機制網絡

2:客戶端發起與服務端創建鏈接,並註冊寫事件與讀事件發送心跳包機制socket

3:服務端創建鏈接後註冊讀事件讀取心跳包並註冊寫事件迴應心跳包ide


下面先貼出服務端的代碼:學習

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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;

public class NIOServer {
	
	/*標識數字*/ 
    private  int flag = 0;  
    /*緩衝區大小*/ 
    private  int BLOCK = 4096;  
    /*接受數據緩衝區*/ 
    private  ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);  
    /*發送數據緩衝區*/ 
    private  ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);  
    private  Selector selector;  
    private boolean connecFa=true;
    
    public NIOServer() throws IOException{
    	this(8080);
    }
    public NIOServer(int port) throws IOException {  
        // 打開服務器套接字通道  
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
        // 服務器配置爲非阻塞  
        serverSocketChannel.configureBlocking(false);  
        // 檢索與此通道關聯的服務器套接字  
        ServerSocket serverSocket = serverSocketChannel.socket();  
        // 進行服務的綁定  
        serverSocket.bind(new InetSocketAddress(port));  
        // 經過open()方法找到Selector  
        selector = Selector.open();  
        // 註冊到selector,等待鏈接  
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  
        System.out.println("Server Start----:"+port);  
    }  
    
    // 監聽  
    private void listen() throws IOException {  
        while (connecFa) {  
            // 選擇一組鍵,而且相應的通道已經打開  
            selector.select();  
            // 返回此選擇器的已選擇鍵集。  
            Set<SelectionKey> selectionKeys = selector.selectedKeys();  
            Iterator<SelectionKey> iterator = selectionKeys.iterator();  
            while (iterator.hasNext()) {          
                SelectionKey selectionKey = iterator.next(); 
                System.out.println("selectionKey.interestOps()\t"+selectionKey.interestOps());
                //iterator.remove();  
                handleKey(selectionKey);  
                selectionKeys.clear();
            }  
        }  
    }  
    
    // 處理請求  
    private void handleKey( SelectionKey selectionKey) throws IOException {  
        // 接受請求  
        ServerSocketChannel server = null;  
        SocketChannel client = null;  
        String receiveText;  
        String sendText;  
        int count=0;  
        // 測試此鍵的通道是否已準備好接受新的套接字鏈接。  
        if (selectionKey.isAcceptable()) {  
            // 返回爲之建立此鍵的通道。  
            server = (ServerSocketChannel) selectionKey.channel();  
            // 接受到此通道套接字的鏈接。  
            // 此方法返回的套接字通道(若是有)將處於阻塞模式。  
            client = server.accept();  
			client.configureBlocking(false);  
            // 註冊到selector,等待鏈接  
			client.register(selector, SelectionKey.OP_READ);  
        } else if (selectionKey.isReadable()) {  
        	System.err.println("服務端開始讀操做==="+selectionKey.isReadable());  
        	System.err.println("selectionKey.attachment()==="+selectionKey.attachment());  
            // 返回爲之建立此鍵的通道。  
            client = (SocketChannel) selectionKey.channel();  
            //將緩衝區清空以備下次讀取  
            receivebuffer.clear();  
            //讀取服務器發送來的數據到緩衝區中  
            count = client.read(receivebuffer);   
            if (count > 0) {  
                receiveText = new String( receivebuffer.array(),0,count);  
                System.out.println("服務器端接受客戶端數據--:"+receiveText);  
                /*if(selectionKey.interestOps()==1){
                	selectionKey.interestOps(selectionKey.interestOps() | (SelectionKey.OP_WRITE));
                	
                }*/
                client.register(selector, SelectionKey.OP_WRITE); 
            }  

        } else if (selectionKey.isWritable()&&selectionKey.isValid()) {  
        	System.err.println("客戶端開始寫操做==="+selectionKey.isWritable());  
        	System.err.println("selectionKey.attachment()==="+selectionKey.attachment());  
            //將緩衝區清空以備下次寫入  
            sendbuffer.clear();  
            // 返回爲之建立此鍵的通道。  
            client = (SocketChannel) selectionKey.channel();  
            sendText="message from server--" + flag++;  

            //向緩衝區中輸入數據  
            sendbuffer.put(sendText.getBytes());  
             //將緩衝區各標誌復位,由於向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位  
            sendbuffer.flip();  
            //輸出到通道  
            client.write(sendbuffer);  
            System.out.println("服務器端向客戶端發送數據--:"+sendText);  
            
           /* if(!"heart".equals(selectionKey.attachment())){
            	selectionKey.interestOps(selectionKey.interestOps() & (~SelectionKey.OP_WRITE));
            }*/
          
        }  
    }  

	/**
	 * @throws IOException 
	 * @param args
	 * @throws  
	 */
	public static void main(String[] args) throws IOException   {
		// TODO Auto-generated method stub
		  int port = 8080;  
	      NIOServer server = new NIOServer(port);  
	      server.listen();  
	}

}

如下是客戶端的代碼:測試

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.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOClient {
	/*標識數字*/ 
    private static int flag = 0;  
    /*緩衝區大小*/ 
    private static int BLOCK = 4096;  
    /*接受數據緩衝區*/ 
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);  
    /*發送數據緩衝區*/ 
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);  
    /*服務器端地址*/ 
    private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(  
            "localhost", 1111);  
 
    public static void main(String[] args) throws IOException {  
        // TODO Auto-generated method stub  
    	int read=0;
    	int write=0;
        // 打開socket通道  
        SocketChannel socketChannel = SocketChannel.open();  
        // 設置爲非阻塞方式  
        socketChannel.configureBlocking(false);  
        // 打開選擇器  
        Selector selector = Selector.open();  
        // 註冊鏈接服務端socket動做  
        socketChannel.register(selector, SelectionKey.OP_CONNECT);  
        // 鏈接  
        socketChannel.connect(SERVER_ADDRESS);  
        // 分配緩衝區大小內存  
          
        Set<SelectionKey> selectionKeys;  
        Iterator<SelectionKey> iterator;  
        SelectionKey selectionKey;  
        SocketChannel client;  
        String receiveText;  
        String sendText;  
        int count=0;  
 
        while (true) {  
            //選擇一組鍵,其相應的通道已爲 I/O 操做準備就緒。  
            //此方法執行處於阻塞模式的選擇操做。  
            selector.select();  
            //返回此選擇器的已選擇鍵集。  
            selectionKeys = selector.selectedKeys();  
            //System.out.println(selectionKeys.size());  
            iterator = selectionKeys.iterator();  
            while (iterator.hasNext()) {  
                selectionKey = iterator.next();  
                System.out.println("selectionKey.interestOps()\t"+selectionKey.interestOps());

                if (selectionKey.isConnectable()) {  
                	System.err.println("selectionKey.isAcceptable()==="+selectionKey.isAcceptable());  
                	System.err.println("selectionKey.isConnectable()==="+selectionKey.isConnectable());  
                    client = (SocketChannel) selectionKey.channel();  
                    client.configureBlocking(false);
                    // 判斷此通道上是否正在進行鏈接操做。  
                    // 完成套接字通道的鏈接過程。  
                    if (client.isConnectionPending()) {  
                        client.finishConnect();  
                        System.out.println("完成鏈接!");  
                        sendbuffer.clear();  
                        sendbuffer.put("Hello,Server".getBytes());  
                        sendbuffer.flip();  
                        client.write(sendbuffer);  
                    }  
                    //heart能夠理解爲事件類型在讀寫的時候能夠經過selectionKey.attachment()得到註冊的值
                    client.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE,"heart");  
                } else if (selectionKey.isReadable()) {  
                	System.err.println("客戶端開始讀操做==="+selectionKey.isReadable());  
                    client = (SocketChannel) selectionKey.channel();  
                    //將緩衝區清空以備下次讀取  
                    receivebuffer.clear();  
                    //讀取服務器發送來的數據到緩衝區中  
                    count=client.read(receivebuffer);  
                    if(count>0){  
                        receiveText = new String( receivebuffer.array(),0,count);  
                        System.out.println("客戶端接受服務器端數據--:"+receiveText);  
                    }  
 
                } else if (selectionKey.isWritable()) {  
                	
                	System.err.println("客戶端開始寫操做==="+selectionKey.isWritable());  
                    sendbuffer.clear();  
                    client = (SocketChannel) selectionKey.channel();  
                    sendText = "發送心跳包" + (flag++);  
                    if("heart".equals(selectionKey.attachment())){
                    	try {
							Thread.sleep(5000);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
                    	 sendText = "發送心跳包" + (flag++);  
                    }
                    sendbuffer.put(sendText.getBytes());  
                     //將緩衝區各標誌復位,由於向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位  
                    sendbuffer.flip();  
                    client.write(sendbuffer);  
                    System.out.println("客戶端向服務器端發送數據--:"+sendText);  
                  
                }  
                iterator.remove();
            }  
        }  
    }  
}

1:運行服務端this

2:運行客戶端spa


發現服務端寫事件死循環,在這裏問題就來了,我這邊只想每次收到心跳包纔回應一個寫事件而不是註冊了寫事件後就陷入死循環狀態,分析問題應該是在寫事件上,那是何時才能觸發寫事件呢?

總結以下:

    寫操做的就緒條件爲底層緩衝區有空閒空間,而寫緩衝區絕大部分時間都是有空閒空間的,因此當註冊寫事件後,寫操做一直是就緒的,選擇處理線程會佔用整個CPU資源。因此,只有當確實有數據要寫時再註冊寫操做,並在寫完之後立刻取消註冊。


解決方法:思路是每次寫完以後取消寫事件的註冊,在寫玩以後增長以下代碼:

selectionKey.interestOps(selectionKey.interestOps() & (~SelectionKey.OP_WRITE));

固然讀完以後要從新註冊寫事件,代碼以下:

if(selectionKey.interestOps()==1){
                	selectionKey.interestOps(selectionKey.interestOps() | (SelectionKey.OP_WRITE));
                	
                }

以上代碼均在服務端實現的註釋部分存在,經過這個實踐完全的瞭解了Java NIO基於事件驅動模型的編程思想與處理細節,不過這裏有點想不通的是爲何對寫事件在操做層面上沒有一個好的解決方案,我想應該跟操做系統的底層實現有關係。

本文出自 「陳硯羲」 博客,請務必保留此出處http://chenyanxi.blog.51cto.com/4599355/1540353

相關文章
相關標籤/搜索