Java NIO Selector详解(含多人聊天室实例)

2023-05-16

一、Java NIO 的核心组件

Java NIO的核心组件包括:Channel(通道),Buffer(缓冲区),Selector(选择器),其中Channel和Buffer比较好理解
简单来说 NIO是面向通道和缓冲区的,意思就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中。

关于Channel 和 Buffer的详细讲解请看:Java NIO 教程

二、Java NIO Selector

1. Selector简介

选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。

在开始之前,需要回顾一下Selector、SelectableChannel和SelectionKey:

选择器(Selector)

Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

可选择通道(SelectableChannel)

SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。因为FileChannel类没有继承SelectableChannel因此是不是可选通道,而所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

选择键(SelectionKey)

选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

下面是使用Selector管理多个channel的结构图:
这里写图片描述

2. Selector的使用

(1)创建Selector

Selector对象是通过调用静态工厂方法open()来实例化的,如下:

Selector Selector=Selector.open();

类方法open()实际上向SPI1发出请求,通过默认的SelectorProvider对象获取一个新的实例。

(2)将Channel注册到Selector

要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:

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

通过调用通道的register()方法会将它注册到一个选择器上。与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常,这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字通道都可以。另外通道一旦被注册,将不能再回到阻塞状态,此时若调用通道的configureBlocking(true)将抛出BlockingModeException异常。

register()方法的第二个参数是“interest集合”,表示选择器所关心的通道操作,它实际上是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。比如一个选择器对通道的read和write操作感兴趣,那么选择器在检查该通道时,只会检查通道的read和write操作是否已经处在就绪状态。
它有以下四种操作类型:

  • Connect 连接
  • Accept 接受
  • Read 读
  • Write 写

需要注意并非所有的操作在所有的可选择通道上都能被支持,比如ServerSocketChannel支持Accept,而SocketChannel中不支持。我们可以通过通道上的validOps()方法来获取特定通道下所有支持的操作集合。

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

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

如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int interestSet=SelectionKey.OP_READ|SelectionKey.OP_WRITE;
当通道触发了某个操作之后,表示该通道的某个操作已经就绪,可以被操作。因此,某个SocketChannel成功连接到另一个服务器称为“连接就绪”(OP_CONNECT)。一个ServerSocketChannel准备好接收新进入的连接称为“接收就绪”(OP_ACCEPT)。一个有数据可读的通道可以说是“读就绪”(OP_READ)。等待写数据的通道可以说是“写就绪”(OP_WRITE)。

我们注意到register()方法会返回一个SelectionKey对象,我们称之为键对象。该对象包含了以下四种属性:

  • interest集合
  • read集合
  • Channel
  • Selector

interest集合是Selector感兴趣的集合,用于指示选择器对通道关心的操作,可通过SelectionKey对象的interestOps()获取。最初,该兴趣集合是通道被注册到Selector时传进来的值。该集合不会被选择器改变,但是可通过interestOps()改变。我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:

   int interestSet=selectionKey.interestOps();
   boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

read集合是通道已经就绪的操作的集合,表示一个通道准备好要执行的操作了,可通过SelctionKey对象的readyOps()来获取相关通道已经就绪的操作。它是interest集合的子集,并且表示了interest集合中从上次调用select()以后已经就绪的那些操作。(比如选择器对通道的read,write操作感兴趣,而某时刻通道的read操作已经准备就绪可以被选择器获知了,前一种就是interest集合,后一种则是read集合。)。JAVA中定义以下几个方法用来检查这些操作是否就绪:

    //int readSet=selectionKey.readOps();
    selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();

需要注意的是,通过相关的选择键的readyOps()方法返回的就绪状态指示只是一个提示,底层的通道在任何时候都会不断改变,而其他线程也可能在通道上执行操作并影响到它的就绪状态。另外,我们不能直接修改read集合。

取出SelectionKey所关联的Selector和Channel
通过SelectionKey访问对应的Selector和Channel:

Channel channel =selectionKey.channel();
Selector selector=selectionKey.selector();

关于取消SelectionKey对象的那点事

我们可以通过SelectionKey对象的cancel()方法来取消特定的注册关系。该方法调用之后,该SelectionKey对象将会被”拷贝”至已取消键的集合中,该键此时已经失效,但是该注册关系并不会立刻终结。在下一次select()时,已取消键的集合中的元素会被清除,相应的注册关系也真正终结。

