使用NIO从阻塞式通信到非阻塞式通信——各种示例循序渐进带你理解

使用NIO进行阻塞式通信

在该工程根目录放置了一个图片1.jpg。以下为客户端代码,使用SocketChannel来连接上服务端,将本地文件的字节流通过SocketChannel发送给了服务端。

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);

        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (inChannel.read(buf) != -1) {  //从本地文件读出数据,写入到buffer
            buf.flip();  //切换为buffer的读模式
            sChannel.write(buf);  //向SocketChannel写入数据
            buf.clear();  //切换为buffer的写模式
        }
        
        inChannel.close();
        sChannel.close();
    }
}

以下为服务端代码,它接受一个SocketChannel的数据(即客户端),并将数据写入到新建的本地文件中(若文件存在则覆盖)。

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.bind(new InetSocketAddress(9898));
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),
            StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        SocketChannel sChannel = ssChannel.accept();  //先获取一个来自客户端的SocketChannel
        System.out.println("BlockingServer accept");  
        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (sChannel.read(buf) != -1) {  //从SocketChannel中读出数据,写入到buffer中
            buf.flip();  //切换为buffer的读模式
            outChannel.write(buf);  //向本地文件写入数据
            buf.clear();  //切换为buffer的写模式
        }
        System.out.println("already get data, and output to local file");

        sChannel.close();
        outChannel.close();
        ssChannel.close();
    }
}

执行顺序是:先运行服务端代码,但你会发现线程回阻塞在ssChannel.accept()这行,服务端为一直保持这种状态,直到你再运行客户端代码,然后你会发现,客户端和服务端都会执行完了,本地会新增一个文件。

  • 如果你没有执行服务端代码,先去执行客户端代码,将会报错java.net.ConnectException: Connection refused: connect。通过debug发现,在第一句SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898))会抛出这个异常,因为这句会尝试去连接服务端。

分析阻塞过程

现在让我们给服务端加上一点log:

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.bind(new InetSocketAddress(9898));
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),
            StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        SocketChannel sChannel = ssChannel.accept();
        System.out.println("after BlockingServer accept");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("before loop");
        int i = 1;
        while (sChannel.read(buf) != -1) {
            System.out.println("read from channel loop "+i++);
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        System.out.println("already get data, and output to local file");
        sChannel.close();
        outChannel.close();
        ssChannel.close();
    }
}

然后在客户端代码加上断点,进行debug模式。(run服务端,debug客户端)

  • 当客户端执行完SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));这句后,服务端打印出了after BlockingServer accept before loop。这说明服务端一直阻塞在SocketChannel sChannel = ssChannel.accept();这里,直到客户端执行完了SocketChannel.open操作,服务端才停止阻塞,继续往下执行。
  • 但服务端没有打印出一句read from channel loop来,说明之后服务端又阻塞在了sChannel.read(buf)这里。
  • 当客户端第一次执行完sChannel.write(buf);这句后,服务端打印出了read from channel loop 1。这说明服务端一直阻塞在sChannel.read(buf)这里,直到客户端执行完了sChannel.write(buf);,服务端才停止阻塞,第一次从sChannel读出了数据,然后继续循环,在下一次的sChannel.read(buf)继续阻塞(可以通过继续debug来观察得出)。

当还有一个问题是,服务端这边是怎么退出循环的呢,让我们重新运行服务端,然后debug客户端,去掉之前所有断点,然后在客户端的sChannel.close()打一个断点。

  • 在客户端执行sChannel.close()之前,服务端这边打印了很多条read from channel loop(条数根据你的图片大小),但始终没有打印出already get data, and output to local file,这说明客户端这边其实已经没有往channel里写入数据,但是服务端还是继续阻塞在了sChannel.read(buf)这里。
  • 说明需要有一个信号告诉服务端,我这边已经结束写入数据,服务端不用阻塞在sChannel.read(buf)这里了。
  • 在客户端执行sChannel.close()之后,服务端这边打印了最后一条log,并结束了程序。这说明客户端执行sChannel.close()之后,会让服务端不再阻塞在sChannel.read(buf)这里,并且使得sChannel.read(buf)的返回值为-1。
  • 将客户端sChannel.close()替换为sChannel.shutdownOutput()也能有同样效果。

