Java NIO 学习demo

来源:互联网 发布:c语言fopen函数的用法 编辑:程序博客网 时间:2024/05/23 16:41
我们都知道TCP是面向连接的传输层协议,一个socket必定会有绑定一个连接,在普通的BIO(阻塞式IO)中,需要有三次握手,然后一般的socket编程就是这样的形式。

Socket服务器端流程如下:加载套接字->创建监听的套接字->绑定套接字->监听套接字->处理客户端相关请求。

Socket客户端同样需要先加载套接字,然后创建套接字,不过之后不用绑定和监听了,而是直接连接服务器,发送相关请求。

 

       他们一直就占用这个连接,如果有信息发送,那么就响应,否则就一直阻塞着。如果有多连接,那么就要使用多线程,一个线程处理一个连接,在连接还少的情况下,是允许的,但如果同时处理的连接过多比如说1000,那么在win平台上就会遇到瓶颈了如果2000,那么在linux上就遇到瓶颈了,因为在不同的平台上每一个进程能够创建的线程数是有限度的,并且过多的线程必将会引起系统对线程调度的效率问题,再怎么也要保证线程优先队列,阻塞队列;假设一千个线程,一个线程最少一兆的栈大小,对内存也是一个很大的消耗。

       总之阻塞式的IO是:一连接<一一一>一线程

 

       然后出现了NIO,在java1.4引入了java.nio包,java new I/O。引入了操作系统中常用的缓冲区和通道等概念。

 

       缓冲区: 在操作系统中缓冲区是为了解决CPU的计算速度和外设输入输出速度不匹配的问题,因为外设太慢了,如果没有缓冲区,那么CPU在外设输入的时候就要一直等着,就会造成CPU处理效率的低下,引入了缓冲之后,外设直接把数据放到缓冲中,当数据传输完成之后,给CPU一个中断信号,通知CPU:“我的数据传完了,你自己从缓冲里面去取吧”。如果是输出也是一样的道理。

       通道: 那么通道用来做什么呢?其实从他的名字就可以看出,它就是一条通道,您想传递出去的数据被放置在缓冲区中,然后缓冲区中怎么从哪里传输出去呢?或者外设怎么把数据传输到缓冲中呢?这里就要用到通道。它可以进一步的减少CPU的干预,同时更有效率的提高整个系统的资源利用率,例如当CPU要完成一组相关的读操作时,只需要向I/O通道发送一条指令,以给出其要执行的通道程序的首地址和要访问的设备,通道执行通道程序便可以完成CPU指定的I/O任务。

      选择器: 另外一项创新是选择器,当我们使用通道的时候也许通道没有准备好,或者有了新的请求过来,或者线程遇到了阻塞,而选择器恰恰可以帮助CPU了解到这些信息,但前提是将这个通道注册到了这个选择器。

 

 

下面一个例子是我看过的一个讲述的很贴切的例子:   

一辆从 A 开往 B 的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?   

1. 司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。 ( 类似阻塞式 )   

2. 每个人告诉售票员自己的目的地,然后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 ( 类似非阻塞 )     

很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是 CPU 。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客 ( 线程 ) 都在睡觉 ( 休眠 ) ,只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。  

 

 

在非阻塞式IO中实现的是:一请求<一一一>一线程

 

     下面这个例子实现了一个线程监听两个ServerSocket,只有等到请求的时候才会有处理。

Server

 