(3)为SelectionKey绑定附加对象

可以将一个或者多个附加对象绑定到SelectionKey上,以便容易的识别给定的通道。通常有两种方式:
1 在注册的时候直接绑定:

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

2 在绑定完成之后附加:

selectionKey.attach(theObject);//绑定

绑定之后,可通过对应的SelectionKey取出该对象:

selectionKey.attachment();。

如果要取消该对象,则可以通过该种方式:

selectionKey.attach(null).

需要注意的是如果附加的对象不再使用,一定要人为清除,因为垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。

一个单独的通道可被注册到多个选择器中,有些时候我们需要通过isRegistered()方法来检查一个通道是否已经被注册到任何一个选择器上。 通常来说,我们并不会这么做。

(4)通过Selector选择通道

我们知道选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中。接下来我们简单的了解一下Selector维护的三种类型SelectionKey集合:

已注册的键的集合(Registered key set)

所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys()方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

已选择的键的集合(Selected key set)

已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(并有可能是空的)。
不要将已选择的键的集合与ready集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。

已取消的键的集合(Cancelled key set)

已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

在刚初始化的Selector对象中,这三个集合都是空的。通过Selector的select()方法可以选择已经准备就绪的通道(这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:

select():阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒。
selectNow():非阻塞,只要有通道就绪就立刻返回。

select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合。如下:
Set selectedKeys=selector.selectedKeys();
进而可以放到和某SelectionKey关联的Selector和Channel。如下所示:

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

关于Selector执行选择的过程

我们知道调用select()方法进行通道,现在我们再来深入一下选择的过程,也就是select()执行过程。当select()被调用时将执行以下几步:

  1. 首先检查已取消键集合,也就是通过cancle()取消的键。如果该集合不为空,则清空该集合里的键,同时该集合中每个取消的键也将从已注册键集合和已选择键集合中移除。(一个键被取消时,并不会立刻从集合中移除,而是将该键“拷贝”至已取消键集合中,这种取消策略就是我们常提到的“延迟取消”。)

  2. 再次检查已注册键集合(准确说是该集合中每个键的interest集合)。系统底层会依次询问每个已经注册的通道是否准备好选择器所感兴趣的某种操作,一旦发现某个通道已经就绪了,则会首先判断该通道是否已经存在在已选择键集合当中,如果已经存在,则更新该通道在已注册键集合中对应的键的ready集合,如果不存在,则首先清空该通道的对应的键的ready集合,然后重设ready集合,最后将该键存至已注册键集合中。这里需要明白,当更新ready集合时,在上次select()中已经就绪的操作不会被删除,也就是ready集合中的元素是累积的,比如在第一次的selector对某个通道的read和write操作感兴趣,在第一次执行select()时,该通道的read操作就绪,此时该通道对应的键中的ready集合存有read元素,在第二次执行select()时,该通道的write操作也就绪了,此时该通道对应的ready集合中将同时有read和write元素。

深入已注册键集合的管理

到现在我们已经知道一个通道的的键是如何被添加到已选择键集合中的,下面我们来继续了解对已选择键集合的管理 。首先要记住:选择器不会主动删除被添加到已选择键集合中的键,而且被添加到已选择键集合中的键的ready集合只能被设置,而不能被清理。如果我们希望清空已选择键集合中某个键的ready集合该怎么办?我们知道一个键在新加入已选择键集合之前会首先置空该键的ready集合,这样的话我们可以人为的将某个键从已注册键集合中移除最终实现置空某个键的ready集合。被移除的键如果在下一次的select()中再次就绪,它将会重新被添加到已选择的键的集合中。这就是为什么要在每次迭代的末尾调用keyIterator.remove()。

(5)停止选择

选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。

  1. 通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回
    该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
  2. 通过close()方法关闭Selector**
    该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。
  3. 调用interrupt()
    调用该方法会使睡眠的线程抛出InterruptException异常,捕获该异常并在调用wakeup()

上面有些人看到“系统底层会依次询问每个通道”时可能在想如果已选择键非常多是,会不会耗时较长?答案是肯定的。但是我想说的是通常你可以选择忽略该过程,至于为什么,后面再说。

三、NIO多人聊天室

服务端

public class ChatServer implements Runnable{

    private Selector selector;
    private SelectionKey serverKey;
    private Vector<String> usernames;
    private static final int PORT = 9999;

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public ChatServer(){
        usernames = new Vector<String>();
        init();
    }

    public void init(){
        try {
            selector = Selector.open();
            //创建serverSocketChannel
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            ServerSocket socket = serverChannel.socket();
            socket.bind(new InetSocketAddress(PORT));
            //加入到selector中
            serverChannel.configureBlocking(false);
            serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            printInfo("server starting.......");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while(true){
                //获取就绪channel
                int count = selector.select();
                if(count > 0){
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
                        SelectionKey key = iterator.next();

                        //若此key的通道是等待接受新的套接字连接
                        if(key.isAcceptable()){
                            System.out.println(key.toString() + " : 接收");
                            //一定要把这个accpet状态的服务器key去掉,否则会出错
                            iterator.remove();
                            ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                            //接受socket
                            SocketChannel socket = serverChannel.accept();
                            socket.configureBlocking(false);
                            //将channel加入到selector中,并一开始读取数据
                            socket.register(selector, SelectionKey.OP_READ);
                        }
                        //若此key的通道是有数据可读状态
                        if(key.isValid() && key.isReadable()){
                            System.out.println(key.toString() + " : 读");
                            readMsg(key);
                        }
                        //若此key的通道是写数据状态
                        if(key.isValid() && key.isWritable()){
                            System.out.println(key.toString() + " : 写");
                            writeMsg(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readMsg(SelectionKey key) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) key.channel();
            //设置buffer缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //假如客户端关闭了通道,这里在对该通道read数据,会发生IOException,捕获到Exception后,关闭掉该channel,取消掉该key
            int count = channel.read(buffer);
            StringBuffer buf = new StringBuffer();
            //如果读取到了数据
            if(count > 0){
                //让buffer翻转,把buffer中的数据读取出来
                buffer.flip();
                buf.append(new String(buffer.array(), 0, count));
            }
            String msg = buf.toString();

            //如果此数据是客户端连接时发送的数据
            if(msg.indexOf("open_") != -1){
                String name = msg.substring(5);//取出名字
                printInfo(name + " --> online");
                usernames.add(name);
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey skey = iter.next();
                    //若不是服务器套接字通道的key,则将数据设置到此key中  
                    //并更新此key感兴趣的动作  
                    if(skey != serverKey){
                        skey.attach(usernames);
                        skey.interestOps(skey.interestOps() | SelectionKey.OP_WRITE);
                    }
                }
                //如果是下线时发送的数据
            }else if(msg.indexOf("exit_") != -1){
                String username = msg.substring(5);
                usernames.remove(username);
                key.attach("close");
                //要退出的当前channel加上close的标示,并把兴趣转为写,如果write中收到了close,则中断channel的链接
                key.interestOps(SelectionKey.OP_WRITE);
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey sKey = iter.next();
                    sKey.attach(usernames);
                    sKey.interestOps(sKey.interestOps() | SelectionKey.OP_WRITE);
                }
                //如果是聊天发送数据
            }else{
                String uname = msg.substring(0, msg.indexOf("^"));
                msg = msg.substring(msg.indexOf("^") + 1);
                printInfo("("+uname+")说:" + msg);
                String dateTime = sdf.format(new Date());
                String smsg = uname + " " + dateTime + "\n  " + msg + "\n";
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey sKey = iter.next();
                    sKey.attach(smsg);
                    sKey.interestOps(sKey.interestOps() | SelectionKey.OP_WRITE);
                }
            }
            buffer.clear();
        } catch (IOException e) {
            //当客户端关闭channel时,服务端再往通道缓冲区中写或读数据,都会报IOException,解决方法是:在服务端这里捕获掉这个异常,并且关闭掉服务端这边的Channel通道
            key.cancel();
            try {
                channel.socket().close();
                channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    private void writeMsg(SelectionKey key) {
        try {
            SocketChannel channel = (SocketChannel) key.channel();
            Object attachment = key.attachment();
            //获取key的值之后,要把key的值置空,避免影响下一次的使用
            key.attach("");
            channel.write(ByteBuffer.wrap(attachment.toString().getBytes()));
            key.interestOps(SelectionKey.OP_READ);

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

    private void printInfo(String str) {
        System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
    }

    public static void main(String[] args) {
        ChatServer server = new ChatServer();
        new Thread(server).start();
    }
}

注意这里readMsg 和 writeMsg中,read操作的key重新设置interest要遍历所有key,而write操作的key重新设置interest只需要设置传入的当前key,原因:
读操作之所以要遍历key,是因为这里channel的读写操作的流程是:
1. read到数据后,把数据加到每一个key的attach中
2. 写数据时,从key的attach中取出数据,从而把该数据写到buffer中

例如:当选择器有3个channel的情况下,实现多人聊天,流程:
1. 其中一个channel发送数据,该channel接受到数据
2. 在该channel的读操作中,遍历所有的channel,为每一个channel的attach加上该数据
3. 每一个channel在写操作时,从key的attach中取出数据,分别把该数据写到各自的buffer中
4. 于是每一个channel的界面都能看到其中一个channel发送的数据

客户端:

public class ChatClient {

    private static final String HOST = "127.0.0.1";
    private static int PORT = 9999;
    private static SocketChannel socket;
    private static ChatClient client;

    private static byte[] lock = new byte[1];
    //单例模式管理
    private ChatClient() throws IOException{
        socket = SocketChannel.open();
        socket.connect(new InetSocketAddress(HOST, PORT));
        socket.configureBlocking(false);
    }

    public static ChatClient getIntance(){
        synchronized(lock){
            if(client == null){
                try {
                    client = new ChatClient();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return client;
        }
    }

    public void sendMsg(String msg){
        try {
            socket.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String receiveMsg(){
        String msg = null;
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            StringBuffer buf = new StringBuffer();
            int count = 0;
            //不一定一次就能读满,连续读
            while((count = socket.read(buffer)) > 0){
                buf.append(new String(buffer.array(), 0, count));
            }
            //有数据
            if(buf.length() > 0){
                msg = buf.toString();
                if(buf.toString().equals("close")){
                    //不过不sleep会导致ioException的发生,因为如果这里直接关闭掉通道,在server里,
                    //该channel在read(buffer)时会发生读取异常,通过sleep一段时间,使得服务端那边的channel先关闭,客户端
                    //的channel后关闭,这样就能防止read(buffer)的ioException
                    //但是这是一种笨方法
                    //Thread.sleep(100);
                    //更好的方法是,在readBuffer中捕获异常后,手动进行关闭通道
                    socket.socket().close();
                    socket.close();
                    msg = null;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return msg;
    }
}

界面代码:设置姓名

public class SetNameFrame extends JFrame {
    private static final long serialVersionUID = 1L;
    private static JTextField txtName;
    private static JButton btnOK;
    private static JLabel label;

    public SetNameFrame() {
        this.setLayout(null);
        Toolkit kit = Toolkit.getDefaultToolkit();
        int w = kit.getScreenSize().width;
        int h = kit.getScreenSize().height;
        this.setBounds(w / 2 - 230 / 2, h / 2 - 200 / 2, 230, 200);
        this.setTitle("设置名称");
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setResizable(false);
        txtName = new JTextField(4);
        this.add(txtName);
        txtName.setBounds(10, 10, 100, 25);
        btnOK = new JButton("OK");
        this.add(btnOK);
        btnOK.setBounds(120, 10, 80, 25);
        label = new JLabel("[w:" + w + ",h:" + h + "]");
        this.add(label);
        label.setBounds(10, 40, 200, 100);
        label.setText("<html>在上面的文本框中输入名字<br/>显示器宽度:" + w + "<br/>显示器高度:" + h
                + "</html>");

        btnOK.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String uname = txtName.getText();
                ChatClient service = ChatClient.getIntance();
                ChatFrame chatFrame = new ChatFrame(service, uname);
                chatFrame.show();
                setVisible(false);
            }
        });
    }

    public static void main(String[] args) {
        SetNameFrame setNameFrame = new SetNameFrame();
        setNameFrame.setVisible(true);
    }

}

界面代码:聊天界面

public class ChatFrame {

    private JTextArea readContext = new JTextArea(18, 30);// 显示消息文本框
    private JTextArea writeContext = new JTextArea(6, 30);// 发送消息文本框

    private DefaultListModel modle = new DefaultListModel();// 用户列表模型
    private JList list = new JList(modle);// 用户列表

    private JButton btnSend = new JButton("发送");// 发送消息按钮
    private JButton btnClose = new JButton("关闭");// 关闭聊天窗口按钮

    private JFrame frame = new JFrame("ChatFrame");// 窗体界面

    private String uname;// 用户姓名

    private ChatClient service;// 用于与服务器交互

    private boolean isRun = false;// 是否运行

    public ChatFrame(ChatClient service, String uname) {
        this.isRun = true;
        this.uname = uname;
        this.service = service;
    }

    // 初始化界面控件及事件
    private void init() {
        frame.setLayout(null);
        frame.setTitle(uname + " 聊天窗口");
        frame.setSize(500, 500);
        frame.setLocation(400, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        JScrollPane readScroll = new JScrollPane(readContext);
        readScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        frame.add(readScroll);
        JScrollPane writeScroll = new JScrollPane(writeContext);
        writeScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        frame.add(writeScroll);
        frame.add(list);
        frame.add(btnSend);
        frame.add(btnClose);
        readScroll.setBounds(10, 10, 320, 300);
        readContext.setBounds(0, 0, 320, 300);
        readContext.setEditable(false);
        readContext.setLineWrap(true);// 自动换行
        writeScroll.setBounds(10, 315, 320, 100);
        writeContext.setBounds(0, 0, 320, 100);
        writeContext.setLineWrap(true);// 自动换行
        list.setBounds(340, 10, 140, 445);
        btnSend.setBounds(150, 420, 80, 30);
        btnClose.setBounds(250, 420, 80, 30);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                isRun = false;
                service.sendMsg("exit_" + uname);
                System.exit(0);
            }
        });

        btnSend.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String msg = writeContext.getText().trim();
                if(msg.length() > 0){
                    service.sendMsg(uname + "^" + writeContext.getText());
                }
                writeContext.setText(null);
                writeContext.requestFocus();
            }
        });

        btnClose.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                isRun = false;
                service.sendMsg("exit_" + uname);
                System.exit(0);
            }
        });

        list.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                // JOptionPane.showMessageDialog(null,
                // list.getSelectedValue().toString());
            }
        });

        writeContext.addKeyListener(new KeyListener() {

            @Override
            public void keyTyped(KeyEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void keyReleased(KeyEvent e) {
                if(e.getKeyCode() == KeyEvent.VK_ENTER){
                    String msg = writeContext.getText().trim();
                    if(msg.length() > 0){
                        service.sendMsg(uname + "^" + writeContext.getText());
                    }
                    writeContext.setText(null);
                    writeContext.requestFocus();
                }
            }

            @Override
            public void keyPressed(KeyEvent e) {
                // TODO Auto-generated method stub

            }
        });
    }

    // 此线程类用于轮询读取服务器发送的消息
    private class MsgThread extends Thread {
        @Override
        public void run() {
            while (isRun) {
                String msg = service.receiveMsg();
                if (msg != null) {
                    //如果存在[],这是verctor装的usernames的toString生成的
                    if (msg.indexOf("[") != -1 && msg.lastIndexOf("]") != -1) {
                        msg = msg.substring(1, msg.length() - 1);
                        String[] userNames = msg.split(",");
                        modle.removeAllElements();
                        for (int i = 0; i < userNames.length; i++) {
                            modle.addElement(userNames[i].trim());
                        }
                    } else {//如果是普通的消息
                        String str = readContext.getText() + msg;
                        readContext.setText(str);
                        readContext.selectAll();
                    }
                }
            }
        }
    }

    // 显示界面
    public void show() {
        this.init();
        service.sendMsg("open_" + uname);
        MsgThread msgThread = new MsgThread();
        msgThread.start();
        this.frame.setVisible(true);
    }
}