反过来,client读取数据,server发送数据

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.text.SimpleDateFormat;
import java.util.Date;

public class BlockingServer_WriteToSocket {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.bind(new InetSocketAddress(9898));
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);

        SocketChannel sChannel = ssChannel.accept();
        System.out.println("BlockingServer accept");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("befor loop");
        int i = 1;
        while (inChannel.read(buf) != -1) {
            System.out.println("BlockingServer write to channel "+ i++ + " "+new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date()));
            buf.flip();
            sChannel.write(buf);  //服务端发送数据
            buf.clear();
        }
        System.out.println("already get data, and output to local file");

        sChannel.close();
        inChannel.close();
        ssChannel.close();
    }
}

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingClient_ReadFromSocket {
    public static void main(String[] args) throws IOException {
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),
                StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (sChannel.read(buf) != -1) {  //客户端读取数据
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }

        outChannel.close();
        sChannel.close();
    }
}

运行后根目录新增一个图片。

client和server同时写入

//server code
package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.text.SimpleDateFormat;
import java.util.Date;

public class BlockingServer_WriteToSocket {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.bind(new InetSocketAddress(9898));
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);

        SocketChannel sChannel = ssChannel.accept();
        System.out.println("BlockingServer accept");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("befor loop");
        int i = 1;
        while (inChannel.read(buf) != -1) {
            System.out.println("BlockingServer write to channel "+ i++ + " "+new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date()));
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        System.out.println("already get data, and output to local file");

        sChannel.close();
        inChannel.close();
        ssChannel.close();
    }
}
//client code
package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.text.SimpleDateFormat;
import java.util.Date;

public class BlockingClient_WriteToSocket {
    public static void main(String[] args) throws IOException {
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        FileChannel inChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.READ);

        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("befor loop");
        int i = 1;
        while (inChannel.read(buf) != -1) {
            System.out.println("BlockingClient write to channel "+i++ + " "+new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date()));
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        System.out.println("already get data, and output to local file");

        inChannel.close();
        sChannel.close();
    }
}

先后运行server和client,你会发现只有服务端最后一次写入channel执行后,客户端才会开始第一次写入数据到channel里,然后由于服务端关闭了socket连接,客户端报错。

//服务端打印,前面省略
BlockingServer write to channel 52 2020/03/15-14:58:17:665
BlockingServer write to channel 53 2020/03/15-14:58:17:665
BlockingServer write to channel 54 2020/03/15-14:58:17:665
already get data, and output to local file

//客户端打印
befor loop
BlockingClient write to channel 1 2020/03/15-14:58:17:670
BlockingClient write to channel 2 2020/03/15-14:58:17:671
Exception in thread "main" java.io.IOException: 你的主机中的软件中止了一个已建立的连接。

但不要以为这是由于channel不能两端同时写入造成的,改变一下测试方法,在服务端的System.out.println("already get data, and output to local file");这句打断点,然后先debug服务端,再run客户端。从二者的打印结果可以看到,二者向channel写入的时间段,大部分是重合的。所以,是可以两端同时写入的。

client和server同时读取

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingServer_ReadFromSocket {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.bind(new InetSocketAddress(9898));
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),
                StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        SocketChannel sChannel = ssChannel.accept();
        System.out.println("BlockingServer accept");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (sChannel.read(buf) != -1) {
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        System.out.println("already get data, and output to local file");

        sChannel.close();
        outChannel.close();
        ssChannel.close();
    }
}

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingClient_ReadFromSocket {
    public static void main(String[] args) throws IOException {
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),
                StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (sChannel.read(buf) != -1) {
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }

        outChannel.close();
        sChannel.close();
    }
}

这个运行结果就很明显了,因为双方都未曾写入过任何数据,所以从channel中读取数据肯定会一直阻塞在read那里。

