Java NIO详解

从事网络编程的应该都知道传输层的主要协议是TCP/UDP,关于两者的区别网络上有好多资料这里就不多说介绍,然而数据的传输过程大都有个IO操作,因此就衍生出了BIO,NIO,AIO三大模型,关于这三者的区别本系列博客有介绍,欢迎大家参考并指正,本篇主要写基于Java实现的NIO编程模型的一些使用细节,欢迎正在使用NIO编程的朋友们出来讨论,希望起到一个抛砖引玉的效果。

最近一直在看mina与netty的源代码,从中学习到了好多编程技巧与编程方式,于是花了点时间研究了NIO的Java层面的调用,本篇主要以代码为主:

首先我需要实现的是:

1:服务端启动监听socket机制

2:客户端发起与服务端建立连接,并注册写事件与读事件发送心跳包机制

3:服务端建立连接后注册读事件读取心跳包并注册写事件回应心跳包

下面先贴出服务端的代码:

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:运行服务端

2:运行客户端

发现服务端写事件死循环,在这里问题就来了,我这边只想每次收到心跳包才回应一个写事件而不是注册了写事件后就陷入死循环状态,分析问题应该是在写事件上,那是什么时候才能触发写事件呢?

总结如下:

写操作的就绪条件为底层缓冲区有空闲空间,而写缓冲区绝大部分时间都是有空闲空间的,所以当注册写事件后,写操作一直是就绪的,选择处理线程会占用整个CPU资源。所以,只有当确实有数据要写时再注册写操作,并在写完以后马上取消注册。


解决方法:思路是每次写完之后取消写事件的注册,在写玩之后增加如下代码:

selectionKey.interestOps(selectionKey.interestOps() & (~SelectionKey.OP_WRITE));

当然读完之后要重新注册写事件,代码如下:

if(selectionKey.interestOps()==1){
                	selectionKey.interestOps(selectionKey.interestOps() | (SelectionKey.OP_WRITE));
                
                }

以上代码均在服务端实现的注释部分存在,通过这个实践彻底的了解了Java NIO基于事件驱动模型的编程思想与处理细节,不过这里有点想不通的是为什么对写事件在操作层面上没有一个好的解决方案,我想应该跟操作系统的底层实现有关系。

Java NIO详解

时间: 2024-08-01 09:35:08

Java NIO详解的相关文章

Java NIO详解及实例和源码下载(二)

这里写链接内容 - WatchService public class WatchServiceTest { public static void main(String[] args) { try { WatchService watchService = FileSystems.getDefault().newWatchService(); Paths.get("D:\\").register(watchService, StandardWatchEventKinds.ENTRY_

Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO

Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO Java 非阻塞 IO 和异步 IO 转自https://www.javadoop.com/post/nio-and-aio 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.html 部分代码会放在我的的Github:https://github.com/h2p

Java网络编程和NIO详解3:IO模型与Java网络编程模型

Java网络编程和NIO详解3:IO模型与Java网络编程模型 基本概念说明 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限.为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间.针对linux操作系统而言,将最高的1G字节(从虚拟地址

Java网络编程和NIO详解开篇:Java网络编程基础

Java网络编程和NIO详解开篇:Java网络编程基础 计算机网络编程基础 转自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA 我们是幸运的,因为我们拥有网络.网络是一个神奇的东西,它改变了你和我的生活方式,改变了整个世界. 然而,网络的无标度和小世界特性使得它又是复杂的,无所不在,无所不能,以致于我们无法区分甚至无法描述. 对于一个码农而言,了解网络的基础知识可能还是从了解定义开始,认识OSI的七层协议模型,深入Socket内部,进而熟练地

Java网络编程和NIO详解6:Linux epoll实现原理详解

Java网络编程和NIO详解6:Linux epoll实现原理详解 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.html 部分代码会放在我的的Github:https://github.com/h2pl/ Linux epoll实现原理详解 在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者pol

Java网络编程和NIO详解9:基于NIO的网络编程框架Netty

Java网络编程和NIO详解9:基于NIO的网络编程框架Netty 转自https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introduction/ netty是基于NIO实现的异步事件驱动的网络编程框架,学完NIO以后,应该看看netty的实现,netty框架涉及的内容特别多,这里只介绍netty的基本使用和实现原理,更多扩展的内容将在以后推出. 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎

Java网络编程和NIO详解4:浅析NIO包中的Buffer、Channel 和 Selector

Java网络编程与NIO详解4:浅析NIO包中的Buffer.Channel 和 Selector 转自https://www.javadoop.com/post/nio-and-aio 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.html 部分代码会放在我的的Github:https://github.com/h2pl/ J

Java网络编程和NIO详解8:浅析mmap和Direct Buffer

Java网络编程与NIO详解8:浅析mmap和Direct Buffer 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.html 部分代码会放在我的的Github:https://github.com/h2pl/ Java网络编程与NIO详解8:浅析mmap和Direct Buffer 之前看到一篇文章说epoll中在维护epo

Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理

Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理 转自:https://www.jianshu.com/p/2b71ea919d49 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.html 部分代码会放在我的的Github:https://github.com/h2pl/ 浅谈 Linux