javaNIO

本文详细介绍了Java NIO的工作原理及应用实例,包括服务端与客户端的交互过程,重点讲解了如何通过Selector处理连接和读写操作。同时,探讨了在处理慢速网络连接时,如何有效利用OP_WRITE事件来节省CPU资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


说明: 用语言描述下nio

server 端得到一个selector binding到了 serverchannel并给它(强调 serverchannel)注册了 ACCEPTABLE 事件

client 端一样得到一个selector binding到了 socketchannel 并给它(强调socketchannel)注册了CONNECTED 事件

两边一联 各自触发了 ACCEPTABLE 事件(服务端) 和 CONNECTED 事件(客户端) 并各自拿到了 SelectionKey

再通过SelectionKey 得到 socketchannel(强调注意 是socketchannel) 并channel.register(this.selector, SelectionKey.OP_READ); 注册上selector 和 添加 READ事件

之后每次socketchannel.write(outBuffer) 都会给对方的selector 一个READ事件,对方通过selectionkey 并识别是READ事件后, 可以从selectionkey 拿到对应的socketchannel

然后 读 和写 数据



refer to http://weixiaolu.iteye.com/blog/1479656   (following is modified by this article)

server 端代码

package cn.nio;


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;  
  
/** 
 * NIO服务端 
 * @author 小路 
 */  
public class NIOServer {  
    //通道管理器  
    private Selector selector; 
    
  
    /** 
     * 获得一个ServerSocket通道,并对该通道做一些初始化的工作 
     * @param port  绑定的端口号 
     * @throws IOException 
     */  
    public void initServer(int port) throws IOException {  
        // 获得一个ServerSocket通道  
        ServerSocketChannel serverChannel = ServerSocketChannel.open();  
        // 设置通道为非阻塞  
        serverChannel.configureBlocking(false);  
        // 将该通道对应的ServerSocket绑定到port端口  
        serverChannel.socket().bind(new InetSocketAddress(port));  
        // 获得一个通道管理器  
        this.selector = Selector.open();  
        //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,  
        //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。  
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
    }  
  
    /** 
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 
     * @throws IOException 
     * @throws InterruptedException 
     */ 
    // FYI 这里没用到
    @SuppressWarnings("unchecked")  
    public void listen() throws IOException, InterruptedException {  
        System.out.println("服务端启动成功!");  
        // 轮询访问selector  
        
        while (true) {  
        	/*try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}*/
            //当注册的事件到达时,方法返回;否则,该方法会一直阻塞  
            selector.select();  
            // 获得selector中选中的项的迭代器,选中的项为注册的事件  
            Iterator ite = this.selector.selectedKeys().iterator();  
            while (ite.hasNext()) {  
                SelectionKey key = (SelectionKey) ite.next();  
                // 删除已选的key,以防重复处理  
                ite.remove();  
                // 客户端请求连接事件  
                if (key.isAcceptable()) {  
                	
                    ServerSocketChannel server = (ServerSocketChannel) key  
                            .channel();  
                    
                    System.out.println("debug1:" +server.socket().getLocalPort());
                 try {
					Thread.sleep(500000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
                    // 获得和客户端连接的通道  
                    SocketChannel channel = server.accept();  
                    // 设置成非阻塞  
                    channel.configureBlocking(false); 
                    
  
                    //在这里可以给客户端发送信息哦  
                    //channel.write(ByteBuffer.wrap(new String("hello chickens").getBytes()));  
                    //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。  
                    //channel.register(this.selector, SelectionKey.OP_READ);
                    
                      
                    // 获得了可读的事件  
                } else if (key.isReadable()) {
                	System.out.println("readable");
                        read(key);  
                }  
  
            }  
  
        }  
    }  
    /** 
     * 处理读取客户端发来的信息 的事件 
     * @param key 
     * @throws IOException  
     */  
    public void read(SelectionKey key) throws IOException{  
        // 服务器可读取消息:得到事件发生的Socket通道  
        SocketChannel channel = (SocketChannel) key.channel();  
        // 创建读取的缓冲区  
        System.out.println("debug2:" +channel.socket().getLocalPort());
        ByteBuffer buffer = ByteBuffer.allocate(10);  
        channel.read(buffer);  
        byte[] data = buffer.array();  
        String msg = new String(data).trim();  
        System.out.println("服务端收到信息:"+msg);  
        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());  
        channel.write(outBuffer);// 将消息回送给客户端  
    }  
      
    /** 
     * 启动服务端测试 
     * @throws IOException  
     * @throws InterruptedException 
     */  
    public static void main(String[] args) throws IOException, InterruptedException {  
        NIOServer server = new NIOServer();  
        server.initServer(8000);  
        //server.listen(); 
        while(true){
        	
        	System.out.println("select " + server.selector.select() + "event");
        	Iterator ite = server.selector.selectedKeys().iterator();
        	Thread.sleep(5000);
        	
        	while (ite.hasNext()) {  
        		SelectionKey key = (SelectionKey) ite.next();  
        		// 删除已选的key,以防重复处理  
        		ite.remove();  
        		// 客户端请求连接事件  
        		if (key.isAcceptable()) {  
        			System.out.println("ssssssssss");	
        			//Thread.sleep(5000000);
        			System.out.println("yyyyy");
        			ServerSocketChannel serverch = (ServerSocketChannel) key  
        					.channel();  
        			
        			// 获得和客户端连接的通道  
        			SocketChannel channel = serverch.accept();  
        			// 设置成非阻塞
        			System.out.println("debug1:" +channel.socket().getPort());
        			channel.configureBlocking(false);
        			String msg = "first sendmsg to client";
        			ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());  
        	        channel.write(outBuffer);// 将消息回送给客户端 
        	        
        	        Thread.sleep(5000);
        	        String msgs = "second sendmsg to client";
        			ByteBuffer outBuffers = ByteBuffer.wrap(msgs.getBytes());  
        	        channel.write(outBuffers);// 将消息回送给客户端
        		}
        	}    
        }
                

                //在这里可以给客户端发送信息哦  
                //channel.write(ByteBuffer.wrap(new String("hello chickens").getBytes()));  
                //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。  
                //channel.register(this.selector, SelectionKey.OP_READ);
      
    }  
  
}  