服务端加上反馈

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.bind(new InetSocketAddress(9898));
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),
            StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        SocketChannel sChannel = ssChannel.accept();
        System.out.println("after BlockingServer accept");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("before loop");
        int i = 1;
        while (sChannel.read(buf) != -1) {
            System.out.println("read from channel loop "+i++);
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        System.out.println("already get data, and output to local file");
		// 服务端退出循环后,让channel输出数据
        i = sChannel.write(ByteBuffer.wrap("我是服务端,这是给你的反馈".getBytes()));
        System.out.println(i);
        sChannel.shutdownOutput();  //这里不用这句也能成功,因为下面有close

        sChannel.close();
        outChannel.close();
        ssChannel.close();
        System.out.println("after channel is close");
    }
}
package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BlockingClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);

        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (inChannel.read(buf) != -1) {
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        sChannel.shutdownOutput();  //这句必须加,不然服务端那边会一直阻塞在read那里
		//客户端向channel发送完数据后,就开始从channel读取数据
        int len = 0;
        while ((len = sChannel.read(buf)) != -1) {
            buf.flip();
            System.out.println(new String(buf.array(), 0 ,len));
            buf.clear();
        }

        sChannel.close();
        inChannel.close();
        System.out.println("after channel is close");

    }
}

运行后,客户端控制台会打印出我是服务端,这是给你的反馈

关于shutdownOutput和shutdownInput

虽然上面所有例子为了让另一方能够从read循环退出,而使用了shutdownOutput,但使用它并不是一个好主意。

  • 不论客户端还是服务端,现把当前所处的程序称为local socket,把另一端称为remote socket。
  • 假如local socket调用了shutdownOutput,那么local socket则再也无法write to channel了,并且remote socket也再无法read from channel了。
  • 同样的,假如local socket调用了shutdownInput,那么local socket则再也无法read from channel了,并且remote socket也再无法write to channel了。
  • 所以,当你的socket想要进行多次收发数据时,使用这两个函数并不是一个好的选择。

使用NIO进行非阻塞式通信

服务端代码:

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class NonBlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.bind(new InetSocketAddress(9898));
        ssChannel.configureBlocking(false);  //ServerSocketChannel切换非阻塞模式
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),
            StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        SocketChannel sChannel = null;
        while ((sChannel = ssChannel.accept()) == null) {  //这里不再阻塞,所以循环直到有连接进来
            System.out.println("finding accept channel");
        }
        sChannel.configureBlocking(false);  //获得的SocketChannel也切换非阻塞模式
        System.out.println("after BlockingServer accept");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("before loop");
        int i = 1; int len = 0;
        while ((len = sChannel.read(buf)) != -1) {  //这里不再阻塞
            //当客户端没有数据发来时,将一直打印no data
            System.out.println("read from channel loop "+i++ + (len > 0 ? " have "+len+" data":" no data"));
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        System.out.println("already get data, and output to local file");

        byte[] sendBytes = "我是服务端,这是给你的反馈".getBytes();
        ByteBuffer sendBuff = ByteBuffer.wrap(sendBytes);
        len = sChannel.write(sendBuff);
        System.out.println("sendBuff" +" "+len);
        while (sendBuff.remaining() > 0) {  //写数据是有可能没有写完的,所以需要循环
            sChannel.write(sendBuff);
            System.out.println("sendBuff" +" "+len);
        }

        sChannel.close();
        outChannel.close();
        ssChannel.close();
        System.out.println("after channel is close");
    }
}

客户端代码:

package TestNIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class NonBlockingClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        sChannel.configureBlocking(false);
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);

        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (inChannel.read(buf) != -1) {
            buf.flip();
            sChannel.write(buf);
            while (buf.remaining() > 0) {
                sChannel.write(buf);
            }
            buf.clear();
        }
        sChannel.shutdownOutput();

        int len = 0;
        while ((len = sChannel.read(buf)) != -1) {
            buf.flip();
            System.out.println(new String(buf.array(), 0 ,len));
            buf.clear();
        }

        sChannel.close();
        inChannel.close();
        System.out.println("after channel is close");
    }
}

