JavaNIO(4)、SocketChannel

套接字的某些操作可能会无限期地阻塞,如accept()方法的调用可能会因为等待一个客户端连接而阻塞,read()方法的调用可能会因为没有数据可读而阻塞。

NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。

在非阻塞式的信道上调用一个方法总是会立即返回。这种调用的返回值指示了所请求的操作完成的程度。例如,在一个非阻塞式的ServerSocketChannel上调用accept()方法,如果有连接请求来了,则返回客户端SocketChannel,否则返回null。

这里举一个Socket通信的例子,客户端使用NIO实现,服务端先使用BIO实现。

客户端实现

public class NIOSocketClient {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        SocketChannel socketChannel = null;

        try {
            socketChannel = SocketChannel.open();
            //配置为非阻塞
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
            if(socketChannel.finishConnect()){
                int i = 0;
                while(true){
                    TimeUnit.SECONDS.sleep(1);
                    String msg = ++i + "times msg from client";
                    //重置缓冲区
                    buffer.clear();
                    //对缓冲区设值
                    buffer.put(msg.getBytes());
                    //准备读取缓冲区
                    buffer.flip();

                    //循环写入缓冲区数据到通道中 可能一次写不完 所以在循环中处理
                    while (buffer.hasRemaining()){
                        System.out.println(buffer);
                        //从缓冲区写入数据到通道中
                        socketChannel.write(buffer);
                    }
                }
            }
        } catch (IOException | InterruptedException  e) {
            e.printStackTrace();
        }
        finally {
            try {
                if (socketChannel != null) {
                    socketChannel.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
}

服务端的BIO实现,该实现同时只能处理一个客户端连接

public class BIOSocketServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        InputStream in = null;

        try {
            serverSocket = new ServerSocket(8080);
            int recvMsgSize = 0;
            byte[] recvBuf = new byte[1024];
            while(true){
                Socket clntSocket = serverSocket.accept();          //该方法阻塞 直到有连接进来为止
                SocketAddress clientAddr = clntSocket.getRemoteSocketAddress();
                System.out.println("a client is linked," + clientAddr);
                in = clntSocket.getInputStream();
                //read方法将阻塞 直到有数据为止
                while ((recvMsgSize = in.read(recvBuf)) != -1){
                    byte[] temp = new byte[recvMsgSize];
                    System.arraycopy(recvBuf,0,temp,0,recvMsgSize);
                    System.out.println(new String(temp));
                }
            }

        } catch (IOException e){

        }
        finally {

        }
    }
}

服务端的NIO实现。该实现中使用了Selector选择器,该选择器监控多个事件,当有一个事件就绪时,会通知程序,并指出是哪个信道哪个事件。在linux下的底层实现是I/O多路复用的epoll模型。

public class NIOSocketServer {
    private static final int BUF_SIZE = 1024;            //buffer长度
    private static final int PORT = 8080;               //监听端口
    private static final int TIMEOUT = 3000;            //select()方法超时时长

    public static void main(String[] args) {
        selector();
    }

    public static void selector(){
        Selector selector = null;
        ServerSocketChannel ssc = null;

        try{
            //创建Selector
            selector = Selector.open();
            //打开ServerSocketChannel
            ssc = ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress(PORT));
            //Channel和Selector组合使用 必须把Channel设置为非阻塞模式
            ssc.configureBlocking(false);
            //注册Channel到Selector上
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            while (true){
                //select里面加入一个超时时间
                //防止程序卡死的假象
                if(selector.select(TIMEOUT) == 0){
                    System.out.println("===");
                    continue;
                }
                //有事件发生了 遍历事件
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while (iter.hasNext()){
                    SelectionKey key = iter.next();
                    if(key.isAcceptable()){
                        //处理客户端连接
                        handleAccpet(key);
                    }
                    if(key.isReadable()){
                        //处理读操作
                        handleRead(key);
                    }
                    if(key.isWritable() && key.isValid()){
                        //处理写操作
                        handleWrite(key);
                    }

                    if(key.isConnectable()){
                        System.out.println("连接成功...");
                    }
                    //处理完后 将当前事件移除 避免重复处理
                    iter.remove();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            try{
                if(selector != null){
                    selector.close();
                }

                if(ssc != null){
                    //关闭ServerSocketChannel
                    ssc.close();
                }

            }catch (IOException e){
                e.printStackTrace();
            }
        }

    }

    public static void handleAccpet(SelectionKey key) throws IOException {
        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
        //这里会马上返回 因为目前已经有一个连接事件已经准备就绪
        SocketChannel sc = ssChannel.accept();
        sc.configureBlocking(false);
        sc.register(key.selector(),SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUF_SIZE));
    }