分析整个程序的流程:

只有一个客户端连接的注释:

[2017-01-23 21:26:14] -> server starting…….
sun.nio.ch.SelectionKeyImpl@99436c6 : 接收
sun.nio.ch.SelectionKeyImpl@3ee5015 : 读
[2017-01-23 21:26:19] -> a –> online
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写

可以看出流程是:服务端接受通道 -> 通道进行读操作 -> 通道进行写操作
1. 当客户端的channel调用connect后,服务端接受到该Channel,于是把该通道的兴趣改为read就绪
2. 客户端connect后,立马写数据”open_”到通道缓冲区中,于是该通道进入了有数据可读状态(即读状态),且该通道的兴趣为read,所以select()的返回值为1,进入了readMsg();
3. readMsg中把每一个key的状态改为了写状态,而此时客户端一直在read数据,要求你服务端要给我数据,于是服务器的channel此时是写状态,且该通道的兴趣为write,所以select()的返回值为1,进入了writeMsg();

有两个个客户端连接的注释:

sun.nio.ch.SelectionKeyImpl@99436c6 : 接收
sun.nio.ch.SelectionKeyImpl@3ee5015 : 读
[2017-01-23 21:26:19] -> a –> online
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写
sun.nio.ch.SelectionKeyImpl@99436c6 : 接收
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 读
[2017-01-23 21:32:30] -> b –> online
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 写
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写