将之前的例子使用非阻塞模式进行升级,确实有了些变化(读者可以通过run服务端,debug客户端并加不同的断点来观察效果,):

  • 直到客户端执行了SocketChannel.open之前,服务端都会一直进行while ((sChannel = ssChannel.accept()) == null)的循环。
  • 直到客户端执行了sChannel.write(buf),服务端len = sChannel.read(buf)这句read的结果都一直会是0。
  • 当客户端开始执行包含sChannel.write(buf)的循环时,服务端的打印结果也并不都是have data,它可能是下面这样,毕竟到下一次数据可读时总会有个时间间隔,这个间隔总以再执行两次循环了。
...
read from channel loop 587 have 1024 data
read from channel loop 588 no data
read from channel loop 589 no data
read from channel loop 590 have 1024 data
read from channel loop 591 no data
read from channel loop 592 no data
read from channel loop 593 have 1024 data
read from channel loop 594 no data
read from channel loop 595 no data
read from channel loop 596 have 1024 data
read from channel loop 597 have 358 data
read from channel loop 598 no data    //之后一直都没有数据来了
read from channel loop 599 no data
...
  • 当执行sChannel.write(sendBuff)时,由于现在是非阻塞模式,所以还得考虑一种情况:就是buffer里的数据并没有完全写入到Channel中,所以还需要检测sendBuff.remaining()是否等于0.

使用NIO+Selector进行非阻塞式通信

调动阻塞的select方法

客户端代码:

package NonBlocking;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Scanner;

public class NonBlockingClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
        socketChannel.configureBlocking(false);

        ByteBuffer buf=ByteBuffer.allocate(1024);
        Scanner scanner =new Scanner(System.in);
        while(scanner.hasNext()){
            String inputStr=scanner.next();
            buf.put((new Date().toString()+"\n"+inputStr).getBytes());
            buf.flip();
            socketChannel.write(buf);
            while (buf.remaining() > 0) {
                socketChannel.write(buf);
            }
            buf.clear();
        }
        
        scanner.close();
        socketChannel.close();
    }
}

服务端代码:

package NonBlocking;

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 NonBlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8888));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  //首先注册ACCEPT事件

		int result = 0; int i = 1;
        while((result = selector.select()) > 0) {  //遍历获得就绪事件
        	System.out.println(String.format("selector %dth loop, ready event number is %d", i++, result));
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){  //就绪事件可能不止一个
                SelectionKey sk=iterator.next();

                if(sk.isAcceptable()){  //如果是ACCEPT,那么与之关联的channel肯定是个ServerSocketChannel
                    ServerSocketChannel ss = (ServerSocketChannel)sk.channel();
                    SocketChannel socketChannel = ss.accept();
                    socketChannel.configureBlocking(false);  //也切换非阻塞
                    socketChannel.register(selector, SelectionKey.OP_READ);  //注册read事件
                }
                else if(sk.isReadable()){    //如果是READ,那么与之关联的channel肯定是个SocketChannel
                    SocketChannel socketChannel = (SocketChannel)sk.channel();
                    ByteBuffer buf=ByteBuffer.allocate(1024);
                    int len=0;
                    while((len=socketChannel.read(buf))>0){
                        buf.flip();
                        System.out.println(new String(buf.array(),0,len));
                        buf.clear();
                    }
                }
                iterator.remove();
            }
        }
        serverSocketChannel.close();
    }
}

实现效果是一个单向的聊天室。

为了进一步分析过程,debug客户端:

可以发现调用selector.select()时,服务端会阻塞在这里。只有当有客户端的连接到来,或者已建立连接的channel有可读数据,才会停止阻塞,然后下一次循环继续阻塞。

调动非阻塞的select方法

