java nio

来源:互联网 发布:月中天 知乎 编辑:程序博客网 时间:2024/06/16 01:22

一.同步阻塞IO

BIO就是阻塞式的IO,网络通信中对于多客户端的连入,服务器端总是与客户端数量一致的线程去处理每个客户端任务,即,客户端与线程数1:1,并且进行读写操作室阻塞的,当有你成千上完的客户端进行连接,就导致服务器不断的建立新的线程,最后导致低通资源不足,后面的客户端不能连接服务器,并且连接入的客户端并不是总是在于服务器进行交互,很可能就只是占用着资源而已。

二.伪异步IO

伪异步IO对同步IO进行了优化,后端通过一个线程池和任务队列去处理所有客户端的请求,当用完后在归还给线程池,线程池的原理和数据库连接池的原理很像,他减少了线程创建和销毁的时间,无论多少客户端的连接都是这些固定的线程数量去处理,这在大量客户端与服务器信息交互量很少的情况下,是一种很好的处理方式,但是想一种情况,当有一个客户端的读取信息非常慢时,服务器对其的写操作时会很慢,甚至会阻塞很长时间,因为线程池中的线程是有限的,当有客户端需要分配线程时,就会导致新任务在队列中一直等待阻塞的客户端释放线程。当任务队列已经满时,就会有大量的用户发生连接超时。其实,伪异步IO也是同步阻塞IO。

 

三.javaNIO原理

为了解决上面的两种阻塞IO的缺陷,java在1.4版本开始加入NIO也称为new IO,异步IO模型。网络通信中,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道来实现,可以设置阻塞余非阻塞两种模式,为了实现高负载高并发都采取非阻塞的模式。NIO采用缓冲区BUFFER,实现对数据的读写操作,缓冲区是固定大小,并由内部状态记录有多少数据被放入或者取出。与阻塞IO不同,阻塞IO采用阻塞式流(Stream)的方式进行读写,流是单向的只能向一个方向读数据或者写数据,而通道是双向的,可以同时在通道上发送和读取数据,而且是非阻塞的,在没有数据可读可写时可以去做别的事情。

NIO改进了上面的一对一或者M:N的模型。服务器仅采用一个一个线程去处理所有客户端线程,这就需要创建一个selector,并将其注册到想要监控的信道上(注意是通过channel的方法来实现),并返回一个selectorKey实例(包含通道和select以及感兴趣的操作),selector就好像是一个观察者,通过不断轮询所注册的一组通道上有没有等待的操作发生,当等待事件发生的时候可以做其他事情,当有信道出现感兴趣的操作,则该信道就进入就绪状态。

Slector的select方法阻塞等待又没有就绪的通道,当出现就绪的信道或者等待超时返回,就绪信道的个数,若等待超时则返回-1,selectedKeys方法返回就绪的信道。

下面附代码

这是处理感兴趣信道的接口,因为他可以放在多个服务器上所以把他做成了接口。

?
1
2
3
4
5
6
7
8
9
10
packageNio;
 
importjava.io.IOException;
importjava.nio.channels.SelectionKey;
 
