import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.sql.Time; import java.util.concurrent.TimeUnit; public class NIOTCPClient { public static void main(String[] args) { // TODO Auto-generated method stub try { new NIOTCPClient().work("127.0.0.1", 9898, "你好"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void work(String ip, int port, String message) throws Exception { // 创建一个信道,并设为非阻塞模式 SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // 向服务端发起连接 if (!socketChannel.connect(new InetSocketAddress(ip, port))) { // 不断地轮询连接状态,直到完成连接 while (!socketChannel.finishConnect()) { // 在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性 // 这里为了演示该方法的使用,只是一直打印"----" TimeUnit.MILLISECONDS.sleep(500); System.out.println("。。。。。。"); } } // 为了与后面打印的"."区别开来,这里输出换行符 System.out.print("\n"); // 分别实例化用来读写的缓冲区 byte[] msgBytes = message.getBytes(); ByteBuffer writeBuf = ByteBuffer.wrap(msgBytes); ByteBuffer readBuf = ByteBuffer.allocate(msgBytes.length); // 接收到的总的字节数 int totalBytesRcvd = 0; // 每一次调用read()方法接收到的字节数 int bytesRcvd; // 循环执行,直到接收到的字节数与发送的字符串的字节数相等 while (totalBytesRcvd < msgBytes.length) { // 如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道 if (writeBuf.hasRemaining()) { socketChannel.write(writeBuf); } // 如果read()接收到-1,表明服务端关闭,抛出异常 if ((bytesRcvd = socketChannel.read(readBuf)) == -1) { throw new SocketException("Connection closed prematurely"); } // 计算接收到的总字节数 totalBytesRcvd += bytesRcvd; // 在等待通信完成的过程中,程序可以执行其他任务,以体现非阻塞IO的异步特性 // 这里为了演示该方法的使用,同样只是一直打印"." System.out.print("."); } // 打印出接收到的数据 System.out.println("Received: " + new String(readBuf.array(), 0, totalBytesRcvd)); // 关闭信道 socketChannel.close(); } }服务端:
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.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class NIOTCPServer { public static void main(String[] args) { // TODO Auto-generated method stub try { new NIOTCPServer().work(9898); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void work(int... ports) throws Exception { // 缓冲区的长度 final int BUFSIZE = 256; // select方法等待信道准备好的最长时间 final int TIMEOUT = 3000; // 创建一个选择器 Selector selector = Selector.open(); for (int port : ports) { // 实例化一个信道 ServerSocketChannel listenSocketChannel = ServerSocketChannel.open(); // 将该信道绑定到指定端口 listenSocketChannel.socket().bind(new InetSocketAddress(port)); // 配置信道为非阻塞模式 listenSocketChannel.configureBlocking(false); // 将选择器注册到各个信道 listenSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } // 创建一个实现了协议接口的对象 EchoSelectorProtocol protocol = new EchoSelectorProtocol(BUFSIZE); // 不断轮询select方法,获取准备好的信道所关联的Key集 while (true) { // 一直等待,直至有信道准备好了I/O操作 if (selector.select(TIMEOUT) == 0) { // 在等待信道准备的同时,也可以异步地执行其他任务, // 这里只是简单地打印"." System.out.println("*****等待*******"); continue; } System.out.println("--------------"); // 获取准备好的信道所关联的Key集合的iterator实例 Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator(); // 循环取得集合中的每个键值 while (keyIter.hasNext()) { SelectionKey key = keyIter.next(); // 如果服务端信道感兴趣的I/O操作为accept if (key.isAcceptable()) { System.out.println("key.isAcceptable()"); protocol.handleAccept(key); } // 如果客户端信道感兴趣的I/O操作为read if (key.isReadable()) { System.out.println("key.isReadable()"); protocol.handleRead(key); } // 如果该键值有效,并且其对应的客户端信道感兴趣的I/O操作为write if (key.isValid() && key.isWritable()) { System.out.println("key.isValid() && key.isWritable()"); protocol.handleWrite(key); } // 这里需要手动从键集中移除当前的key keyIter.remove(); } } } } class EchoSelectorProtocol { private int bufSize; // 缓冲区的长度 public EchoSelectorProtocol(int bufSize) { this.bufSize = bufSize; } // 服务端信道已经准备好了接收新的客户端连接 public void handleAccept(SelectionKey key) throws IOException { SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept(); socketChannel.configureBlocking(false); // 将选择器注册到连接到的客户端信道,并指定该信道key值的属性为OP_READ,同时为该信道指定关联的附件 socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize)); System.out.println("set SelectionKey.OP_READ"); } // 客户端信道已经准备好了从信道中读取数据到缓冲区 public void handleRead(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); // 获取该信道所关联的附件,这里为缓冲区 ByteBuffer buf = (ByteBuffer) key.attachment(); long bytesRead = socketChannel.read(buf); // 如果read()方法返回-1,说明客户端关闭了连接,那么客户端已经接收到了与自己发送字节数相等的数据,可以安全地关闭 if (bytesRead == -1) { socketChannel.close(); } else if (bytesRead > 0) { // 如果缓冲区总读入了数据,则将该信道感兴趣的操作设置为为可读可写 key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); System.out.println("set SelectionKey.OP_READ | SelectionKey.OP_WRITE"); } } // 客户端信道已经准备好了将数据从缓冲区写入信道 public void handleWrite(SelectionKey key) throws IOException { // 获取与该信道关联的缓冲区,里面有之前读取到的数据 ByteBuffer buf = (ByteBuffer) key.attachment(); // 重置缓冲区,准备将数据写入信道 buf.flip(); SocketChannel socketChannel = (SocketChannel) key.channel(); // 将数据写入到信道中 socketChannel.write(buf); if (!buf.hasRemaining()) { // 如果缓冲区中的数据已经全部写入了信道,则将该信道感兴趣的操作设置为可读 key.interestOps(SelectionKey.OP_READ); System.out.println("set SelectionKey.OP_READ"); } // 为读入更多的数据腾出空间 buf.compact(); } }运行结果:
*****等待******* -------------- key.isAcceptable() set SelectionKey.OP_READ -------------- key.isReadable() set SelectionKey.OP_READ | SelectionKey.OP_WRITE -------------- key.isReadable() key.isValid() && key.isWritable() set SelectionKey.OP_READ -------------- key.isReadable() set SelectionKey.OP_READ | SelectionKey.OP_WRITE -------------- key.isValid() && key.isWritable() set SelectionKey.OP_READ -------------- key.isReadable() *****等待******* *****等待******* *****等待*******
.....................Received: 你好说明:以上的服务端程序,select()方法第一次能选择出来的准备好的信道都是服务端信道,其关联键值的属性都为OP_ACCEPT,其有效操作都为 accept,在执行handleAccept方法时,为取得连接的客户端信道也进行了注册,属性为OP_READ,这样下次轮询调用select()方法时,便会检查到对read操作感兴趣的客户端信道(当然也有可能有关联accept操作兴趣集的信道),从而调用handleRead方法,在该方法中又注册了OP_WRITE属性,那么第三次调用select()方法时,便会检测到对write操作感兴趣的客户端信道(当然也有可能有关联read操作兴趣集的信道),从而调用handleWrite方法。
NIO
1、NIO主要包括两个部分:java.nio.channles包介绍Selector和Channel抽象,java.nio包介绍Buffer抽象。Selector和Channel抽象的关键点是:一次轮询一组客户端,查找哪个客户端需要服务;Buffer则提供了比Stream抽象更高效和可预测的I/O。Channel使用的不是流,正是Buffer缓冲区来发送或读写数据。