把上面的例子的服务端代码的selector.select()改成selector.selectNow()(从不阻塞)或者selector.select(1000)(代表阻塞一秒后停止阻塞),然后你运行服务端代码,你会发现服务端的程序自己马上就会退出,如果是selector.select(1000)则一秒后退出。

  • selector.selectNow()从不阻塞,调用后的返回值可能为0,为0代表当前没有就绪事件。
  • selector.select(1000)阻塞一秒后停止阻塞,调用后的返回值可能为0,为0代表一秒后没有就绪事件。

所以为了让上面的服务端能够正常运行,你必须把循环改成死循环,然后在循环体里select。

        int result = 0; int i = 1;
        while(true) {  //遍历获得就绪事件
            result = selector.select(1000);
            System.out.println(String.format("selector %dth loop, ready event number is %d", i++, result));
            if (result == 0) {
                continue;
            }
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){  //就绪事件可能不止一个
                SelectionKey sk=iterator.next();

                if(sk.isAcceptable()){  //如果是ACCEPT,那么与之关联的channel肯定是个ServerSocketChannel
                    ServerSocketChannel ss = (ServerSocketChannel)sk.channel();
                    SocketChannel socketChannel = ss.accept();
                    socketChannel.configureBlocking(false);  //也切换非阻塞
                    socketChannel.register(selector, SelectionKey.OP_READ);  //注册read事件
                }
                else if(sk.isReadable()){    //如果是READ,那么与之关联的channel肯定是个SocketChannel
                    SocketChannel socketChannel = (SocketChannel)sk.channel();
                    ByteBuffer buf=ByteBuffer.allocate(1024);
                    int len=0;
                    while((len=socketChannel.read(buf))>0){
                        buf.flip();
                        System.out.println(new String(buf.array(),0,len));
                        buf.clear();
                    }
                }
                iterator.remove();
                System.out.println("after remove key");
            }
        }

为了让执行效果更好,建议使用selector.select(1000),因为使用selector.selectNow()的话,log会刷屏,程序也在不停的运行(因为一直没有就绪事件)。

关于write事件

  • 其实write事件大部分时候不用注册,因为绝大多数情况下channel都是可write的,只要你本机的内核缓冲区没有满,那么就是可write的了。
  • 正确处理是:读出数据后,注册WRITE事件,然后调用attach方法来暂存你要发的数据。当WRITE事件就绪后,再通过attachment()函数取出之前要发的数据。

使用WRITE事件

服务端代码:

package NonBlocking;

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 NonBlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8888));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  //首先注册ACCEPT事件

        int result = 0; int i = 1;
        while(true) {  //遍历获得就绪事件
            result = selector.select(1000);
            System.out.println(String.format("selector %dth loop, ready event number is %d", i++, result));
            if (result == 0) {
                continue;
            }
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){  //就绪事件可能不止一个
                SelectionKey sk=iterator.next();

                if(sk.isAcceptable()){  //如果是ACCEPT,那么与之关联的channel肯定是个ServerSocketChannel
                    System.out.println("服务端有ACCEPT事件就绪");
                    ServerSocketChannel ss = (ServerSocketChannel)sk.channel();
                    SocketChannel socketChannel = ss.accept();
                    socketChannel.configureBlocking(false);  //也切换非阻塞
                    socketChannel.register(selector, SelectionKey.OP_READ);  //注册read事件
                }
                else if(sk.isReadable()){    //如果是READ,那么与之关联的channel肯定是个SocketChannel
                    System.out.println("服务端有READ事件就绪");
                    SocketChannel socketChannel = (SocketChannel)sk.channel();
                    ByteBuffer buf=ByteBuffer.allocate(1024);
                    int len=0;
                    StringBuilder sb = new StringBuilder();
                    while((len=socketChannel.read(buf))>0){
                        buf.flip();
                        String s  = new String(buf.array(),0,len);
                        sb.append(s);
                        buf.clear();
                    }
                    //服务端开始响应消息
                    socketChannel.register(selector, SelectionKey.OP_WRITE);  
                    String sendStr = "您的消息'"+sb.toString()+"'我已经收到了";
                    System.out.println(sendStr);
                    sk.attach(ByteBuffer.wrap(sendStr.getBytes()));
                }
                else if(sk.isWritable()){
                    System.out.println("服务端有WRITE事件就绪");
                    SocketChannel socketChannel = (SocketChannel)sk.channel();

                    ByteBuffer buf = (ByteBuffer) sk.attachment();
                    while (buf.remaining() >0) {
                        socketChannel.write(buf);
                    }
                    socketChannel.register(selector, SelectionKey.OP_READ);  //注册read事件
                }
                iterator.remove();
                System.out.println("after remove key");
            }
        }
    }
}

