NIO 非阻塞网络编程快速入门

案例:

编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)

目的:理解 NIO 非阻塞网络编程机制

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;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws Exception{
        //创建ServerSocketChannel -> ServerSocket

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //得到一个Selecor对象
        Selector selector = Selector.open();

        //绑定一个端口6666, 在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        //把 serverSocketChannel 注册到  selector 关心 事件为 OP_ACCEPT       pos_1
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1



        //循环等待客户端连接
        while (true) {

            //这里我们等待1秒,如果没有事件发生, 返回
            if(selector.select(1000) == 0) { //没有事件发生
                System.out.println("服务器等待了1秒,无连接");
                continue;
            }

            //如果返回的>0, 就获取到相关的 selectionKey集合
            //1.如果返回的>0, 表示已经获取到关注的事件
            //2. selector.selectedKeys() 返回关注事件的集合
            //   通过 selectionKeys 反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            System.out.println("selectionKeys 数量 = " + selectionKeys.size());

            //遍历 Set<SelectionKey>, 使用迭代器遍历
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while (keyIterator.hasNext()) {
                //获取到SelectionKey
                SelectionKey key = keyIterator.next();
                //根据key 对应的通道发生的事件做相应处理
                if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
                    //该该客户端生成一个 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept(); //这里不会阻塞,会马上执行
                    System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
                    //将  SocketChannel 设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
                    //关联一个Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                    System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..


                }
                if(key.isReadable()) {  //发生 OP_READ

                    //通过key 反向获取到对应channel
                    SocketChannel channel = (SocketChannel)key.channel();

                    //获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("from 客户端 " + new String(buffer.array()));

                }

                //手动从集合中移动当前的selectionKey, 防止重复操作
                keyIterator.remove();

            }

        }

    }
}

pos1:

1、对操作系统有一定了解的同学,就会大概知道这里监听的是一个Accept通道。这个通道的 作用就是监听,实际建立连接了还会有一个通道。
2、简单说一下为什么。因为客户端发请求的时候,服务器这边是肯定要先有一个监听通道,
监听某个端口是否有客户端要建立链接,如果有客户端想要建立链接,那么会再创建一个和 客户端真正通信的通道。
3、如果有其它客户端还想要建立链接,这个Accept监听端口监听到了,就会再创建几个真正 的通信通道。
4、也就是Server的一个端口可以建立多个TCP连接,因为IP层协议通过 目标地址+端口+源地址+源端口四个信息识别一个上下文

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {
    public static void main(String[] args) throws Exception{
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //提供服务器端的ip 和 端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {

            while (!socketChannel.finishConnect()) {
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
            }
        }

        //...如果连接成功,就发送数据
        String str = "hello, gujie~";
        //Wraps a byte array into a buffer,根绝字节数组的大小来调节buffer的大小
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //发送数据,将 buffer 数据写入 channel
        socketChannel.write(buffer);
        System.in.read();
    }
}

运行结果

java Mono 阻塞获取结果_java


注意,为了可以让IDEA让客户端和服务端同时运行可以选择以下方式

java Mono 阻塞获取结果_java Mono 阻塞获取结果_02


java Mono 阻塞获取结果_java Mono 阻塞获取结果_03


注意:selector.selectedKeys()和selector.keys()不同,一个选择的所有注册过的客户端链接,一个是选择发生过事件的客户端链接。

SelectionKey

1.SelectionKey,表示 Selector 和网络通道的注册关系,共四种:
int OP_ACCEPT:有新的网络连接可以 accept,值为 16
int OP_CONNECT:代表连接已经建立,值为 8
int OP_READ:代表读操作,值为 1
int OP_WRITE:代表写操作,值为 4
————————————————
源码中:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

2.SelectionKey 相关方法

* public abstract Selector selector(),得到与之,关联的Selector对象
* public abstract SelectableChannel channel(),得到与之关联的通道
* public final Object attachment(),得到与之关联的共享数据
* public abstract SelectionKey interestOps(int ops),设置或改变监听事件
*  public final boolean isAcceptable(), 是否可以accept
* public final boolean isReadable(),是否可以读
* public final boolean isWritable(),是否可以写

java Mono 阻塞获取结果_客户端_04

ServerSocketChannel

1.ServerSocketChannel 在服务器端监听新的客户端 Socket 连接,负责监听,不负责实际的读写操作

2.相关方法如下

* public static ServerSocketChannel open(),得到- - 个ServerSocketChannel通道
* public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
* public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
* public SocketChannel accept(),接受-一个连接,返回代表这个连接的通道对象
* public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件

java Mono 阻塞获取结果_客户端_05

SocketChannel

1.SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
2.相关方法如下

* public static ServerSocketChannel open(),得到一个ServerSocketChannel通道
* public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
* public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值false 表示采用非阻塞模式
* public SocketChannel accept(),接受-一个连接,返回代表这个连接的通道对象
* public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件

java Mono 阻塞获取结果_netty_06


ServerSocketChannel在服务端主要负责有一个连接来了,就生成一个socketChannel,而SocketChannel在客户端则主要负责数据的读写。