Netty学习笔记_7(Selector的基本使用及NIO小案例)

1、Selector(选择器)

1.1、基本介绍

  1. Java的NIO使用了非阻塞的io方式->一个线程处理若干个客户端连接,使用到了selector(选择器)
  2. Selector能够检测到多个注册通道上是否有事件发生(多个Channel以事件的形式注册到同一个selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。
  3. 只有在连接/通道真正有读写事件发生时,才会进行读写,减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
  4. 避免了多线程之间上下文切换导致的开销。

Netty的IO线程NioEventLoop聚合了Selector(选择器 / 多路复用器),可以并发处理成百上千个客户端连接。

当线程从某客户端Socket通道进行读写时,若没有数据可用,该线程可以进行其他任务。

线程通常将非阻塞IO的空闲时间用于其他通道上执行IO操作,所以单独的线程可以管理多个输入输出通道。

由于读写操作都是非阻塞的,就可以充分提高IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起。

一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO一连接一线程模型,架构性能、弹性伸缩能力和可靠性都得到极大地提升。


1.2、Selector类相关方法

1.2.1、Selector类是一个抽象类,常用方法:

Public abstract class Selector implement Closeable{

    Public static Selector open(); //得到一个选择器对象

    Public int select(long timeout); //监控所有注册的通道,当其中的IO操作可以进行时,将对应的selectionkey加入内部集合并返回,参数设置超时时间

    Public Set selectionKeys(); //从内部集合中得到所有的SelectionKey

}

Netty学习笔记_7(Selector的基本使用及NIO小案例)_第1张图片

1.2.2、Selector相关方法说明:

       Selector.select(); //若未监听到注册管道中有事件,则持续-阻塞

       Selector.select(1000); //阻塞1000毫秒,1000毫秒后返回

       Selector.wakeup(); //唤醒selector

       Selector.selectNow(); //不阻塞,立即返回

1.3、NIO非阻塞网络编程原理分析

1.3.1、NIO非阻塞网络编程相关(Selector,SelectionKey,ServerSocketChannel,SocketChannel)

        

Netty学习笔记_7(Selector的基本使用及NIO小案例)_第2张图片

说明

  1. 当客户端连接时,会通过SeverSocketChannel得到对应的SocketChannel
  2. Selector进行监听,调用select()方法,返回注册该Selector的所有通道中有事件发生的通道个数
  3. 将socketChannel注册到Selector上,public final SelectionKey register(Selector sel, int ops),一个selector上可以注册多个SocketChannel
  4. 注册后返回一个SelectionKey,会和该Selector关联(以集合的形式)
  5. 进一步得到各个SelectionKey(有事件发生
  6. 再通过SelectionKey反向获取SocketChannel,使用channnel()方法
  7. 可以通过得到的channel,完成业务

【注】SelectionKey中定义了四个操作标志位:OP_READ—表示通道中发生事件;OP_WRITE—表示通道中发生事件;OP_CONNECT—表示建立连接OP_ACCEPT请求新连接

 

1.3.2、NIO非阻塞网络编程案例

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

【2】设计思路

服务器端:

1)创建ServerSocketChannel,将其设置为非阻塞,并绑定一个端口进行监听。

2)创建一个Selector对象,将ServerSocketChannel注册到Selector上,并设置该Selector关注的事件(OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT)

3)调用select()方法,循环等待客户端channel的连接

4)一旦有客户端连接,则返回一个SelectionKey的集合

5)使用迭代器将集合中的Key取出,获取其对应通道的事件{

isAcceptable()

有新的客户端请求连接

isConnectable()

数据传输完成,连接结束

isReadable()

测试该通道是否有数据可以读取

isWritable()

测试该通道是否有数据写入

}

6)每处理完一个通道的请求,需要将其对应的Key从SelectionKey的集合中移除,防止操作被重复执行。

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

public class NIOServer {
    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel,类似ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //创建一个Selector对象
        Selector selector = Selector.open();

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

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

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

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

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

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

            //遍历集合
            Iterator iterator = selectionKeys.iterator();

            while (iterator.hasNext()){
                //获取到selectionKey
                SelectionKey key = iterator.next();

                //根据Key所对应的通道发生的事件做相应处理
                if (key.isAcceptable()){
                    //如果是OP_ACCEPT,表示有新的客户端连接
                    //给该客户端生成一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //将socketChannel设置为非阻塞
                    socketChannel.configureBlocking(false);
                    System.out.println("客户端连接成功,生成一个socketChannel "+socketChannel.hashCode());
                    //将当前的socketChannel注册到selector,关注事件为读事件,同时给socket Channel关联一个`buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                    System.out.println("客户端连接后,注册的selectionkey的数量 = "+selector.keys().size());
                }
                if (key.isReadable()){ //发生读事件
                    //通过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,防止重复操作
                iterator.remove();
            }


        }
    }
}

客户端:

1)首先同样要创建一个通道SocketChannel,类似与Socket通信编程,将其设置为非阻塞形式,并提供服务器IP和端口号进行连接。

