nio&netty系列之一nio基础

 

写在文章初始

 

 

       很久没有读源码了,近来据说netty的源码很漂亮,而且自身对nio的理解其实一直也不到位,所以有天突然心血来潮,决定沉下心,重新学习nio的知识,并通过读netty的源码来加深理解,但是现实生活中,工作,娱乐,心情等各种原因,导致整个过程很漫长,所以我决定一篇一篇的整理我的笔记内容发布到我的博客。算是一个进步过程吧.

 

JAVA NIO基础

 

        NIO是jdk1.4推出的新概念,同JDK1.5版本concurrent并发包,是当前java应用中非常火的两个玩意,前者提升了网络传输编码的性能,后者整个将并发编程的高度提升了一个档次。此文首先从NIO的基础入手(jdk7都有nio2了,得赶紧补旧知识的说),然后再去慢慢深入netty的设计与实现,仅当做我最近一段时间从事netty使用这个工作的一个笔记。

      NIO有三个重要概念:缓冲区(ByteBuffer),通道(Channel),选择器(Selected),通道传输的数据必须是缓冲区对象,通道注册在选择器上,选择器上有四个事件,op_accept(服务器上特有,接受连接事件),op_connection(客户端特有,建立连接事件),op_writer(写事件),op_read(读事件).

     我们先略微介绍下这三个概念,如果对这三个概念需要细究的同学,附件有上传我学习的nio知识的PDF,个人认为已经很详细了.

  1. 缓冲区:首先在NIO中,我们不再以传统的socket的字符流传递数据,而是以块传递数据,是将数据放到缓存区然后推送到通道的。也就是说通道只接受Buffer对象,通常我们使用ByteBuffer.
  2.  通道:字面意思,客户端和服务器端连接的一条通道,用于传输数据,只接受Buffer对象。通道可以注册到选择器上。
  3. 选择器:这个概念较复杂,如果熟悉linux的IO系统的童鞋,那么恭喜,概念是类似的,java的nio的思想应该是来源于linux的异步非阻塞模式select.选择器用于管理所有的通道,是个事件触发模式,当通道就绪的时候,就会触发响应的事件并使对应的key就绪,然后主程序只要获取所有的就绪key(每一个key对应一个唯一的通道),然后做对应的业务逻辑即可。

NIO的过程

      JAVA的NIO模型是借鉴了操作系统,应该可以说是直接借助了操作系统的IO模型,由于本人对NIO的源码未曾仔细深入,且对linux太菜,所以这些资料基本都是从别人的博客资料查询而知。因没自信介绍NIO的模型,所以暂时用生活的一个场景来描述NIO的工作过程。

      餐厅点菜是一个很好的场景,我记得也有人用过。在以前,我们点菜的时候,服务员会等在你旁边记录你要点的菜,有时候你思考的越久,服务员等待的越久,这就是传统的SOCKET,这个服务员在你点菜的过程中被阻塞了。那么现在点菜就不一样了,服务员给你一张菜单和点菜单,你自己写完菜单然后叫服务员即可,这就是NIO模型,那我们就来分析这个新的点菜过程。

      1、客户进入餐厅,餐厅总台指定一个服务员上来迎接,帮你安排座位,给你菜单和点菜单。

      2、服务员去总台服务员那里登记有新客户到来,并且由我服务,客户正在准备点餐。

      3、客户思考今天要吃什么菜,并且记录到点菜单上,让服务员把点菜单放到总台哪边去

      4、总台服务员定时扫描点菜单

      5、将点菜内容发送给厨房。

      6、厨房厨师开始准备指定的餐

      7、总台服务员定时扫描,看那些客户的菜做完了,就通知与客户对应的服务员过来

      8、服务员就把菜送到客户那里。

      这里我们客户初略的概括为,餐厅总台是服务器,客户是客户端,服务员是通道(channel)。对于餐厅而言,服务员收取点餐单是读事件,送餐是写事件,点菜单和餐都是缓冲区(buffer),总台的服务员是选择器(selector),厨房的厨师就是业务逻辑实现者。所以上述六个过程概括为

      1、餐厅与客户创建一个通道,这个通道是指服务员。

      2、然后将此通道注册到selector(总台服务员)上,并为此selector注册了一个读事件(告诉总台服务员客户要准备点餐)

      3、客户写数据到点菜单,并有通道(服务员)交给餐厅总台哪里。

      4、服务器端的选择器selector查看哪边的点菜单写完了,触发服务器的读事件

      5、总台读取缓存区(点菜单)数据发送到业务逻辑那里

      6、业务逻辑(做菜),把做好的菜放到缓冲区(餐盘)

      7、selector扫描哪个客户的数据处理完毕,就通知指定通道

      8、通道将数据传输给客户。