[java] view plaincopy
  1. package cn.vicky.channel;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.InetSocketAddress;  
  5. import java.nio.ByteBuffer;  
  6. import java.nio.channels.SelectionKey;  
  7. import java.nio.channels.Selector;  
  8. import java.nio.channels.ServerSocketChannel;  
  9. import java.nio.channels.SocketChannel;  
  10. import java.nio.channels.spi.SelectorProvider;  
  11. import java.util.Iterator;  
  12.   
  13. /**    
  14.  * TCP/IP的NIO非阻塞方式   
  15.  * 服务器端   
  16.  * */  
  17. public class Server implements Runnable {  
  18.   
  19.     //第一个端口     
  20.     private Integer port1 = 8099;  
  21.     //第二个端口     
  22.     private Integer port2 = 9099;  
  23.     //第一个服务器通道 服务A     
  24.     private ServerSocketChannel serversocket1;  
  25.     //第二个服务器通道 服务B     
  26.     private ServerSocketChannel serversocket2;  
  27.     //连接1     
  28.     private SocketChannel clientchannel1;  
  29.     //连接2     
  30.     private SocketChannel clientchannel2;  
  31.   
  32.     //选择器,主要用来监控各个通道的事件     
  33.     private Selector selector;  
  34.       
  35.     //缓冲区     
  36.     private ByteBuffer buf = ByteBuffer.allocate(512);  
  37.       
  38.     public Server() {  
  39.         init();  
  40.     }  
  41.   
  42.     /**   
  43.      * 这个method的作用 
  44.      * 1:是初始化选择器   
  45.      * 2:打开两个通道   
  46.      * 3:给通道上绑定一个socket   
  47.      * 4:将选择器注册到通道上   
  48.      * */  
  49.     public void init() {  
  50.         try {  
  51.             //创建选择器     
  52.             this.selector = SelectorProvider.provider().openSelector();  
  53.             //打开第一个服务器通道     
  54.             this.serversocket1 = ServerSocketChannel.open();  
  55.             //告诉程序现在不是阻塞方式的     
  56.             this.serversocket1.configureBlocking(false);  
  57.             //获取现在与该通道关联的套接字     
  58.             this.serversocket1.socket().bind(new InetSocketAddress("localhost"this.port1));  
  59.             //将选择器注册到通道上,返回一个选择键     
  60.             //OP_ACCEPT用于套接字接受操作的操作集位     
  61.             this.serversocket1.register(this.selector, SelectionKey.OP_ACCEPT);  
  62.   
  63.             //然后初始化第二个服务端     
  64.             this.serversocket2 = ServerSocketChannel.open();  
  65.             this.serversocket2.configureBlocking(false);  
  66.             this.serversocket2.socket().bind(new InetSocketAddress("localhost"this.port2));  
  67.             this.serversocket2.register(this.selector, SelectionKey.OP_ACCEPT);  
  68.   
  69.         } catch (Exception e) {  
  70.             e.printStackTrace();  
  71.         }  
  72.   
  73.     }  
  74.   
  75.     /**   
  76.      * 这个方法是连接   
  77.      * 客户端连接服务器   
  78.      * @throws IOException    
  79.      * */  
  80.     public void accept(SelectionKey key) throws IOException {  
  81.         ServerSocketChannel server = (ServerSocketChannel) key.channel();  
  82.         if (server.equals(serversocket1)) {  
  83.             clientchannel1 = server.accept();  
  84.             clientchannel1.configureBlocking(false);  
  85.             //OP_READ用于读取操作的操作集位     
  86.             clientchannel1.register(this.selector, SelectionKey.OP_READ);  
  87.         } else {  
  88.             clientchannel2 = server.accept();  
  89.             clientchannel2.configureBlocking(false);  
  90.             //OP_READ用于读取操作的操作集位     
  91.             clientchannel2.register(this.selector, SelectionKey.OP_READ);  
  92.         }  
  93.     }  
  94.   
  95.     /**   
  96.      * 从通道中读取数据   
  97.      * 并且判断是给那个服务通道的   
  98.      * @throws IOException    
  99.      * */  
  100.     public void read(SelectionKey key) throws IOException {  
  101.   
  102.         this.buf.clear();  
  103.         //通过选择键来找到之前注册的通道     
  104.         //但是这里注册的是ServerSocketChannel为什么会返回一个SocketChannel??     
  105.         SocketChannel channel = (SocketChannel) key.channel();  
  106.         //从通道里面读取数据到缓冲区并返回读取字节数     
  107.         int count = channel.read(this.buf);  
  108.   
  109.         if (count == -1) {  
  110.             //取消这个通道的注册     
  111.             key.channel().close();  
  112.             key.cancel();  
  113.             return;  
  114.         }  
  115.   
  116.         //将数据从缓冲区中拿出来     
  117.         String input = new String(this.buf.array()).trim();  
  118.         //那么现在判断是连接的那种服务     
  119.         if (channel.equals(this.clientchannel1)) {  
  120.             System.out.println("欢迎您使用服务A");  
  121.             System.out.println("您的输入为:" + input);  
  122.         } else {  
  123.             System.out.println("欢迎您使用服务B");  
  124.             System.out.println("您的输入为:" + input);  
  125.         }  
  126.   
  127.     }  
  128.   
  129.     @Override  
  130.     public void run() {  
  131.         while (true) {  
  132.             try {  
  133.                 System.out.println("running ... ");  
  134.                 //选择一组键,其相应的通道已为 I/O 操作准备就绪。     
  135.                 this.selector.select();  
  136.   
  137.                 //返回此选择器的已选择键集     
  138.                 //public abstract Set<SelectionKey> selectedKeys()     
  139.                 Iterator selectorKeys = this.selector.selectedKeys().iterator();  
  140.                 while (selectorKeys.hasNext()) {  
  141.                     System.out.println("running2 ... ");  
  142.                     //这里找到当前的选择键     
  143.                     SelectionKey key = (SelectionKey) selectorKeys.next();  
  144.                     //然后将它从返回键队列中删除     
  145.                     selectorKeys.remove();  
  146.                     if (!key.isValid()) { // 选择键无效  
  147.                         continue;  
  148.                     }  
  149.                     if (key.isAcceptable()) {  
  150.                         //如果遇到请求那么就响应     
  151.                         this.accept(key);  
  152.                     } else if (key.isReadable()) {  
  153.                         //读取客户端的数据     
  154.                         this.read(key);  
  155.                     }  
  156.                 }  
  157.             } catch (Exception e) {  
  158.                 e.printStackTrace();  
  159.             }  
  160.         }  
  161.     }  
  162.   
  163.     public static void main(String[] args) {  
  164.         Server server = new Server();  
  165.         Thread thread = new Thread(server);  
  166.         thread.start();  
  167.     }  
  168. }  


