从事网络编程的应该都知道传输层的主要协议是TCP/UDP,关于二者的区别网络上有好多资料这里就很少说介绍,然而数据的传输过程大都有个IO操做,所以就衍生出了BIO,NIO,AIO三大模型,关于这三者的区别本系列博客有介绍,欢迎你们参考并指正,本篇主要写基于Java实现的NIO编程模型的一些使用细节,欢迎正在使用NIO编程的朋友们出来讨论,但愿起到一个抛砖引玉的效果。java
最近一直在看mina与netty的源代码,从中学习到了好多编程技巧与编程方式,因而花了点时间研究了NIO的Java层面的调用,本篇主要以代码为主:编程
首先我须要实现的是:服务器
1:服务端启动监听socket机制网络
2:客户端发起与服务端创建链接,并注册写事件与读事件发送心跳包机制socket
3:服务端创建链接后注册读事件读取心跳包并注册写事件回应心跳包ide
下面先贴出服务端的代码:学习
import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; 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; import java.util.Set; public class NIOServer { /*标识数字*/ private int flag = 0; /*缓冲区大小*/ private int BLOCK = 4096; /*接受数据缓冲区*/ private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK); /*发送数据缓冲区*/ private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK); private Selector selector; private boolean connecFa=true; public NIOServer() throws IOException{ this(8080); } public NIOServer(int port) throws IOException { // 打开服务器套接字通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 服务器配置为非阻塞 serverSocketChannel.configureBlocking(false); // 检索与此通道关联的服务器套接字 ServerSocket serverSocket = serverSocketChannel.socket(); // 进行服务的绑定 serverSocket.bind(new InetSocketAddress(port)); // 经过open()方法找到Selector selector = Selector.open(); // 注册到selector,等待链接 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("Server Start----:"+port); } // 监听 private void listen() throws IOException { while (connecFa) { // 选择一组键,而且相应的通道已经打开 selector.select(); // 返回此选择器的已选择键集。 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); System.out.println("selectionKey.interestOps()\t"+selectionKey.interestOps()); //iterator.remove(); handleKey(selectionKey); selectionKeys.clear(); } } } // 处理请求 private void handleKey( SelectionKey selectionKey) throws IOException { // 接受请求 ServerSocketChannel server = null; SocketChannel client = null; String receiveText; String sendText; int count=0; // 测试此键的通道是否已准备好接受新的套接字链接。 if (selectionKey.isAcceptable()) { // 返回为之建立此键的通道。 server = (ServerSocketChannel) selectionKey.channel(); // 接受到此通道套接字的链接。 // 此方法返回的套接字通道(若是有)将处于阻塞模式。 client = server.accept(); client.configureBlocking(false); // 注册到selector,等待链接 client.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { System.err.println("服务端开始读操做==="+selectionKey.isReadable()); System.err.println("selectionKey.attachment()==="+selectionKey.attachment()); // 返回为之建立此键的通道。 client = (SocketChannel) selectionKey.channel(); //将缓冲区清空以备下次读取 receivebuffer.clear(); //读取服务器发送来的数据到缓冲区中 count = client.read(receivebuffer); if (count > 0) { receiveText = new String( receivebuffer.array(),0,count); System.out.println("服务器端接受客户端数据--:"+receiveText); /*if(selectionKey.interestOps()==1){ selectionKey.interestOps(selectionKey.interestOps() | (SelectionKey.OP_WRITE)); }*/ client.register(selector, SelectionKey.OP_WRITE); } } else if (selectionKey.isWritable()&&selectionKey.isValid()) { System.err.println("客户端开始写操做==="+selectionKey.isWritable()); System.err.println("selectionKey.attachment()==="+selectionKey.attachment()); //将缓冲区清空以备下次写入 sendbuffer.clear(); // 返回为之建立此键的通道。 client = (SocketChannel) selectionKey.channel(); sendText="message from server--" + flag++; //向缓冲区中输入数据 sendbuffer.put(sendText.getBytes()); //将缓冲区各标志复位,由于向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位 sendbuffer.flip(); //输出到通道 client.write(sendbuffer); System.out.println("服务器端向客户端发送数据--:"+sendText); /* if(!"heart".equals(selectionKey.attachment())){ selectionKey.interestOps(selectionKey.interestOps() & (~SelectionKey.OP_WRITE)); }*/ } } /** * @throws IOException * @param args * @throws */ public static void main(String[] args) throws IOException { // TODO Auto-generated method stub int port = 8080; NIOServer server = new NIOServer(port); server.listen(); } }
如下是客户端的代码:测试
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.SocketChannel; import java.util.Iterator; import java.util.Set; public class NIOClient { /*标识数字*/ private static int flag = 0; /*缓冲区大小*/ private static int BLOCK = 4096; /*接受数据缓冲区*/ private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK); /*发送数据缓冲区*/ private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK); /*服务器端地址*/ private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress( "localhost", 1111); public static void main(String[] args) throws IOException { // TODO Auto-generated method stub int read=0; int write=0; // 打开socket通道 SocketChannel socketChannel = SocketChannel.open(); // 设置为非阻塞方式 socketChannel.configureBlocking(false); // 打开选择器 Selector selector = Selector.open(); // 注册链接服务端socket动做 socketChannel.register(selector, SelectionKey.OP_CONNECT); // 链接 socketChannel.connect(SERVER_ADDRESS); // 分配缓冲区大小内存 Set<SelectionKey> selectionKeys; Iterator<SelectionKey> iterator; SelectionKey selectionKey; SocketChannel client; String receiveText; String sendText; int count=0; while (true) { //选择一组键,其相应的通道已为 I/O 操做准备就绪。 //此方法执行处于阻塞模式的选择操做。 selector.select(); //返回此选择器的已选择键集。 selectionKeys = selector.selectedKeys(); //System.out.println(selectionKeys.size()); iterator = selectionKeys.iterator(); while (iterator.hasNext()) { selectionKey = iterator.next(); System.out.println("selectionKey.interestOps()\t"+selectionKey.interestOps()); if (selectionKey.isConnectable()) { System.err.println("selectionKey.isAcceptable()==="+selectionKey.isAcceptable()); System.err.println("selectionKey.isConnectable()==="+selectionKey.isConnectable()); client = (SocketChannel) selectionKey.channel(); client.configureBlocking(false); // 判断此通道上是否正在进行链接操做。 // 完成套接字通道的链接过程。 if (client.isConnectionPending()) { client.finishConnect(); System.out.println("完成链接!"); sendbuffer.clear(); sendbuffer.put("Hello,Server".getBytes()); sendbuffer.flip(); client.write(sendbuffer); } //heart能够理解为事件类型在读写的时候能够经过selectionKey.attachment()得到注册的值 client.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE,"heart"); } else if (selectionKey.isReadable()) { System.err.println("客户端开始读操做==="+selectionKey.isReadable()); client = (SocketChannel) selectionKey.channel(); //将缓冲区清空以备下次读取 receivebuffer.clear(); //读取服务器发送来的数据到缓冲区中 count=client.read(receivebuffer); if(count>0){ receiveText = new String( receivebuffer.array(),0,count); System.out.println("客户端接受服务器端数据--:"+receiveText); } } else if (selectionKey.isWritable()) { System.err.println("客户端开始写操做==="+selectionKey.isWritable()); sendbuffer.clear(); client = (SocketChannel) selectionKey.channel(); sendText = "发送心跳包" + (flag++); if("heart".equals(selectionKey.attachment())){ try { Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } sendText = "发送心跳包" + (flag++); } sendbuffer.put(sendText.getBytes()); //将缓冲区各标志复位,由于向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位 sendbuffer.flip(); client.write(sendbuffer); System.out.println("客户端向服务器端发送数据--:"+sendText); } iterator.remove(); } } } }
1:运行服务端this
2:运行客户端spa
发现服务端写事件死循环,在这里问题就来了,我这边只想每次收到心跳包才回应一个写事件而不是注册了写事件后就陷入死循环状态,分析问题应该是在写事件上,那是何时才能触发写事件呢?
总结以下:
写操做的就绪条件为底层缓冲区有空闲空间,而写缓冲区绝大部分时间都是有空闲空间的,因此当注册写事件后,写操做一直是就绪的,选择处理线程会占用整个CPU资源。因此,只有当确实有数据要写时再注册写操做,并在写完之后立刻取消注册。
解决方法:思路是每次写完以后取消写事件的注册,在写玩以后增长以下代码:
selectionKey.interestOps(selectionKey.interestOps() & (~SelectionKey.OP_WRITE));
固然读完以后要从新注册写事件,代码以下:
if(selectionKey.interestOps()==1){ selectionKey.interestOps(selectionKey.interestOps() | (SelectionKey.OP_WRITE)); }
以上代码均在服务端实现的注释部分存在,经过这个实践完全的了解了Java NIO基于事件驱动模型的编程思想与处理细节,不过这里有点想不通的是为何对写事件在操做层面上没有一个好的解决方案,我想应该跟操做系统的底层实现有关系。
本文出自 “陈砚羲” 博客,请务必保留此出处http://chenyanxi.blog.51cto.com/4599355/1540353