可以看到,@99436c6是ServerSocketChannel,@3ee5015是第一个链接的Channel,@12cb94b7是第二个连接的Channel,可以看见,第二个Channel连接之后

sun.nio.ch.SelectionKeyImpl@3ee5015 : 写
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 读
[2017-01-23 21:32:30] -> b –> online
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 写
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写

两个Channel是交替运行的,说明Selector处理Channle,是轮询处理的

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java NIO Selector详解(含多人聊天室实例) 的相关文章

  • Linux 休眠和挂起

    Linux休眠和挂起 xff08 2008新版 xff09 Linux2 6内核已经有了非常多的变化 xff0c 配置也要相应的改变 The only thing that not changes is Change xff1a xff09
  • 使用dpkg命令安装deb文件包

    1 使用dpkg命令进行安装 sudo dpkg i deb文件名 2 根据经验 xff0c 通常情况下会报依赖关系的错误 xff0c 我们可以使用以下的命令修复安装 sudo apt get install f 3 如果要卸载安装的应用我
  • UITableViewController (列表视图控制器)

    tableview里Cell的小对勾颜色改成别的颜色 xff1f mTableView tintColor 61 UIColor redColor 怎么在不新建一个Cell的情况下调整separaLine的位置 xff1f 一 myTabl
  • Python零代码小游戏 · FreeGames

    Python在日常的办公或者其他领域都有涉及 xff0c 如网站开发 数据分析 爬虫 可视化等等 我们其实还可以选择用Python开发小游戏 xff0c 回忆童年的美好时光 这次并非用PyGame制作的 xff0c 而是一个很有趣的库 Fr
  • Linux 系统使用 git 提交代码-- git 的安装及使用(简明教学指南)

    序 2023 02 09 晚 鉴于本篇文章收藏量比较多 xff0c 那就给大家分享点在实际工作中使用频率最高的工作流命令吧 场景如下 多人共同开发一个项目 xff1a 我叫小明 xff0c 参与了一个名为 chatGPT 的项目 xff0c
  • pyperclip 粘贴失效

    最近在pyautogui自动化输入时 xff0c 发现英文数字都可以 xff0c 中文比较麻烦 xff0c 而且还牵扯到输入法切换问题 xff0c pyautogui typewrite 这是相当于键盘输入 xff0c 如果现在是中文 xf
  • mac 安装问题汇总

    1 问题 xff1a 应用程序添加到登陆项后需要输入密码 xff0c 怎么才能不让它提示输入密码 xff0c 直接运行 xff1f 回答 xff1a 终端内输入 sudo s 输入密码 chown root Applications Gen
  • 如何在Java中加密和解密zip文件?

    在本文中 xff0c 我们来学习如何用Zip4j库创建受密码保护的压缩文件并将其解压 依赖 让我们先把 zip4j 依赖关系添加到我们的 pom xml 文件中 lt dependency gt lt groupId gt net ling
  • Alibaba技术大牛丢给我一份Spring Cloud笔记,在GitHub的热度居然高达81.6k标星,太强了!

    前言 阿里巴巴 xff0c 作为国内互联网公司的Top xff0c 算是业界的标杆 xff0c 有阿里背景的程序员 xff0c 也更具有权威性 作为程序员 xff0c 都清楚阿里对于员工要求有多高 xff0c 技术人员掌握的技术水平更是望尘
  • VNC安装教程

    服务器远程访问工具 xff0c 图形化界面 xff0c VNC安装 需要先在服务器安装sever xff0c 然后在本地安装客户端进行访问 一 首先在服务器安装vncserver span class token comment 以root
  • Debian11安装Docker稳定版

    1 安装依赖包 apt get update amp amp apt get install ca certificates curl gnupg lsb release apt transport https software prope
  • 程序员成神之路,一年挖坑,五年扛旗,十年成神

    自人类社会诞生以来 xff0c 等级就一直存在 xff0c 有人指点江山 xff0c 称之为 大神 有人卸瓦搬砖 xff0c 称之为 小白 在程序员的世界里 xff0c 等级同样森严 特别是在1年 5年 10年时会有大不同 据说到达上面每一
  • [安装fastfds中的nginx执行make命令报错]src/core/ngx_murmurhash.c:37:11: error

    问题 在nginx文件夹里执行make命令报错 src core ngx murmurhash c 37 11 error this statement may fall through Werror 61 implicit fallthr
  • 七牛云融合CDN到底怎么配置?

    人生如戏 xff0c 你得先有故事 老李 由于来年头一个月公司产品接入了一个旅游项目 xff0c 为了保证系统的稳定性 xff0c 必须要对现有架构进行改进以应对大流量的冲击 那么问题来了 xff1f 怎么改 xff1f 首先 xff0c
  • 【二、Arm平台直接安装QT】

    在目标开发平台空间资源不紧张的情况下 xff0c 可直接安装QT常规库 xff0c 省去交叉编译QT源码的时间 span class token function sudo span span class token function ap
  • ubuntu sudo apt-get update时执行失败应该怎么办

    命中 1 http security ubuntu com ubuntu focal security InRelease 命中 2 http us archive ubuntu com ubuntu focal InRelease 命中
  • 不吹不黑,逛GitHub没看过这10个开源项目,绝对血亏

    今天的分享 xff0c 也算是一次简单的复盘 xff0c 我们花了点时间梳理了一下 xff0c 以便诸位在空余时间可以研究学习 下面开始进入正题 xff1a 1 Build Your Own X GitHub Star xff1a 61 3
  • 快速精准的人头检测,代码已开源

    昨天arXiv一篇新上论文 FCHD A fast and accurate head detector xff0c 来自江森自控 xff08 Johnson Controls Inc xff09 的软件工程师Aditya Vora分享了一
  • UDP 用户数据报协议

    UDP 用户数据报协议 引言 UDP是一种保留消息边界 xff08 不合并 xff0c 不拆分 xff09 的简单的面向数据报的传输层协议 使用UDP协议的时候 xff0c 一般来说 xff0c 每个被应用程序请求的UDP输出操作只生产一个
  • 有新家了

    我在CSDN有个小窝了 我是一个JAVA初学者 虽然不是从事IT业 但对计算机有着浓厚的兴趣 希望在CSDN这个大家庭里 能得到朋友们的帮助 当然 我也会力所能及的帮助其它初学者解决一些简单问题的 以后我会把每天学习的内容 来这里发表一下