客户端代码:

package NonBlocking;

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.Scanner;

public class NonBlockingClient {
    static SocketChannel socketChannel = null;
    static Selector selector = null;

    public static void main(String[] args) throws IOException {
        socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
        socketChannel.configureBlocking(false);
        selector = Selector.open();

        new Thread(new Runnable() {
            @Override
            public void run () {
                ByteBuffer buf=ByteBuffer.allocate(1024);
                Scanner scanner =new Scanner(System.in);
                while(scanner.hasNext()){
                    String inputStr=scanner.next();
                    buf.put(inputStr.getBytes());
                    buf.flip();
                    try {
                        socketChannel.write(buf);
                        while (buf.remaining() > 0) {
                            socketChannel.write(buf);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    buf.clear();
                }
                scanner.close();
            }
        }).start();

        socketChannel.register(selector, SelectionKey.OP_READ);
        int result = 0; int i = 1;
        while((result = selector.select()) > 0) {
            System.out.println(String.format("selector %dth loop, ready event number is %d", i++, result));
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey sk=iterator.next();

                if (sk.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel)sk.channel();
                    ByteBuffer buf=ByteBuffer.allocate(1024);
                    int len=0;
                    while((len=socketChannel.read(buf))>0){
                        buf.flip();
                        System.out.println(new String(buf.array(), 0 ,len));
                        buf.clear();
                    }
                }
                iterator.remove();
            }
        }
        socketChannel.close();
    }
}

客户端运行效果如下:

你好
selector 1th loop, ready event number is 1
您的消息'你好'我已经收到了
我擦
selector 2th loop, ready event number is 1
您的消息'我擦'我已经收到了
好无聊啊
selector 3th loop, ready event number is 1
您的消息'好无聊啊'我已经收到了
  • 客户端另起一个线程来读取控制台输入并输出给服务端(客户端在write时,并没有使用WRITE事件)。
  • 服务端读取数据,并加上自己的数据,通过WRITE事件 写回给客户端。
  • 在服务端开始响应消息时,socketChannel.register(selector, SelectionKey.OP_WRITE);必须在sk.attach(ByteBuffer.wrap(sendStr.getBytes()));之前,否则服务端在处理WRITE事件时,从sk.attachment()取出的对象为空指针。因为注册事件,如果使用的是两个参数的版本,那么会调用到重载版本register(Selector sel, int ops, Object att),其中第三个则为null,所以会把attachment给清空的。
  • 服务端里的socketChannel.register(selector, SelectionKey.OP_WRITE),可用sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);同理socketChannel.register(selector, SelectionKey.OP_READ),可用sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE)代替。效果一样。

如果写缓冲区满了

如果写缓冲区满了,ByteBuffer里还有未读数据,但write的返回结果已经是0了。此时只能再次等待下一次WRITE事件了。

修改服务端:

                else if(sk.isWritable()){
                    System.out.println("服务端有WRITE事件就绪");
                    SocketChannel socketChannel = (SocketChannel)sk.channel();

                    ByteBuffer buf = (ByteBuffer) sk.attachment();
                    while (buf.remaining() >0) {
                        socketChannel.write(buf);
                    }
                    // 循环结束后,buf还有数据未读
                    if (buf.remaining() > 0) {  //可能读了buffer一部分,也可能一点也没读
                        //sk.attach(buf);  //不用再次attach,因为sk会一直保持附件的引用
                    } else {
                        sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
                    }
                }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值