NIO与传统socket对比      

那么说到此处,概念仍旧模糊,那么这个NIO与传统的Socket编程的优势体现在哪里?我来比较下传统socket编程的流程和nio编程的流程来分析下孰优孰劣.

 传统socket编程流程

     1、客户端创建一个socket连接到服务器

     2、客户端等待socket建立连接(阻塞)

     3、客户端发起请求

     4、客户端读取数据(阻塞)

     5、结束。

     注意:

     第一步:创建socket建立连接是个阻塞模式,所以说创建socket的成本很高,以前编程我们会使用一个socket连接池,创建一批socket连接放到连接池中去,从连接池获取socket进行操作,操作完成归还socket回连接池,例如数据库连接池是一样的设计。我们通常称之为长连接。

     第三步,客户端读取数据是个阻塞模式,在读取数据的时候我在等待服务器返回数据。如果服务器响应时间为10S,那么我这里要等待10秒才能获取数据。这个很难优化。

     NIO编程流程

     1、客户端创建一个通道(SocketChannel)

     2、创建一个选择器(Selector,如果没有选择器的话)

     3 、通道注册到选择器上,注册事件为OP_CONNECT

     4、客户端选择器触发OP_CONNECT事件。(编码中移除该就绪key)

     5、捕捉到OP_CONNECT后等待连接创建完毕(阻塞)

     6、连接创建完毕,直接发送数据。并且注册当前通道为OP_READ事件。

     7、当服务器端有数据返回的时候,触发通道的OP_READ事件

     8、读取内容。

     注意:
     1、第五步其实也是一个阻塞模式,所以不要频繁的创建通道,一般一个服务器和一个客户端之间只需要一个通道存在,最好是长期存在的。
     两者比较:NIO编程比传统的socket编程从逻辑上看就复杂很多,但是它主要是优化了传统socket编程的第四步,读取数据的时候可以不再是个阻塞模式,而是一个异步模式,在NIO状态下,发送完数据,注册一个OP_READ事件后就不管了,执行接下去的业务逻辑,直到有数据从服务器返回才会触发读取操作。
     疑问二:我客户端就是要同步操作,发送完数据后就是想等待返回数据,那么NIO的优势不是没有了么?

     其实大家如果换位思考,上面我换成服务器端,那么大家思考下,传统socket编程模式下,客户端与服务器端建立socket后,服务器首先也是在等待客户端传输数据过来,这个过程是一直阻塞的,而服务器端此过程完全没必要使用同步,这是很消耗服务器性能的。在NIO模式下,服务器与客户端建立完通道,服务器注册一个OP_READ事件后,服务器的CPU就可以去忙其它事情了,等客户端数据传输过来后,服务器才会触发读事件并处理相应业务逻辑,所以NIO对于服务器的性能提升是很明显的,并且由于NIO使用了块的传输方式,充分的利用了当今操作系统的性能,在传输性能上也会有明显的提升。

      BTW:这是我对传统socket编程和NIO的一些浅薄理解

 JAVA NIO 简单实例

//服务器端类
public class NioService {
       private static final int port = 4444;
       private Selector selector = null;

       // 启动服务
       public void startUp() {
             try {
                   int channels = 0;
                   int nKeys = 0;
                   int currentSelector = 0;

                   // 创建一个通道
                  ServerSocketChannel serverSocketChannel = ServerSocketChannel
                              . open();
                   // 绑定地址,类似 sokcet的绑定
                  serverSocketChannel.socket().bind(
                               new InetSocketAddress(InetAddress.getLocalHost(), 4444));
                   // 必须设置成非同步模式,在要注册到选择器的前提下,否则在注册的时候会报异常
                  serverSocketChannel.configureBlocking( false);
                   // 使用Selector
                   selector = Selector.open();
                   // 注册一个感兴趣的事件,这里是OP_ACCEPT事件.
                  SelectionKey s = serverSocketChannel.register(selector ,
                              SelectionKey. OP_ACCEPT);
                  System. out.println("服务器启动成功" );
                  listen();

            } catch (IOException e) {
                   // TODO Auto-generated catch block
                  e.printStackTrace();
            }
      }