publicinterfaceTCPProtocol {
    voidhandleAccept(SelectionKey key) throwsIOException;
    voidhandleRead(SelectionKey key) throwsIOException;
    voidhandleWrite(SelectionKey key) throwsIOException;
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<span style="font-family:FangSong_GB2312;">这是对上面接口的具体实现</span>
packageNio;
 
importjava.io.IOException;
importjava.nio.ByteBuffer;
importjava.nio.channels.SelectionKey;
importjava.nio.channels.ServerSocketChannel;
importjava.nio.channels.SocketChannel;
 
publicclassSelectorProtocol implementsTCPProtocol {
    privateintbufSize ;
    publicSelectorProtocol(intbuffsize){
        this.bufSize = buffsize;
    }
 
     //服务端信道已经准备好了接收新的客户端连接 
    publicvoidhandleAccept(SelectionKey key) throwsIOException { 
        SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept(); 
        clntChan.configureBlocking(false); 
        //将选择器注册到连接到的客户端信道,并指定该信道key值的属性为OP_READ,同时为该信道指定关联的附件 
        clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize)); 
    
   
    //客户端信道已经准备好了从信道中读取数据到缓冲区 
    publicvoidhandleRead(SelectionKey key) throwsIOException{ 
        SocketChannel clntChan = (SocketChannel) key.channel(); 
        //获取该信道所关联的附件,这里为缓冲区 
        ByteBuffer buf = (ByteBuffer) key.attachment(); 
        longbytesRead = clntChan.read(buf); 
        //如果read()方法返回-1,说明客户端关闭了连接,那么客户端已经接收到了与自己发送字节数相等的数据,可以安全地关闭 
        if(bytesRead == -1){  
            clntChan.close(); 
        }elseif(bytesRead > 0){ 
        //如果缓冲区总读入了数据,则将该信道感兴趣的操作设置为为可读可写 
        key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); 
        
    
       
    //客户端信道已经准备好了将数据从缓冲区写入信道 
    publicvoidhandleWrite(SelectionKey key) throwsIOException { 
    //获取与该信道关联的缓冲区,里面有之前读取到的数据 
    ByteBuffer buf = (ByteBuffer) key.attachment(); 
    //重置缓冲区,准备将数据写入信道 
    buf.flip();  
    SocketChannel clntChan = (SocketChannel) key.channel(); 
    //将数据写入到信道中 
    clntChan.write(buf); 
    if(!buf.hasRemaining()){  
    //如果缓冲区中的数据已经全部写入了信道,则将该信道感兴趣的操作设置为可读 
      key.interestOps(SelectionKey.OP_READ); 
    
    //为读入更多的数据腾出空间 
    buf.compact();  
  
}
客户端

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
packageNio;
importjava.net.InetSocketAddress;
importjava.net.SocketException;
importjava.nio.ByteBuffer;
importjava.nio.channels.SocketChannel;
 
publicclassTCPEchoClientNonblocking {
    publicstaticvoid main(String args[]) throwsException{
        //第一个参数作为要连接的服务端的主机名或IP
        String server = "localhost";
        //第二个参数为要发送到服务端的字符串
        byte[] argument = "nihaopengyou".getBytes();
        //如果有第三个参数,则作为端口号,如果没有,则端口号设为7
        intservPort = 2002;
        //创建一个信道,并设为非阻塞模式
        SocketChannel clntChan = SocketChannel.open();
        clntChan.configureBlocking(false);
        //向服务端发起连接
        if(!clntChan.connect(newInetSocketAddress(server, servPort))){
            //不断地轮询连接状态,直到完成连接
            while(!clntChan.finishConnect()){
                //在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性
                //这里为了演示该方法的使用,只是一直打印"."
                System.out.print("."); 
            }
        }
        //为了与后面打印的"."区别开来,这里输出换行符
        System.out.print("\n");
        //分别实例化用来读写的缓冲区
        ByteBuffer writeBuf = ByteBuffer.wrap(argument);
        ByteBuffer readBuf = ByteBuffer.allocate(argument.length);
        //接收到的总的字节数
        inttotalBytesRcvd = 0;
        //每一次调用read()方法接收到的字节数
        intbytesRcvd;
        //循环执行,直到接收到的字节数与发送的字符串的字节数相等
        while(totalBytesRcvd < argument.length){
            //如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道
            if(writeBuf.hasRemaining()){
                clntChan.write(writeBuf);
            }
            //如果read()接收到-1,表明服务端关闭,抛出异常
            if((bytesRcvd = clntChan.read(readBuf)) == -1){
                thrownewSocketException("Connection closed prematurely");
            }
            //计算接收到的总字节数
            totalBytesRcvd += bytesRcvd;
            //在等待通信完成的过程中,程序可以执行其他任务,以体现非阻塞IO的异步特性
            //这里为了演示该方法的使用,同样只是一直打印"."
            System.out.print(".");
        }
        //打印出接收到的数据
        System.out.println("Received: " newString(readBuf.array(),0, totalBytesRcvd));
        //关闭信道
        clntChan.close();
    }
}

服务器

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
packageNio;
importjava.io.IOException;
importjava.net.InetSocketAddress;
importjava.nio.channels.SelectionKey;
importjava.nio.channels.Selector;
importjava.nio.channels.ServerSocketChannel;
importjava.util.Iterator;
 
publicclassTCPServerSelector{
    //缓冲区的长度
    privatestaticfinal int BUFSIZE = 256;
    //select方法等待信道准备好的最长时间
    privatestaticfinal int TIMEOUT = 3000;
    publicstaticvoid main(String[] args) throwsIOException {
        if(args.length < 1){
            thrownewIllegalArgumentException("Parameter(s): <port> ...");
        }
        //创建一个选择器
        Selector selector = Selector.open();
        for(String arg : args){
            //实例化一个信道
            ServerSocketChannel listnChannel = ServerSocketChannel.open();
            //将该信道绑定到指定端口
            listnChannel.socket().bind(newInetSocketAddress(Integer.parseInt(arg)));
            System.out.println("启动服务器"+Integer.parseInt(arg));
            //配置信道为非阻塞模式
            listnChannel.configureBlocking(false);
            //将选择器注册到各个信道
            listnChannel.register(selector, SelectionKey.OP_ACCEPT);
        }
        //创建一个实现了协议接口的对象
        TCPProtocol protocol = newSelectorProtocol(BUFSIZE);
        //不断轮询select方法,获取准备好的信道所关联的Key集
        while(true){
            //一直等待,直至有信道准备好了I/O操作
            if(selector.select(TIMEOUT) == 0){
                //在等待信道准备的同时,也可以异步地执行其他任务,
                //这里只是简单地打印"."
                System.out.print(".");
                continue;
            }
            //获取准备好的信道所关联的Key集合的iterator实例
            Iterator<selectionkey> keyIter = selector.selectedKeys().iterator();
            //循环取得集合中的每个键值
            while(keyIter.hasNext()){
                SelectionKey key = keyIter.next();
                //如果服务端信道感兴趣的I/O操作为accept
                if(key.isAcceptable()){
                    protocol.handleAccept(key);
                }
                //如果客户端信道感兴趣的I/O操作为read
                if(key.isReadable()){
                    protocol.handleRead(key);
                }
                //如果该键值有效,并且其对应的客户端信道感兴趣的I/O操作为write
                if(key.isValid() && key.isWritable()) {
                    protocol.handleWrite(key);
                }
                //这里需要手动从键集中移除当前的key
                keyIter.remove();
            }
        }
    }
}
</selectionkey></port>

运行结果:

 

\\

原创粉丝点击