Selector和非阻塞网络编程

ServerSocketChannel 和 SocketChannel

传统的网络编程,比如TCP的 socket.accept() 方法和UDP的 receive(packet) 方法都是具有阻塞功能的,所以属于同步阻塞网络编程。

ServerSocketChannel、SocketChannel可以实现非阻塞式网络编程。

ServerSocketChannel是一个基于通道的socket监听器,等同于ServerSocket类。
SocketChannel是一个基于通道的客户端套接字,等同于Socket类。

基本步骤

服务器端
1.创建ServerSocketChannel,服务器套接字
2.绑定地址
3.监听客户的套接字,创建连接
4.创建缓冲区ByteBuffer处理数据
5.关闭

客户端
1.创建SocketChannel
2.创建缓冲区ByteBuffer处理数据
3.发送数据
4.关闭

代码实现

//服务器
public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1.创建服务器端ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();

        //2.绑定地址和端口号,注意地址不是IP地址而是Socket的InetAddress地址
        //SocektInetAddress = ip + port
        ssc.bind(new InetSocketAddress(InetAddress.getByName("10.9.21.249"), 8848));

        //3.监听客户端
        System.out.println("服务器启动。。。。");
        SocketChannel socketChannel = ssc.accept();

        //4.处理数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (socketChannel.read(buffer) > 0) {
            buffer.flip();
            System.out.println("客户端说:" + new String(buffer.array(), 0, buffer.limit()));
            buffer.clear();
        }

        //5.关闭
        socketChannel.close();
        ssc.close();
    }
}

//客户端
public class TCPClient {
    public static void main(String[] args) throws IOException {
        //1.创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getByName("10.9.21.249"), 8848));

        //2.处理数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Scanner input = new Scanner(System.in);
        while (true) {
            String data = input.nextLine();
            buffer.put(data.getBytes());

            //必须转模式,保证position指针归零,方便后面通道的读取
            buffer.flip();

            //3.发送数据
            socketChannel.write(buffer);
            buffer.clear();
            if  (data.equals("88")) {
                break;
            }
        }
        //4.关闭
        input.close();
        socketChannel.close();
    }
}

Selector轮询器

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。
这个方法会一直阻塞到某个注册的通道有事件就绪。
一旦这个方法返回,线程就可以处理这些事件,比如新连接进来的客户端,数据接收等。
选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。

Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。但是不适合一个线程长时间操作
Selector和非阻塞网络编程_第1张图片

实现轮询机制的三大组成部分

选择器(Selector): Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。

可选择通道(SelectableChannel): SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。
因为FileChannel类没有继承SelectableChannel因此是不是可选通道,而所有socket通道都是可选择的,SocketChannel和ServerSocketChannel是SelectableChannel的子类,可以注册轮询器。

选择键(SelectionKey): 选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),选择键支持四种操作类型:

  • Connect 连接
  • Accept 接受请求(常用)
  • Read 读(常用)
  • Write 写

Java中定义了四个常量来表示这四种操作类型:

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

实现非阻塞式网络通信

//服务器
public class Server {
    public static void main(String[] args) throws IOException {
        //1.创建ServerSocektChannel
        ServerSocketChannel listener = ServerSocketChannel.open();

        //2.绑定
        listener.bind(new InetSocketAddress(InetAddress.getLocalHost(), 7788));

        //3.设置为非阻塞模式
        listener.configureBlocking(false);

        //4.轮询器启动
        Selector selector = Selector.open();

        //5.将服务器注册到轮询器,并未接收模式(就是图中的线程部分)
        listener.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器已启动");

        //6.开始轮询,这里处理的是事件,而不是客户端的SocketChannel
        while (selector.select() > 0) {
            //将所有的请求都加载到集合中,依次处理
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> it = selectionKeys.iterator();
            while (it.hasNext()) {
                //处理某一事件
                SelectionKey selectionKey = it.next();
                //事件分为对接请求事件和数据传输事件(对接之后的事件)
                //处理请求,与BIO编程中的while(true)循环创建监听一样,但是轮询器会帮助服务器筛选,一个个对接处理
                if (selectionKey.isAcceptable()) {
                    //服务器的监听就可以创建客户端的SocketChannel了,会一直存在
                    SocketChannel socketChannel = listener.accept();
                    //客户端的请求必须设置为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //对接成功,设置成功,注册到轮询器,处理后续的数据传输,模式为读取
                    socketChannel.register(selector, SelectionKey.OP_READ);
                //处理数据
                } else if (selectionKey.isReadable()) {
                    //由于已经注册都轮询器上了,这里获取SocketChannel就必须通过SelectionKey获取,也就是从轮询器上搜索客户端的SocketChannel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024 * 8);
                    int len = -1;
                    while ((len = socketChannel.read(buffer)) > 0) {
                        buffer.flip();
                        String data = new String(buffer.array(), 0, buffer.limit());

                        //这个地址是将SocketAddress转为InetSocketeAddress,然后getAddress获取ip地址
                        InetSocketAddress inetSocketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
                        System.out.println(inetSocketAddress.getAddress() + "说" + data);

                        //这个地址返回的是SocketAddress类型,包含的是Ip+端口号
                        //SocketAddress remoteAddress = socketChannel.getRemoteAddress();
                        //System.out.println(remoteAddress + "说" + data);
                        buffer.clear();
                    }

                    //如果读到了数据末尾,则是-1,必须关闭通道,因为客户端很多,不可能最后统一关闭通道
                    //但已经注册的SocketChannel一直在,只是关闭了管道,后面如果有数据事件,则直接重启
                    if (-1 == len) {
                        socketChannel.close();
                    }
                }

                //处理完毕之后,将该事件删除,继续处理后面的事件
                it.remove();
            }
        }
    }
}

//客户端,基本没有变化,就是必须转为非阻塞模式
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(), 7788));

        //2.设置为非阻塞
        socketChannel.configureBlocking(false);

        //3.发送数据
        Scanner input = new Scanner(System.in);
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 8);
        while (true) {
            String data = input.nextLine();
            buffer.put(data.getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
            if (data.equals("88")) {
                break;
            }
        }

        //4.关闭
        input.close();
        socketChannel.close();
    }
}

Selector和非阻塞网络编程_第2张图片

你可能感兴趣的:(java之NIO,Java之网络编程)