2)客户端请求连接,服务端进行处理需要一定的时间,不妨设置一个等待时间,如果在规定的时间内没能连接到服务端,返回连接失败信息。

3)如果连接成功,则发送数据,发送方式参照之前的,通过SocketChannel的write方法写入到byteBuffer中。

import org.w3c.dom.ls.LSInput;

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

public class NIOClient {
    public static void main(String[] args) throws IOException {
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞模式
        socketChannel.configureBlocking(false);
        //提供服务器端的IP和端口
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if (!socketChannel.connect(socketAddress)){
            while (!socketChannel.finishConnect()){
                System.out.println("连接需要时间,客户端不会阻塞,可以做其他工作");
            }
        }

        //如果连接成功,就发送数据
        String str = "hello,NIO";
        //Wraps a byte array into a buffer
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        //发送数据,即将buffer中的数据写入到channel中
        socketChannel.write(byteBuffer);
        System.in.read();
    }
}

1.4、SelectionKey的相关方法

public abstract class SelectionKey {

     public abstract Selector selector();

     public abstract SelectableChannel channel();

     public final Object attachment()

     public abstract SelectionKey interestOps(int ops);

     public final boolean isReadable();

     public final boolean isWritable()

     public final boolean isAcceptable()

}

 

//得到与之关联的Selector对象

//得到与之关联的通道

//得到与之关联的共享数据

//设置或改变监听的事件类型

//测试通道是否可以读

//测试是否可写

//测试是否可以建立连接ACCEPT

1.5、SeverSocKetChannel API介绍

1、SeverSocKetChannel在服务器端监听新的客户端Socket连接

2、相关方法如下:

public abstract class ServerSocketChannel

    extends AbstractSelectableChannel

    implements NetworkChannel{

 

          public static ServerSocketChannel open();

//得到一个ServerSocketChannel通道

          public void bind(SocketAddress endpoint);

//设置服务器端口号

          public final SelectableChannel configureBlocking(boolean block);

//设置阻塞或非阻塞模式,取值false表示采用非阻塞模式

          public abstract SocketChannel accept();

//接受一个连接,返回代表这个连接的通道对象

          public final SelectionKey register(Selector sel, int ops); //注册一个选择器并设置监听事件
}  

1.6、SocketChannel API介绍

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

    public abstract class SocketChannel

        extends AbstractSelectableChannel Implements ByteChannel,       ScatteringByteChannel, GatheringByteChannel, NetworkChannel{

     

    public static SocketChannel open();

    //得到一个SocketChannel通道

    public final SelectableChannel configureBlocking(boolean block);

    //设置阻塞或非阻塞模式,取值false为非阻塞模式

    public abstract boolean connect(SocketAddress remote);

    //连接服务器

    public abstract boolean finishConnect();

    //如果上述方法连接失败,接下来则通过该方法完成链接

    public abstract int write(ByteBuffer src);

    //向通道中写入数据

    public abstract int read(ByteBuffer dst);

    //从通道中读取数据

    public final SelectionKey register(Selector sel, int ops,Object att);

    //注册一个选择器并设置监听事件,最后一个参数可以设置共享数据

    public final void close();

    //关闭通道
    }  

2、NIO网络编程应用实例-群聊系统

2.1、要求:

  1. 1、编写一个NIO群聊系统,实现服务器端与客户端的数据简单通讯(非阻塞)实现多人群聊
  2. 2、服务器端:可以检测用户上线,离线,并实现消息转发功能
  3. 3、客户端:通过channel可以无阻塞发送数据给其他所有用户,同时可以接收其他用户发送的消息(由服务器转发得到)
  4. 2.2、目的:进一步理解NIO非阻塞网络编程机制

2.3、设计思路:

2.3.1、先编写服务器端

  1. 服务器端自动并监听端口6667
  2. 服务器提示客户端上线和离线
  3. 服务器接收客户端信息,并实现转发
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.*;
    import java.util.Iterator;
    
    public class GroupChatServer {
        //定义相关的属性
        private Selector selector;
        private ServerSocketChannel listenChannel;
        private static final int PORT = 6667;
    
        //构造器 初始化操作
        public GroupChatServer(){
            try{
                //得到选择器
                selector = Selector.open();
                //初始化ServerSocketChannel
                listenChannel = ServerSocketChannel.open();
                //绑定端口
                listenChannel.socket().bind(new InetSocketAddress(PORT));
                //设置非阻塞
                listenChannel.configureBlocking(false);
                //将该listenChannel注册到selector
                listenChannel.register(selector, SelectionKey.OP_ACCEPT);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    
        //监听
        public void listen(){
            try{
                //循环处理
                while(true){
    
                    int count = selector.select();
                    if(count > 0){   //如果有事件则处理
                        //遍历得到的selectorKey集合
                        Iterator iterator = selector.selectedKeys().iterator();
                        while (iterator.hasNext()){
                            //取出SelectionKey
                            SelectionKey key = iterator.next();
    
                            //监听到ACCEPT事件
                            if (key.isAcceptable()){
                                SocketChannel sc = listenChannel.accept();
                                sc.configureBlocking(false);
                                //将该sc注册到selector上
                                sc.register(selector,SelectionKey.OP_READ);
    
                                //提示上线了
                                System.out.println(sc.getRemoteAddress()+"上线了。。。");
    
                            }
                            if (key.isReadable()){
                                //通道发生read事件
                                //专门处理读数据的方法
                                readData(key);
    
                            }
    
                            //将当前的key删除,防止重复处理
                            iterator.remove();
    
                        }
    
    
                    }else{
                        System.out.println("等待....");
                    }
                }
    
            }catch(Exception e){
                e.printStackTrace();
    
            }finally {
                //发生的异常处理
            }
        }
    
        //读取客户端消息
        private void readData(SelectionKey key){
    
            //定义一个SocketChannel
            SocketChannel channel = null;
            try{
                //取到关联的channel
                channel = (SocketChannel) key.channel();
                //创建缓冲buffer
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    
                int count = channel.read(byteBuffer);
    
                //根据count的值做处理
                if (count>0){
                    //把缓冲区的数据转换成字符串
                    String msg = new String(byteBuffer.array());
    
                    //输出该消息
                    System.out.println("from 客户端:"+ msg);
    
                    //向其他客户端转发消息,专门写一个方法处理
                    sendInfoToOthers(msg,channel);
                }
    
            }catch (IOException e){
                //e.printStackTrace();
    
                try {
                    System.out.println(channel.getRemoteAddress() + "已下线");
                    //取消注册
                    key.cancel();
                    //关闭通道
                    channel.close();
                }catch (Exception r){
                    r.printStackTrace();
                }
            }
        }
    
        //转发消息给其他的客户,实际是转发给其他通道************需要排除自身
        private void sendInfoToOthers(String msg, SocketChannel self) throws IOException {
    
            //服务器转发消息
            System.out.println("服务器转发消息中。。。");
            //遍历所有注册到selector的socketchannel并排除自身
            for (SelectionKey key: selector.keys()){
    
                //反向获取通道
                Channel targetchannel = key.channel();
    
                //排除自身
                if (targetchannel instanceof SocketChannel && targetchannel !=self){
    
                    //转型
                    SocketChannel dest = (SocketChannel) targetchannel;
    
                    //将msg存储到buffer中
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
    
                    //将buffer中的数据写入通道
                    dest.write(buffer);
                }
            }
    
        }
    
        public static void main(String[] args) {
    
            //创建一个服务器对象
            GroupChatServer groupChatServer = new GroupChatServer();
    
            //监听
            groupChatServer.listen();
        }
    }
    

     

2.3.2、编写客户端

  1. 连接服务器
  2. 向服务器发送消息
  3. 接收服务器消息
    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;
    import java.util.Set;
    
    public class GroupChatClient {
        //定义相关的属性
        private final String HOST = "127.0.0.1";
        private final int PORT = 6667;
        private Selector selector;
        private SocketChannel socketChannel;
        private String username;
    
        //构造器,完成初始化工作
        public GroupChatClient() throws IOException {
            selector = Selector.open();
            socketChannel = socketChannel.open(new InetSocketAddress(HOST,PORT));
            socketChannel.configureBlocking(false);
            //将channel注册到selector、
            socketChannel.register(selector, SelectionKey.OP_READ);
            //得到用户名
            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(username + " is ready...");
        }
    
        //向服务器发送消息
        public void sendInfo(String info){
    
            info = username + "说" + info;
    
            try {
                socketChannel.write(ByteBuffer.wrap(info.getBytes()));
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    
        //从服务器端读取消息
        public void readInfo(){
            try {
                int readChannels = selector.select();
                if (readChannels > 0){
                    //有可用的通道
                    Iterator iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()){
                        SelectionKey key = iterator.next();
    
                        if (key.isReadable()){
                            //得到相关的通道
                            SocketChannel sc = (SocketChannel) key.channel();
                            //得到一个buffer
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            //读取
                            sc.read(buffer);
                            //把读取到的缓冲区数据转成字符串
                            String msg = new String(buffer.array());
                            System.out.println(msg.trim());
                        }
                    }
    
                    iterator.remove(); //删除当前的selectionKey,防止重复操作
                }else {
                    //System.out.println("没有可用的通道");
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws IOException {
    
            //启动客户端
            GroupChatClient chatClient = new GroupChatClient();
    
            //启动一个线程,每隔3秒,读取从服务器端发送的数据
            new Thread(){
                public void run(){
                    while (true){
                        chatClient.readInfo();
    
                        try{
                            Thread.currentThread().sleep(3000);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
    
            //发送数据给服务器
            Scanner scanner = new Scanner(System.in);
    
            while (scanner.hasNextLine()){
                String s = scanner.nextLine();
                chatClient.sendInfo(s);
            }
    
        }
    
    }

     

 

你可能感兴趣的:(Netty,笔记)