       private void listen() throws IOException {
             int nKeys = 0;
             while (true ) {
                   // 阻塞线程,至到有就绪通道,返回值为有多少key已经就绪
                  nKeys = selector.select();
                   //获取就绪key的集合
                   Set selectedKeys = selector.selectedKeys();
                   for (Iterator keys = selectedKeys.iterator(); keys.hasNext();) {
                        SelectionKey key = (SelectionKey) keys.next();
                         //将就绪的key从就绪key中移除掉,否则每次获取都是就绪的,而非更新的就绪key
                        keys.remove();
                         if (key.isAcceptable()) {
                              SocketChannel socketChannel= ((ServerSocketChannel) key.channel()).accept().socket()
                              .getChannel();
                              socketChannel.configureBlocking( false);
                              socketChannel.register( selector,
                                                      SelectionKey. OP_READ);
                        } else if (key.isReadable()) {
                              Socket socket = ((SocketChannel) key.channel()).socket();
                              SocketChannel sc = socket.getChannel();
                              sc.configureBlocking( false);
                              ByteBuffer bb = ByteBuffer. allocate(1024);
                              sc.read(bb);
                              System. out.println("客户端接受到的数据为:" +new String(bb.array()));
                              sc.register( selector,
                                                      SelectionKey. OP_WRITE);
                        } else if (key.isWritable()) {
                              Socket socket = ((SocketChannel) key.channel()).socket();
                              SocketChannel sc = socket.getChannel();
                              ByteBuffer bb = ByteBuffer. wrap("hello world again!"
                                          .getBytes());
                              sc.write(bb);
                              sc.configureBlocking( false);
                              sc.register( selector,
                                                      SelectionKey. OP_READ);
                        }
                  }
            }
      }

       public static void main(String[] ben) {
            NioService ns = new NioService();
            ns.startUp();
      }

 

//客户端类
public class NioClient {
       public static void main(String[] ben) throws IOException{
             int selectNum;
            
            Selector selector = Selector. open();
            SocketChannel socketChannel = SocketChannel. open();
            socketChannel.configureBlocking( false);
            socketChannel.register(selector, SelectionKey. OP_CONNECT);
            socketChannel.connect( new InetSocketAddress(InetAddress.getLocalHost(), 4444));
            
            
             while(true ){
                  selectNum = selector.select();
                   if(selectNum>0){
                        Set<SelectionKey> keys= selector.selectedKeys();
                         for(Iterator<SelectionKey> it = keys.iterator();it.hasNext();){
                              SelectionKey key = it.next();
                              it.remove();
                               if(key.isConnectable()){
                                    SocketChannel channel = ((SocketChannel)key.channel());
                                    channel.configureBlocking( false);
                                     // 判断此通道上是否正在进行连接操作。 
                          // 完成套接字通道的连接过程。 
                          if (channel.isConnectionPending()) { 
                              channel.finishConnect();
                              channel.write(ByteBuffer. wrap("client connection over!".getBytes()));
                                          channel.register(selector, SelectionKey.OP_WRITE );
                          }
                              } else if (key.isWritable()){
                                    key.channel().configureBlocking( false);
                                    ((SocketChannel)key.channel()).write(ByteBuffer. wrap("hello,i'm from client!".getBytes()));
                                    key.channel().register(selector, SelectionKey.OP_READ );
                                     //客户端线程去处理其它业务逻辑了
                                    System. out.println("cc" );
                                    System. out.println("cc" );
                                    System. out.println("cc" );
                                    System. out.println("cc" );
                                    System. out.println("cc" );
                                    System. out.println("cc" );
                              } else if (key.isReadable()){
                                    ByteBuffer bb = ByteBuffer.allocate(1024);
                                    ((SocketChannel)key.channel()).read(bb);
                                    System. out.println(new String(bb.array()));
                                    key.channel().configureBlocking( false);
//                                  key.channel().register(selector, SelectionKey.OP_WRITE);
                                     //本次模拟的是一次读取过程,所以读完后就关闭
                                    key.channel().close();
                                    selector.close();
                              }
                              
                        }
                  }
            }
      }

}

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值