client端

package cn.nio;

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;

/**
 * NIO客户端
 * 
 * @author 小路
 */
public class NIOClient {
	// 通道管理器
	private Selector selector;

	/**
	 * 获得一个Socket通道,并对该通道做一些初始化的工作
	 * 
	 * @param ip
	 *            连接的服务器的ip
	 * @param port
	 *            连接的服务器的端口号
	 * @throws IOException
	 */
	public void initClient(String ip, int port) throws IOException {
		// 获得一个Socket通道
		SocketChannel channel = SocketChannel.open();
		// 设置通道为非阻塞
		channel.configureBlocking(false);
		// 获得一个通道管理器
		this.selector = Selector.open();

		// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调,用channel.finishConnect();才能完成连接,才能将 select的connect事件消化,否则会
		//一直返回1,且状态属于updated
		channel.connect(new InetSocketAddress(ip, port));
		// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
		channel.register(selector, SelectionKey.OP_CONNECT);
	}

	/**
	 * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
	 * 
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	public void listen() throws IOException {

		// 轮询访问selector
		int i = 0;
		while (true) {

			/*
			 * try { Thread.sleep(5000000); } catch (InterruptedException e) {
			 * // TODO Auto-generated catch block e.printStackTrace(); }
			 */
			selector.select();
			// 获得selector中选中的项的迭代器
			Iterator ite = this.selector.selectedKeys().iterator();
			while (ite.hasNext()) {
				System.out.println("debug3");
				SelectionKey key = (SelectionKey) ite.next();
				// 删除已选的key,以防重复处理
				ite.remove();
				// 连接事件发生
				if (key.isConnectable()) {

					System.out.println("confirm server has connected me");
					/*
					 * try { Thread.sleep(500000); } catch (InterruptedException
					 * e) { // TODO Auto-generated catch block
					 * e.printStackTrace(); } SocketChannel channel =
					 * (SocketChannel) key.channel(); // 如果正在连接,则完成连接 if
					 * (channel.isConnectionPending()) {
					 * channel.finishConnect();
					 * 
					 * } // 设置成非阻塞 channel.configureBlocking(false);
					 * System.out.println("debug4");
					 * 
					 * //break; // 在这里可以给服务端发送信息哦
					 * channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息")
					 * .getBytes()));
					 * 
					 * // 在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
					 * channel.register(this.selector, SelectionKey.OP_READ);
					 * 
					 * // 获得了可读的事件
					 */
				} else if (key.isReadable()) {
					read(key);
				}

			}

		}
	}

	/**
	 * 处理读取服务端发来的信息 的事件
	 * 
	 * @param key
	 * @throws IOException
	 */
	public void read(SelectionKey key) throws IOException {
		// 和服务端的read方法一样
		SocketChannel channel = (SocketChannel) key.channel();
		// 创建读取的缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(10);
		channel.read(buffer);
		byte[] data = buffer.array();
		String msg = new String(data).trim();
		System.out.println("client收到信息:" + msg);
		ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
		channel.write(outBuffer);// 将消息回送给客户端
	}

	/**
	 * 启动客户端测试
	 * 
	 * @throws IOException
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws IOException,
			InterruptedException {
		NIOClient client = new NIOClient();
		client.initClient("localhost", 8000);
		while (true) {
			System.out.println("clientselect " + client.selector.select()
					+ "event");
			Iterator ite = client.selector.selectedKeys().iterator();
			while (ite.hasNext()) {
				SelectionKey key = (SelectionKey) ite.next();
				// 删除已选的key,以防重复处理
				ite.remove();
				// 客户端请求连接事件
				Thread.sleep(1000);
				if (key.isConnectable()) {

					System.out.println("connectable event");
					SocketChannel channel = (SocketChannel) key.channel();
					// 如果正在连接,则完成连接
					if (channel.isConnectionPending()) {
						channel.finishConnect();
						channel.register(client.selector, SelectionKey.OP_READ);// 加了一个稳定的注册时间它就不返回0了
					}
					// System.out.println("debug1:"
					// +channel.socket().getLocalPort());
					// 获得和客户端连接的通道
					// SocketChannel channel = serverch.accept();
					// 设置成非阻塞
					// channel.configureBlocking(false);
				} else if (key.isReadable()) {
					SocketChannel channel = (SocketChannel) key.channel();
					// 创建读取的缓冲区
					ByteBuffer buffer = ByteBuffer.allocate(1010);
					channel.read(buffer);
					byte[] data = buffer.array();
					String msg = new String(data).trim();
					System.out.println("client收到信息:" + msg);
				}
			}

		}

		// client.listen();
	}

}

client输出:

clientselect 1event
connectable event
clientselect 1event
client收到信息:first sendmsg to client
clientselect 1event
client收到信息:second sendmsg to client

server输出:

select 1event
ssssssssss
yyyyy
debug1:59956



NIO的WRTIE事件

简单说 防止客户端(应该说接受方) 因网络问题等,无法接受发送方的消息,导致发送方while true 空循环 大量消耗cpu资源



java nio对OP_WRITE的处理解决网速慢的连接   refer to http://blog.sina.com.cn/s/blog_783ede0301013g5n.html

(2012-05-17 17:12:50)
标签:

java

nio

it

分类:JavaNIO
17.3.1   如何处理慢速的连接

    对企业级的服务器软件,高性能和可扩展性是基本的要求。除此之外,还应该有应对各种不同环境的能力。例如,一个好的服务器软件不应该假设所有的客户端都有很快的处理能力和很好的网络环境。如果一个客户端的运行速度很慢,或者网络速度很慢,这就意味着整个请求的时间变长。而对于服务器来说,这就意味着这个客户端的请求将占用更长的时间。这个时间的延迟不是由服务器造成的,因此CPU的占用不会增加什么,但是网络连接的时间会增加,处理线程的占用时间也会增加。这就造成了当前处理线程和其他资源得不到很快的释放,无法被其他客户端的请求来重用。例如Tomcat,当存在大量慢速连接的客户端时,线程资源被这些慢速的连接消耗掉,使得服务器不能响应其他的请求了。

NIO的异步非阻塞的形式,使得很少的线程就能服务于大量的请求。通过Selector的注册功能,可以有选择性地返回已经准备好的频道,这样就不需要为每一个请求分配单独的线程来服务。

在一些流行的NIO的框架中,都能看到对OP_ACCEPT和OP_READ的处理。很少有对OP_WRITE的处理。我们经常看到的代码就是在请求处理完成后,直接通过下面的代码将结果返回给客户端:

【例17.7】不对OP_WRITE进行处理的样例:

while (bb.hasRemaining()) {
    int len =socketChannel.write(bb);
    if (len <0) {
        throw new EOFException();
    }
}

这样写在大多数的情况下都没有什么问题。但是在客户端的网络环境很糟糕的情况下,服务器会遭到很沉重的打击。

因为如果客户端的网络或者是中间交换机的问题,使得网络传输的效率很低,这时候会出现服务器已经准备好的返回结果无法通过TCP/IP层传输到客户端。这时候在执行上面这段程序的时候就会出现以下情况。

(1) bb.hasRemaining()一直为“true”,因为服务器的返回结果已经准备好了。

(2) socketChannel.write(bb)的结果一直为0,因为由于网络原因数据一直传不过去。

(3)   因为是异步非阻塞的方式,socketChannel.write(bb)不会被阻塞,立刻被返回。

(4)   在一段时间内,这段代码会被无休止地快速执行着,消耗着大量的CPU的资源。事实上什么具体的任务也没有做,一直到网络允许当前的数据传送出去为止。

这样的结果显然不是我们想要的。因此,我们对OP_WRITE也应该加以处理。在NIO中最常用的方法如下。

【例17.8】一般NIO框架中对OP_WRITE的处理:

while (bb.hasRemaining()) {
    int len =socketChannel.write(bb);
    if (len <0){
        throw new EOFException();
    }
    if (len == 0) {
        selectionKey.interestOps(
                        selectionKey.interestOps() | SelectionKey.OP_WRITE);
        mainSelector.wakeup();
        break;
    }
}

上面的程序在网络不好的时候,将此频道的OP_WRITE操作注册到Selector上,这样,当网络恢复,频道可以继续将结果数据返回客户端的时候,Selector会通过SelectionKey来通知应用程序,再去执行写的操作。这样就能节约大量的CPU资源,使得服务器能适应各种恶劣的网络环境。

可是,Grizzly中对OP_WRITE的处理并不是这样的。我们先看看Grizzly的源码吧。在Grizzly中,对请求结果的返回是在ProcessTask中处理的,经过SocketChannelOutputBuffe r的类,最终通过OutputWriter类来完成返回结果的动作。在OutputWriter中处理OP_WRITE的代码如下:

【例17.9】Grizzly中对OP_WRITE的处理:

public static long flushChannel(SocketChannelsocketChannel,
        ByteBuffer bb, long writeTimeout) throwsIOException
{
    SelectionKey key =null;
    Selector writeSelector =null;
    int attempts = 0;
    int bytesProduced =0;
    try {
        while (bb.hasRemaining()) {
            int len =socketChannel.write(bb);
            attempts++;
            if (len< 0){
                throw newEOFException();
            }
            bytesProduced += len;
            if (len ==0) {
                if (writeSelector ==null){
                    writeSelector =SelectorFactory.getSelector();
                    if (writeSelector == null){
                        //Continue using the main one
                        continue;
                    }
                }
                key =socketChannel.register(writeSelector, key.OP_WRITE);
                if(writeSelector.select(writeTimeout) == 0) {
                    if (attempts > 2)
                        throw newIOException("Client disconnected");
                } else {
                    attempts--;
                }
            } else{
                attempts = 0;
            }
        }
    } finally {
        if (key != null) {
            key.cancel();
            key =null;
        }
        if (writeSelector != null) {
            // Cancelthe key.
            writeSelector.selectNow();
            SelectorFactory.returnSelector(writeSelector);
        }
    }
    returnbytesProduced;
上面的程序例17.9与例17.8的区别之处在于:当发现由于网络情况而导致的发送数据受阻(len==0)时,例17.8的处理是将当前的频道注册到当前的Selector中;而在例17.9中,程序从SelectorFactory中获得了一个临时的Selector。在获得这个临时的Selector之后,程序做了一个阻塞的操作:writeSelector.select(writeTimeout)。这个阻塞操作会在一定时间内(writeTimeout)等待这个频道的发送状态。如果等待时间过长,便认为当前的客户端的连接异常中断了。

这种实现方式颇受争议。有很多开发者置疑Grizzly的作者为什么不使用例17.8的模式。另外在实际处理中,Grizzly的处理方式事实上放弃了NIO中的非阻塞的优势,使用writeSelector.select(writeTimeout)做了个阻塞操作。虽然CPU的资源没有浪费,可是线程资源在阻塞的时间内,被这个请求所占有,不能释放给其他请求来使用。

Grizzly的作者对此的回应如下。

(1)   使用临时的Selector的目的是减少线程间的切换。当前的Selector一般用来处理OP_ACCEPT,和OP_READ的操作。使用临时的Selector可减轻主Selector的负担;而在注册的时候则需要进行线程切换,会引起不必要的系统调用。这种方式避免了线程之间的频繁切换,有利于系统的性能提高。

(2)   虽然writeSelector.select(writeTimeout)做了阻塞操作,但是这种情况只是少数极端的环境下才会发生。大多数的客户端是不会频繁出现这种现象的,因此在同一时刻被阻塞的线程不会很多。

(3)   利用这个阻塞操作来判断异常中断的客户连接。

(4)   经过压力实验证明这种实现的性能是非常好的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值