`
weihe6666
  • 浏览: 439059 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Java NIO

 
阅读更多
java 网络编程中,不可避免的要谈论NIO,这篇文章就来谈谈对NIO的认识。

IO传统意义上分为File IO,StreamIO,这两个分别对应文件读写和Socket,文件读写IO目前只有阻塞进行读写,而socket由于Selector和ByteBuffer构成了非阻塞IO,但是由于NIO需要单独的线程去遍历selectoryKey,导致线程资源一致被占用,所以AIO出现了,利用监听回调,来取代遍历selectorykey。


一、阻塞IO网络通信

在传统的阻塞IO模型中,大多是通过多线程处理不同的socket,尤其是在服务端,accept会创建不同的channel,然后利用独立的线程进行处理具体的业务逻辑。由于thread的创建和切换需要占用系统资源,当thread数量比较少时,这种模式还是比较好的,但是当thead量比较大时,就会出现许多问题。一个thread 栈linux默认是8M大小,如果客户端通过DDOS进行攻击,建立多个connect,多导致服务端内存占用非常大,导致服务器挂掉。


下面写一个阻塞IO通信模型:
客户端:
public class Client {

  public static final String IP_ADDR = "localhost"; //  服务端ip地址,这里采用本地地址
  public static final int PORT = 1978;

  public static void main(String[] args) {

         while (true) {  
        	Socket socket = null;
        	try {
        		//创建一个流套接字并将其连接到指定主机上的指定端口号
	        	socket = new Socket(IP_ADDR, PORT);  
	              
	            //读取服务器端数据  
	            DataInputStream input = new DataInputStream(socket.getInputStream());  
	            //向服务器端发送数据  
	            DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
	            System.out.print("请输入: \t");  
	            String str = new BufferedReader(new InputStreamReader(System.in)).readLine();  
	            out.writeUTF(str);  
	              
	            String ret = input.readUTF();   
	            System.out.println("服务器端返回过来的是: " + ret);  
	            // 如接收到 "OK" 则断开连接  
	            if ("OK".equals(ret)) {  
	                System.out.println("客户端将关闭连接");  
	                Thread.sleep(500);  
	                break;  
	            }  
	            
	            out.close();
	            input.close();
        	} catch (Exception e) {
        		System.out.println("客户端异常:" + e.getMessage()); 
        	} finally {
        		if (socket != null) {
        			try {
						socket.close();
					} catch (IOException e) {
						socket = null; 
						System.out.println("客户端 finally 异常:" + e.getMessage()); 
					}
        		}
        	}
        }  
    }
}

}


上面代码是简单的Client建立socket通道,同服务端通信的例子,这里的通道是一个短链接,每次都需要建立连接,发送数据,然后在关闭。

Socket是一个网络IO模块,分为input,output,这里的读和写都是阻塞的,等待服务端有返回时,才会有响应,这种模式就需要占用多个线程资源。

看一下服务端代码:
public class Service {
	public static final int PORT = 12345;//监听的端口号   
	
    public static void main(String[] args) {  
        System.out.println("服务器启动...\n");  
        Service server = new Service();  
        server.init();  
    }  
  
    public void init() {  
        try {  
            ServerSocket serverSocket = new ServerSocket(PORT);  
            while (true) {  
                // 一旦有堵塞, 则表示服务器与客户端获得了连接  
                Socket client = serverSocket.accept();  
                // 处理这次连接  
                new HandlerThread(client);  
            }  
        } catch (Exception e) {  
            System.out.println("服务器异常: " + e.getMessage());  
        }  
    }  
  
    private class HandlerThread implements Runnable {  
        private Socket socket;  
        public HandlerThread(Socket client) {  
            socket = client;  
            new Thread(this).start();  
        }  
  
        public void run() {  
            try {  
                // 读取客户端数据  
                DataInputStream input = new DataInputStream(socket.getInputStream());
                String clientInputStr = input.readUTF();//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException
                // 处理客户端数据  
                System.out.println("客户端发过来的内容:" + clientInputStr);  
  
                // 向客户端回复信息  
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
                System.out.print("请输入:\t");  
                // 发送键盘输入的一行  
                String s = new BufferedReader(new InputStreamReader(System.in)).readLine();  
                out.writeUTF(s);  
                
                out.close();  
                input.close();  
            } catch (Exception e) {  
                System.out.println("服务器 run 异常: " + e.getMessage());  
            } finally {  
                if (socket != null) {  
                    try {  
                        socket.close();  
                    } catch (Exception e) {  
                        socket = null;  
                        System.out.println("服务端 finally 异常:" + e.getMessage());  
                    }  
                }  
            } 
        }  
    }  
}  


这里每次收到一个accept链接请求,都会创建一个socket,然后单独一个线程处理socket请求内容。

Socket client = serverSocket.accept();
这句是阻塞的,等待客户端的链接。



二、NIO使用实例

上面分析了传统的Socket IO通信模型,对于高线程消耗的阻塞模式的IO,采用NIO可以解决这些问题,单个的线程处理所有socket请求,以及同一个线程可以处理不同端口socket请求。

但是NIO的问题也相当明显,线程需要一致占用cpu资源去循环查找selector的key值,处理已经准备好的channel。

客户端代码:
public class TCPClient{
	
	public static final String IP_ADDR = "localhost";//服务器地址 
	public static final int PORT_ONE = 1978;
	public static final int PORT = 12345;//服务器端口号  
	
  // 信道选择器
  private Selector selector;
  
  // 与服务器通信的信道
  SocketChannel socketChannel;
  
  // 要连接的服务器Ip地址
  private String hostIp;
  
  // 要连接的远程服务器在监听的端口
  private int hostListenningPort;
  
  public TCPClient(String HostIp,int HostListenningPort, Selector selector)throws IOException{
    this.hostIp=HostIp;
    this.hostListenningPort=HostListenningPort;   
    
    initialize(selector);
  }
  
  /**
   * 初始化
   * @throws IOException
   */
  private void initialize(Selector selector) throws IOException{
    // 打开监听信道并设置为非阻塞模式
    socketChannel = SocketChannel.open(new InetSocketAddress(hostIp, hostListenningPort));
    socketChannel.configureBlocking(false);
    
    // 打开并注册选择器到信道
    socketChannel.register(selector, SelectionKey.OP_READ);
    
    // 启动读取线程
//    new TCPClientReadThread(selector);
  }
  
  /**
   * 发送字符串到服务器
   * @param message
   * @throws IOException
   */
  public void sendMsg(String message) throws IOException{
    ByteBuffer writeBuffer=ByteBuffer.wrap(message.getBytes("UTF-16"));
    socketChannel.write(writeBuffer);
  }
  
  public Selector getSelector(){
	  return selector;
  }
  
  public static void main(String[] args) throws IOException, Throwable{
	    // 打开并注册选择器到信道
    Selector selector = Selector.open();
    TCPClient clientOne = new TCPClient(IP_ADDR,PORT_ONE, selector);
    Thread.sleep(5000); // 段时间内多次创建链接,会出现Address already in use
    TCPClient clientTwo = new TCPClient(IP_ADDR, PORT, selector);
    // 启动读取线程
    new TCPClientReadThread(selector);
    
    clientOne.sendMsg("你好!Nio one!醉里挑灯看剑,梦回吹角连营 我是端口1978");
    clientTwo.sendMsg("你好!Nio tow! 我是端口12345");
  }
}


public class TCPClientReadThread implements Runnable{
  private Selector selector;
  
  public TCPClientReadThread(Selector selector){
    this.selector=selector;
    
    new Thread(this).start();
  }
  
  public void run() {
    try {
      while (selector.select() > 0) {
        // 遍历每个有可用IO操作Channel对应的SelectionKey
        for (SelectionKey sk : selector.selectedKeys()) {
          
          // 如果该SelectionKey对应的Channel中有可读的数据
          if (sk.isReadable()) {
            // 使用NIO读取Channel中的数据
            SocketChannel sc = (SocketChannel) sk.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            sc.read(buffer);
            buffer.flip();
            
            // 将字节转化为为UTF-16的字符串   
            String receivedString=Charset.forName("UTF-16").newDecoder().decode(buffer).toString();
            
            // 控制台打印出来
            System.out.println("接收到来自服务器"+sc.socket().getRemoteSocketAddress()+"的信息:"+receivedString);
            
            // 为下一次读取作准备
            sk.interestOps(SelectionKey.OP_READ);
          }
          
          // 删除正在处理的SelectionKey
          selector.selectedKeys().remove(sk);
        }
      }
    } catch (IOException ex) {
      ex.printStackTrace();
    }   
  }
}


这里建立了两个socketchannel,通过不同的端口连接到同一个服务端,利用一个线程处理这两个socket数据,通过selector监听不同的socket。


服务端代码:
public class TCPServer{
  // 缓冲区大小
  private static final int BufferSize = 1024;
  
  // 超时时间,单位毫秒
  private static final int TimeOut = 3000;
  
  // 本地监听端口
  private static final int ListenPort = 1978;
  private static final int listenPortTwo = 12345;
  
  public static void Testmain(String[] args) throws IOException{
    // 创建选择器
    Selector selector = Selector.open();
    
    TCPChannel.openChannel(selector, ListenPort, SelectionKey.OP_ACCEPT);
    TCPChannel.openChannel(selector, listenPortTwo, SelectionKey.OP_ACCEPT);
    
    // 创建一个处理协议的实现类,由它来具体操作
    TCPProtocol protocol=new TCPProtocolImpl(BufferSize);
     
    // 主线程进行数据的操作
    // 反复循环,等待IO
    while(true){
      // 等待某信道就绪(或超时)
      if(selector.select(TimeOut)==0){ // block
        System.out.print("独自等待.");
        continue;
      }
      
      // 取得迭代器.selectedKeys()中包含了每个准备好某一I/O操作的信道的SelectionKey
      Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
      
      while(keyIter.hasNext()){
        SelectionKey key=keyIter.next();
        
        try{
          if(key.isAcceptable()){
            // 有客户端连接请求时
            protocol.handleAccept(key);
          }
          
          if(key.isReadable()){
            // 从客户端读取数据
            protocol.handleRead(key);
          }
          
          if(key.isValid() && key.isWritable()){
            // 客户端可写时
            protocol.handleWrite(key);
          }
        }
        catch(IOException ex){
          // 出现IO异常(如客户端断开连接)时移除处理过的键
          keyIter.remove();
          continue;
        }
        
        // 移除处理过的键
        keyIter.remove();
      }
    }
  }
}



这里打开多个端口监听,在主线程里通过selector进行监听处理。

public class TCPProtocolImpl implements TCPProtocol{
  private int bufferSize;
  
  public TCPProtocolImpl(int bufferSize){
    this.bufferSize=bufferSize;
  }

  // 建立通道,并注册到selector进行监听
  public void handleAccept(SelectionKey key) throws IOException {
    SocketChannel clientChannel=((ServerSocketChannel)key.channel()).accept();
    clientChannel.configureBlocking(false);
    clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
  }

  public void handleRead(SelectionKey key) throws IOException {
    // 获得与客户端通信的信道
    SocketChannel clientChannel=(SocketChannel)key.channel();
    
    // 得到并清空缓冲区
    ByteBuffer buffer=(ByteBuffer)key.attachment();
    buffer.clear();
    
    // 读取信息获得读取的字节数
    long bytesRead=clientChannel.read(buffer);
    
    if(bytesRead==-1){
      // 没有读取到内容的情况
      clientChannel.close();
    } else {
      // 将缓冲区准备为数据传出状态
      buffer.flip();
      
      // 将字节转化为为UTF-16的字符串   
      String receivedString=Charset.forName("UTF-16").newDecoder().decode(buffer).toString();
      
      // 控制台打印出来
      System.out.println("接收到来自"+clientChannel.socket().getRemoteSocketAddress()+"的信息:"+receivedString);
      
      // 准备发送的文本
      String sendString="你好,客户端. @"+new Date().toString()+",已经收到你的信息"+receivedString;
      buffer=ByteBuffer.wrap(sendString.getBytes("UTF-16"));
      clientChannel.write(buffer);
      
      // 设置为下一次读取或是写入做准备
      key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
    }
  }

  public void handleWrite(SelectionKey key) throws IOException {
    // do nothing
  }
}

public class TCPChannel {	
	
	public static void openChannel(Selector selector, int port, int selectorKey) throws IOException {
		// 打开监听信道
	    ServerSocketChannel listenerChannel = ServerSocketChannel.open();
	    
	    // 与本地端口绑定
	    listenerChannel.socket().bind(new InetSocketAddress(port));
	    
	    // 设置为非阻塞模式
	    listenerChannel.configureBlocking(false);
	    
	    // 将选择器绑定到监听信道,只有非阻塞信道才可以注册选择器.并在注册过程中指出该信道可以进行Accept操作
	    listenerChannel.register(selector, selectorKey);
	}

}


NIO里关键要理解selector的工作原理,以及底层实现的方式,与linux的epoll,select,poll的区别。
分享到:
评论

相关推荐

    java NIO和java并发编程的书籍

    java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java...

    JavaNIO chm帮助文档

    Java NIO系列教程(一) Java NIO 概述 Java NIO系列教程(二) Channel Java NIO系列教程(三) Buffer Java NIO系列教程(四) Scatter/Gather Java NIO系列教程(五) 通道之间的数据传输 Java NIO系列教程(六)...

    Java NIO英文高清原版

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java平台中用于替代标准I/O(BIO)模型的一种新机制。NIO在Java 1.4版本引入,提供了更高效的数据处理和通道通信方式,特别适用于高并发、大数据...

    java NIO.zip

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java标准库提供的一种替代传统的I/O模型的新技术。自Java 1.4版本引入NIO后,它为Java开发者提供了更高效的数据传输方式,尤其是在处理大量并发...

    java nio 包读取超大数据文件

    ### Java NIO 处理超大数据文件的知识点详解 #### 一、Java NIO简介 Java NIO(New IO)是Java平台上的新输入/输出流API,它提供了与传统IO(即Java IO)不同的数据处理方式。NIO在Java 1.4版本引入,并在后续版本...

    java NIO 视频教程

    Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。 Java NIO: Channels and Buffers(通道和缓冲区) 标准的IO基于字节流和字符流进行操作的,...

    Java NIO 中文 Java NIO 中文 Java NIO 中文文档

    Java NIO 深入探讨了 1.4 版的 I/O 新特性,并告诉您如何使用这些特性来极大地提升您所写的 Java 代码的执行效率。这本小册子就程序员所面临的有代表性的 I/O 问题作了详尽阐述,并讲解了 如何才能充分利用新的 I/O ...

    java NIO技巧及原理

    Java NIO(New Input/Output)是Java标准库提供的一种I/O模型,它与传统的 Blocking I/O(IO)相比,提供了更加高效的数据传输方式。在Java NIO中,"新"主要体现在非阻塞和多路复用这两个特性上,这使得NIO更适合于...

    一个java NIO的例子

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java标准库提供的一种替代传统I/O模型的新技术。在传统的Java IO模型中,读写操作是阻塞的,即当调用read或write方法时,线程会等待数据准备好或...

    java NIO实例

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java从1.4版本开始引入的一种新的I/O模型,它为Java应用程序提供了更高效的数据传输方式。传统的Java I/O模型(BIO)在处理大量并发连接时效率较...

    java nio入门学习,两个pdf

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java标准库提供的一种替代传统I/O模型的新技术。在Java 1.4版本中引入,NIO提供了一种全新的I/O编程方式,使得Java开发者能够更高效地处理I/O操作...

    java nio 实现socket

    ### Java NIO 实现Socket通信详解 #### 一、NIO与传统IO的区别及优势 在探讨如何使用Java NIO实现Socket通信之前,我们需要先理解NIO(Non-blocking I/O,非阻塞I/O)与传统阻塞I/O之间的区别。 **传统阻塞I/O...

    Java NIO测试示例

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java从1.4版本开始引入的一种新的I/O模型,它为Java应用程序提供了更高效、灵活的I/O操作方式。NIO与传统的 Blocking I/O(阻塞I/O)模式相比,...

Global site tag (gtag.js) - Google Analytics