socket 异步通讯

(1)
背景:
Merlin 之前 , 编写 Socket 程序是比较繁琐的工作 . 因为输入输出都必须同步 . 这样 , 对于多客户端客户 / 服务器模式 , 不得不使用多线程 . 即为每个连接的客户都分配一个线程来处理输入输出 . 由此而带来的问题是可想而知的 . 程序员不得不为了避免死锁 , 线程安全等问题 , 进行大量的编码和测试 .
出机制的操作平台在当今操作平台中处于主流地位.于是,Jdk(J2SE)的第五次发布中引入了异步输入输出机制.
(2)
介绍:
下面将介绍使用异步机制的程序设计。
Merlin 中加入了用于实现异步输入输出机制的应用程序接口包: java.nio (新的输入输出包,定义了很多基本类型缓冲 (Buffer) ), java.nio.channels( 通道及选择器等,用于异步输入输出 ) java.nio.charset (字符的编码解码)。通道 (Channel) 首先在选择器 (Selector) 中注册自己感兴趣的事件,当相应的事件发生时,选择器便通过选择键 (SelectionKey) 通知已注册的通道。然后通道将需要处理的信息,通过缓冲( Buffer )打包,编码 / 解码 , 完成输入输出控制。
通道介绍:
这里主要介绍 ServerSocketChannel SocketChannel. 它们都是可选择的 (selectable) 通道,分别可以工作在同步和异步两种方式下(注意,这里的可选择不是指可以选择两种工作方式,而是指可以有选择的注册自己感兴趣的事件)。可以用 channel.configureBlocking(Boolean ) 来设置其工作方式。与以前版本的 API 相比较, ServerSocketChannel 就相当于 ServerSocket (ServerSocketChannel 封装了 ServerSocket), SocketChannel 就相当于 Socket SocketChannel 封装了 Socket )。当通道工作在同步方式时,编程方法与以前的基本相似,这里主要介绍异步工作方式。
所谓异步输入输出机制,是指在进行输入输出处理时,不必等到输入输出处理完毕才返回。所以异步的同义语是非阻塞( None Blocking )。在服务器端, ServerSocketChannel 通过静态函数 open() 返回一个实例 serverChl 。然后该通道调用 serverChl.socket().bind() 绑定到服务器某端口,并调用 register Selector sel, SelectionKey.OP_ACCEPT )注册 OP_ACCEPT 事件到一个选择器中( ServerSocketChannel 只可以注册 OP_ACCEPT 事件)。当有客户请求连接时,选择器就会通知该通道有客户连接请求,就可以进行相应的输入输出控制了;在客户端, clientChl 实例注册自己感兴趣的事件后(可以是 OP_CONNECT,OP_READ,OP_WRITE 的组合),调用 clientChl.connect (InetSocketAddress ) 连接服务器然后进行相应处理。注意,这里的连接是异步的,即会立即返回而继续执行后面的代码。
选择器和选择键介绍:
选择器( Selector )的作用是:将通道感兴趣的事件放入队列中,而不是马上提交给应用程序,等已注册的通道自己来请求处理这些事件。换句话说,就是选择器将会随时报告已经准备好了的通道,而且是按照先进先出的顺序。那么,选择器是通过什么来报告的呢?选择键 (SelectionKey) 。选择键的作用就是表明哪个通道已经做好了准备,准备干什么。你也许马上会想到,那一定是已注册的通道感兴趣的事件。不错,例如对于服务器端 serverChl 来说,可以调用 key.isAcceptable() 来通知 serverChl 有客户端连接请求。相应的函数还有: SelectionKey.isReadable(),SelectionKey.isWritable() 。一般的,在一个循环中轮询感兴趣的事件(具体可参照下面的代码)。如果选择器中尚无通道已注册事件发生,调用 Selector.select() 将阻塞,直到有事件发生为止。另外,可以调用 selectNow() 或者 select(long timeout) 。前者立即返回,没有事件时返回 0 值;后者等待 timeout 时间后返回。一个选择器最多可以同时被 63 个通道一起注册使用。
(3)
显示了使用NIO实现非阻塞式服务器的示意图:
从图17.6中可以看出,服务器上所有Channel(包括ServerSocketChannel和SocketChannel)都需要向 Selector注册,而该Selector则负责监视这些Socket的IO状态,当其中任意一个或多个Channel具有可用的IO操作时,该 Selector的select()方法将会返回大于0的整数,该整数值就表示该Selector上有多少个Channel具有可用的IO操作,并提供了 selectedKeys()方法来返回这些Channel对应的SelectionKey集合。正是通过Selector,使得服务器端只需要不断地调用Selector实例的select()方法即可知道当前所有Channel是否有需要处理的IO操作。
当Selector上注册的所有Channel都没有需要处理的IO操作时,select()方法将被阻塞,调用该方法的线程被阻塞。
本示例程序使用NIO实现了多人聊天室的功能,服务器使用循环不断获取Selector的select()方法返回值,当该返回值大于0时就处理该Selector上被选择SelectionKey所对应的Channel。
(4)
实例:
public class NServer
{
//  用于检测所有Channel状态的Selector
   private Selector selector = null;
//  定义实现编码、解码的字符集对象
   private Charset charset = Charset.forName( "UTF-8");
   public void init() throws IOException
  {
    selector = Selector.open();
//    通过open方法来打开一个未绑定的ServerSocketChannel实例
    ServerSocketChannel server = ServerSocketChannel.open();
    InetSocketAddress isa = new InetSocketAddress( "127.0.0.1", 30000);    
//    将该ServerSocketChannel绑定到指定IP地址
    server.socket().bind(isa);
//    设置ServerSocket以非阻塞方式工作
    server.configureBlocking( false);
//    将server注册到指定Selector对象
    server.register(selector, SelectionKey.OP_ACCEPT);
     while (selector.select() > 0)    
    {
//      依次处理selector上的每个已选择的SelectionKey
       for (SelectionKey sk : selector.selectedKeys())
      {
//        从selector上的已选择Key集中删除正在处理的SelectionKey
        selector.selectedKeys().remove(sk);                                                         //①
//        如果sk对应的通道包含客户端的连接请求
         if (sk.isAcceptable())                                                                                         //②
        {
//          调用accept方法接受连接,产生服务器端对应的SocketChannel
          SocketChannel sc = server.accept();
//          设置采用非阻塞模式
          sc.configureBlocking( false);
//          将该SocketChannel也注册到selector
          sc.register(selector, SelectionKey.OP_READ);
//          将sk对应的Channel设置成准备接受其他请求
          sk.interestOps(SelectionKey.OP_ACCEPT);
        }
//        如果sk对应的通道有数据需要读取
         if (sk.isReadable())                                                                                             //③
        {
//          获取该SelectionKey对应的Channel,该Channel中有可读的数据
          SocketChannel sc = (SocketChannel)sk.channel();
//          定义准备执行读取数据的ByteBuffer
          ByteBuffer buff = ByteBuffer.allocate(1024);
          String content = "";
//          开始读取数据
           try
          {
             while(sc.read(buff) > 0)
            {
              buff.flip();
              content += charset.decode(buff);
            }
//            打印从该sk对应的Channel里读取到的数据
            System.out.println( "=====" + content);
//            将sk对应的Channel设置成准备下一次读取
            sk.interestOps(SelectionKey.OP_READ);
          }
//          如果捕捉到该sk对应的Channel出现了异常,即表明该Channel
//          对应的Client出现了问题,所以从Selector中取消sk的注册
           catch (IOException ex)
          {
//            从Selector中删除指定的SelectionKey
            sk.cancel();
             if (sk.channel() != null)
            {
              sk.channel().close();
            }
          }
//          如果content的长度大于0,即聊天信息不为空
           if (content.length() > 0)
          {
//            遍历该selector里注册的所有SelectKey
             for (SelectionKey key : selector.keys())
            {
//              获取该key对应的Channel
                
              Channel targetChannel = key.channel();
//              如果该channel是SocketChannel对象
               if (targetChannel instanceof SocketChannel)
              {
//                将读到的内容写入该Channel中
                SocketChannel dest = (SocketChannel)targetChannel;
                dest.write(charset.encode(content));
              }
            }
          }
        }
      }
    }
  }
   public static void main(String[] args)
   throws IOException
  {
     new NServer().init();
  }
}
 