Client

[java] view plaincopy
  1. package cn.vicky.channel;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.InetSocketAddress;  
  5. import java.nio.ByteBuffer;  
  6. import java.nio.channels.SocketChannel;  
  7. import java.net.InetAddress;  
  8.   
  9. /**   
  10.  * TCP/IP的NIO非阻塞方式   
  11.  * 客户端   
  12.  * */  
  13. public class Client {  
  14.   
  15.     //创建缓冲区     
  16.     private ByteBuffer buffer = ByteBuffer.allocate(512);  
  17.     //访问服务器     
  18.   
  19.     public void query(String host, int port) throws IOException {  
  20.         InetSocketAddress address = new InetSocketAddress(InetAddress.getByName(host), port);  
  21.         SocketChannel socket = null;  
  22.         byte[] bytes = new byte[512];  
  23.         while (true) {  
  24.             try {  
  25.                 System.in.read(bytes);  
  26.                 socket = SocketChannel.open();  
  27.                 socket.connect(address);  
  28.                 buffer.clear();  
  29.                 buffer.put(bytes);  
  30.                 buffer.flip();  
  31.                 socket.write(buffer);  
  32.                 buffer.clear();  
  33.             } catch (Exception e) {  
  34.                 e.printStackTrace();  
  35.             } finally {  
  36.                 if (socket != null) {  
  37.                     socket.close();  
  38.                 }  
  39.             }  
  40.         }  
  41.     }  
  42.   
  43.     public static void main(String[] args) throws IOException {  
  44.         new Client().query("localhost"8099);  
  45.   
  46.     }  
  47. }  


 

以上的服务端一个线程监听两个服务,整个服务端只有一个阻塞的方法:

//选择一组键,其相应的通道已为 I/O 操作准备就绪。  

this.selector.select();  

 

当客户请求服务器的时候,那么这造成了TCP没有面向连接的假象,其实至少在传输数据的时候是连接的,只是在一次I/O请求结束之后服务器端就把连接给断开,继而继续去处理更多的请求。而在客户端,可以看到也是遇到一次请求的时候就connect服务端一次。所以TCP还是面向连接的。

     现在终于知道了为什么叫非阻塞式IO了,大概就是这个意思。

0 0