随机推荐

  • 动态绑定和多态

    class Animal private String name Animal String name this name 61 name public void enjoy System out println 34 叫声 34 clas
  • 离线安装gitlab

    1 下载跟Linux版本相关的 rpm包 地址 xff1a https packages gitlab com gitlab gitlab ce 2 将下载的rpm包上传到机器 3 解压 rpm ivh gitlab ce 15 6 2 c
  • windows子系统 WSL 的根目录位置

    根目录对应位置 我安装的子系统是 Ubuntu18 04 xff0c 根目录对应的位置是 xff1a C Users Administrator AppData Local Packages CanonicalGroupLimited Ub
  • 2020阿里云学生服务器操作步骤!

    前言 年龄在12岁 24岁之间的大陆个人实名认证用户 和 大陆全日制在校大学生在学生认证有效期内 xff0c 满足上述任一条件即可享受优惠价格 xff0c 同一用户只能保有一台学生优惠弹性计算产品 xff0c 一台数据库RDS产品 xff0
  • python用Selenium爬取携程网机票信息

    一 问题说明 1 selenium库是爬虫过程中比较讨巧的一个第三方库 xff0c 它能够跳过js ajax等交互 xff0c 上手比较容易 2 基础代码是根据其他博主参考而来 xff0c 但携程网站不断变化 xff0c 除ID等不变的信息
  • Docker---Docker-compose安装部署Samba服务

    Docker compose安装部署Samba服务 目录 Docker compose安装部署Samba服务一 环境准备二 创建docker compose yaml文件三 测试服务 一 环境准备 1 拉取samba镜像 xff1a doc
  • 金山词霸2005专业版序列号,绝对正确 JQ7M7-XCD38-834H2-TRTWJ-J7BG4

    金山词霸2005专业版序列号 xff0c 绝对正确 JQ7M7 XCD38 834H2 TRTWJ J7BG4
  • Java数据结构——用顺序表编写一个简易通讯录

    Java数据结构 用顺序表编写一个简易通讯录 1 定义线性表的抽象数据类型 xff08 接口 xff09 2 编写顺序表 xff08 类 xff09 3 编写测试程序 xff08 main方法所在的可运行类 xff09 Java数据结构 用
  • sprintf和snprintf用法

    1 sprintf 函数 sprintf 函数原型为 intsprintf char str const char format 其中的格式控制字符串与 printf 的格式控制字符串的作用是一样的 xff0c 表示的是参数的格式 xff0
  • 官网的订阅发布节点

    发布话题 1 usr bin env python 2 license removed for brevity 3 import rospy 4 from std msgs msg import String 5 6 def talker
  • Tkinter教程之Pack篇

    39 39 39 Tkinter教程之Pack篇 39 39 39 Pack为一布局管理器 xff0c 可将它视为一个弹性的容器 39 39 39 1 一个空的widget 39 39 39 不使用pack coding cp936 fro
  • Sqlserver中解析JSON

    参考 xff1a https www red gate com simple talk sql t sql programming consuming json strings in sql server 主要的过程代码单独贴出来 xff1
  • 解决逃离塔科夫0.12.9离线版修改商人可回收所有物品的问题

    复制这里的代码替换 xff0c 不会出现问题 span class token string property property 34 sell category 34 span span class token operator span
  • 手把手教你一套完善且高效的k8s离线部署方案

    作者 xff1a 郝建伟 背景 面对更多项目现场交付 xff0c 偶而会遇到客户环境不具备公网条件 xff0c 完全内网部署 xff0c 这就需要有一套完善且高效的离线部署方案 系统资源 编号主机名称IP资源类型CPU内存磁盘01k8s m
  • 好日子1/6啦啦啦

    今天我直接开搞把作业搞定 xff0c 上题目 题目背景 小明在 A 公司工作 xff0c 小红在 B 公司工作 题目描述 这两个公司的员工有一个特点 xff1a 一个公司的员工都是同性 A 公司有 NN 名员工 xff0c 其中有 PP 对
  • 树莓派4B安装PHP7.3 Nginx MySQL 教程

    非原创 感谢作者 https web security cn rapberry pi 4b install php7 3 nginx mysql 在树莓派4B上搭建Web服务器环境 xff1a PHP7 3 43 Nginx 43 Mari
  • 只需几步,U盘就能变“光驱”

    从07年开始 xff0c 移动存储市场就开始猛刮降价风 到现在 xff0c 大容量U盘的价格更是降到了难以想象的地步 xff0c 连8GB产品的价格都到了300元以内 不过虽然容量上去了 xff0c 一般U盘的功能却并没有太多的改变和延伸
  • 由<meta charset=“UTF-8“>引发的血案--常见字符编码解析

    lt meta charset 61 34 UTF 8 34 gt 是什么意思 xff1f 最近要找实习 xff0c 时间有限 xff0c 以后一定把底层原理写个明白 首先解释一下这句代码的意义 xff1a lt meta charset
  • 从HTTP响应头看各家CDN缓存技术

    https segmentfault com a 1190000006673084 从HTTP响应头看各家CDN缓存技术 由于国内各家电信运营商互联互通的壁垒 xff0c CDN作为互联网用户加速的最后一公里 xff0c 扮演了很重要的角色
  • Java NIO Selector详解(含多人聊天室实例)

    一 Java NIO 的核心组件 Java NIO的核心组件包括 xff1a Channel 通道 xff0c Buffer 缓冲区 xff0c Selector 选择器 xff0c 其中Channel和Buffer比较好理解 简单来说 N