客户端:
public class NClient
{
//  定义检测SocketChannel的Selector对象
   private Selector selector = null;
//  定义处理编码和解码的字符集
   private Charset charset = Charset.forName( "UTF-8");
//  客户端SocketChannel
   private SocketChannel sc = null;
   public void init() throws IOException
  {
    selector = Selector.open();
    InetSocketAddress isa = new InetSocketAddress( "127.0.0.1", 30000);
//    调用open静态方法创建连接到指定主机的SocketChannel
    sc = SocketChannel.open(isa);
//    设置该sc以非阻塞方式工作
    sc.configureBlocking( false);
//    将SocketChannel对象注册到指定Selector
    sc.register(selector, SelectionKey.OP_READ);
//    启动读取服务器端数据的线程
     new ClientThread().start();
//    创建键盘输入流
    Scanner scan = new Scanner(System.in);
     while (scan.hasNextLine())
    {
//      读取键盘输入
      String line = scan.nextLine();
//      将键盘输入的内容输出到SocketChannel中
      sc.write(charset.encode(line));
    }
  }
//  定义读取服务器数据的线程
   private class ClientThread extends Thread
  {
     public void run()
    {
       try
      {
         while (selector.select() > 0)    
        {
//          遍历每个有可用IO操作Channel对应的SelectionKey
           for (SelectionKey sk : selector.selectedKeys())
          {
//            删除正在处理的SelectionKey
            selector.selectedKeys().remove(sk);
//            如果该SelectionKey对应的Channel中有可读的数据
             if (sk.isReadable())
            {
//              使用NIO读取Channel中的数据
              SocketChannel sc = (SocketChannel)sk.channel();
              ByteBuffer buff = ByteBuffer.allocate(1024);
              String content = "";
               while(sc.read(buff) > 0)
              {
                sc.read(buff);    
                buff.flip();
                content += charset.decode(buff);
              }
//              打印输出读取的内容
              System.out.println( "聊天信息:" + content);
//              为下一次读取作准备
              sk.interestOps(SelectionKey.OP_READ);
            }
          }
        }
      }
       catch (IOException ex)
      {
        ex.printStackTrace();
      }
    }
  }
   public static void main(String[] args)
   throws IOException
  {
     new NClient().init();
  }
}
 

你可能感兴趣的:(职场,123,休闲)