    public static void handleRead(SelectionKey key) throws IOException {
        SocketChannel sc = (SocketChannel)key.channel();
        ByteBuffer buffer = (ByteBuffer)key.attachment();
        long bytesRead = sc.read(buffer);
        while(bytesRead > 0){
            //准备读缓冲区
            buffer.flip();
            while (buffer.hasRemaining()){
                //读取一个字节
                System.out.print((char)buffer.get());
            }
            System.out.println();
            //重置缓冲区
            buffer.clear();
            bytesRead = sc.read(buffer);
        }

        if(bytesRead == -1){
            sc.close();
        }
    }

    public static void handleWrite(SelectionKey key) throws IOException {
        ByteBuffer buffer = (ByteBuffer)key.attachment();
        buffer.flip();
        SocketChannel sc = (SocketChannel)key.channel();
        while (buffer.hasRemaining()){
            sc.write(buffer);
        }
        buffer.compact();
    }
}

程序说明

在selector方法中,我们只对accept事件感兴趣

当accept事件就绪时,表示有一个连接进来了,在handleAccpet方法中注册read事件的监控

当read事件就绪时,表示客户端有数据过来了,在handleRead方法中处理读取数据操作

上面的程序中并没有注册write事件,所以Selector中不会有write事件就绪

Selector说明

创建Selector

selector = Selector.open();

为了将Channel和Selector配合使用,必须将Channel注册到Selector上,通过SelectableChannel类中的register()方法来实现。

//打开ServerSocketChannel
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
//Channel和Selector组合使用 必须把Channel设置为非阻塞模式
ssc.configureBlocking(false);
//注册Channel到Selector上
ssc.register(selector, SelectionKey.OP_ACCEPT);

与Selector一起使用时,Channel必须处于非阻塞模式下。

这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字通道都可以。

register()方法的第二个参数是一个interest集合,有以下四种选择:

  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_WRITE
  • SelectionKey.OP_READ

对应下面4个事件

  • Accept
  • Connect
  • Write
  • Read

SelectionKey

当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含下面一些属性:

  • interest集合

    在register()方法中,interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合

  • ready集合

    ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。可以这样访问ready集合

    int readySet = selectionKey.readyOps();
    
  • Channel

    访问Channel

    Channel channel = selectionKey.channel();
    
  • Selector

    访问Selector

    Selector selector = selectionKey.selector();
    
  • 附加的对象(可选)

    可以将一个对象或者更多信息附加在SelectionKey上,这样就能方便的识别某个给定的通道。

    selectionKey.attach(theObject);
    Object attachedObj = selectionKey.attachment();
    

    或者在register方法中附加对象

    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
    

通过Selector选择通道

一旦向Selector注册了一个或多个通道,就可以调用几个重载的select()方法。

这些方法返回你所感兴趣的事件,如连接,接受,读,写 这些已经准备就绪的通道。

select()方法有以下几个重载:

  • int select()
  • int select(long timeout)
  • int selectNow()

select()阻塞到至少有一个通道在你注册的事件上就绪为止

select(long timeout)阻塞到一个注册事件就绪或者超时为止

selectNow()不会阻塞,如果没有事件就绪,直接返回0

返回int值表示有多少通道已经就绪

如果返回值大于0,则表示至少有一个事件准备就绪,可以通过下列语句查看就绪的状态

Set selectedKeys = selector.selectedKeys();

当处理完成一个事件时,必须从就绪集合中移除该事件。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值