Java NIO之Selector_java nio selecter-程序员宅基地

技术标签: 多路复用  java  nio  JavaIO/NIO流  selector  

一、Java NIO 的核心组件

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

二、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异常。
egister()方法的第二个参数是“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);//绑定

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

selectionKey.attachment();

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

selectionKey.attach(null).

需要注意的是如果附加的对象不再使用,一定要人为清除,因为垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。
一个单独的通道可被注册到多个选择器中,有些时候我们需要通过isRegistered()方法来检查一个通道是否已经被注册到任何一个选择器上。 通常来说,我们并不会这么做。

通过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)停止选择

1.选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。
2.通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回
3.该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
通过close()方法关闭Selector**
4.该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。
调用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,是轮询处理的

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39723544/article/details/80807911

智能推荐

1005 继续(3n+1)猜想(python)_phython 3n+1问题-程序员宅基地

文章浏览阅读122次。卡拉兹(Callatz)猜想已经在1001中给出了描述。在这个题目里,情况稍微有些复杂。当我们验证卡拉兹猜想的时候,为了避免重复计算,可以记录下递推过程中遇到的每一个数。例如对n=3进行验证的时候,我们需要计算 3、5、8、4、2、1,则当我们对n=5、8、4、2 进行验证的时候,就可以直接判定卡拉兹猜想的真伪,而不需要重复计算,因为这 4 个数已经在验证3的时候遇到过了,我们称 5、8、4、2 是被 3“覆盖”的数。我们称一个数列中的某个数n为“关键数”,如果n不能被数列中的其他数字所覆..._phython 3n+1问题

jupyter下的python基本使用和信号处理编程_jupyterlab fft-程序员宅基地

文章浏览阅读1.5k次。jupyter下的python基本使用和信号处理编程简介:jupyter notebook是一种 Web 应用,能让用户将说明文本、数学方程、代码和可视化内容全部组合到一个易于共享的文档中。它可以直接在代码旁写出叙述性文档,而不是另外编写单独的文档。也就是它可以能将代码、文档等这一切集中到一处,让用户一目了然。实验环境:腾讯云服务器centos7一、安装jupyter notebook..._jupyterlab fft

Notepad++ 安装XML Tools插件格式化XML文件-程序员宅基地

文章浏览阅读7.8k次,点赞3次,收藏5次。Notepad++ 安装XML Tools插件格式化XML文件Ritchie_Li2022.02.06 20:37:12字数 183阅读 0编辑文章1. 打开Notepad++ 软件2. 选择插件,选择“插件管理”3. 搜索 XML Tools,找到该插件后,勾选该文件,点击“安装”在Notepad++ 中安装,如果没有成功,可以在多尝试2次,我是第3次成功的,具体原因不知,但有的电脑一次就能安装成功的。4. 安装的进入如下:5.成功之后,插件栏显示6. 格式化XML文件, 单击 "_xml tools

Linux驱动开发———imx6ull的pinctrl子系统源码分析_0x4001b8b0-程序员宅基地

文章浏览阅读1.2k次,点赞3次,收藏21次。目录前言前言 最近在配置pinctrl时,配置了引脚复用寄存器的SION位,配置如下图中的所示,0x4001b8b0中的第30位表示SION位 按照个人理解,imx6ull在设备树中配置的pinctrl节点,后面所带的值应该为配置寄存器的值,而SION位是复用寄存器的第三十位..._0x4001b8b0

TCP三次握手四次挥手及各状态解释_计算机网络中seq是什么意思-程序员宅基地

文章浏览阅读2.1k次,点赞2次,收藏5次。常说的三次握手和四次挥手的意思就是TCP建立连接和断开连接的过程下图为TCP三次握手和四次挥手的过程图状态或符号解释seq(sequence number),序列号,用来标记数据段的顺序,TCP把连接中发送的数据字节都编上一个序号,第一个字节的编号由本地随机产生ack(acknowlege number),确认号,指的是期望接收到下一个字节的编号,因此当前报文段最后一个字节的编号+1即为确认号ACK(acknowledgement),确认,当ACK=1确认号字段才有效,ACK=0确认号无效S_计算机网络中seq是什么意思

基于SpringBoot+Vue+uniapp的企业人事管理系统的详细设计和实现(源码+lw+部署文档+讲解等)-程序员宅基地

文章浏览阅读822次,点赞22次,收藏16次。博主介绍:全网粉丝15W+,CSDN特邀作者、211毕业、高级全栈开发程序员、大厂多年工作经验、码云/掘金/华为云/阿里云/InfoQ/StackOverflow/github等平台优质作者、专注于Java、小程序技术领域和毕业项目实战,以及程序定制化开发、全栈讲解、就业辅导精彩专栏 推荐订阅2023-2024年最值得选的微信小程序毕业设计选题大全:100个热门选题推荐2023-2024年最值得选的Java毕业设计选题大全:500个热门选题推荐Java精品实战案例《500套》

随便推点

Linux那些事儿之我是U盘(37)彼岸花的传说(五)_unsigned soft : 1;-程序员宅基地

文章浏览阅读4.1k次。 燕子去了,有再来的时候;杨柳枯了,有再青的时候;桃花谢了,有再开的时候;老婆离了,有再找的时候,孩子跑了,有回来的时候;煮熟的鸭子飞了,有飞回来的时候.一个函数没讲完就跳走了,有再回来的时候.其实,那些人,那些事,终究不曾远离.于是,她再一次进入我们的视野. 她就是usb_stor_control_thread().唤醒她的是来自queuecommand的up(&(us->sema)_unsigned soft : 1;

usb-serial controller d感叹号_usb serial converter驱动感叹号-程序员宅基地

文章浏览阅读4k次,点赞6次,收藏12次。2. 安装正确的驱动程序:USB-Serial设备通常需要安装驱动程序才能正常工作。这些驱动程序通常可从设备制造商的官方网站下载。请确保下载并安装与您的操作系统兼容的最新驱动程序。解决:1. 确认设备已正确连接:检查USB-Serial设备是否正确插入计算机的USB接口,并确保插头没有松动或损坏。感叹号可能是对于USB-Serial设备发生的问题或错误的表达。这可能是指设备无法被识别、驱动安装问题、通信错误等。_usb serial converter驱动感叹号

Laravel定时任务_laravel 停止schedule:run-程序员宅基地

文章浏览阅读576次。Laravel 定时任务首先:Laravel 制定定时任务很简单的!在app/console 文件夹下面,执行 php artisan make:console TestSchedule,他会生成TestSchedule.php这个文件TestSchedule.php,这个文件写你要定时执行的代码逻辑;class TestSchedule extends Command { //..._laravel 停止schedule:run

LeetCode刷题—树的遍历(前中后序、层次)_leetcod遍历一棵树-程序员宅基地

文章浏览阅读295次。此篇用于梳理二叉树的遍历方式:深度优先遍历(前、中、后序遍历)和广度优先遍历,不仅能快速领会思想和总结规律,还可以顺便刷下这些题:144,二叉树的前序遍历,medium145,二叉树的后序遍历,medium94,二叉树的中序遍历,medium102,二叉树的层序遍历,easy230,二叉搜索树中第k小的元素,medium501,二叉搜索树中的众数,easy530,二叉树搜索树的最小绝对差,easy一、二叉树的遍历有四种方式:1. 前序遍历:根-左-右2. 中序遍历:左-根-右3. 后序_leetcod遍历一棵树

查询冗余数据-程序员宅基地

文章浏览阅读402次。[code="sql"]-- 冗余数据SELECT l.* FROM t_lifeservice_orders l, (SELECT t.* FROM t_lifeservice_orders t WHERE t.orderStatus = 2 GROUP BY t.orderNum, t.orderStatu..._数据冗余查询比联合查询快多少

ACM模式输入输出攻略 | C++篇-程序员宅基地

文章浏览阅读1.4w次,点赞97次,收藏328次。本文内容干货非常非常多,从笔试面试环境的要点,到C++输入输出的具体函数,再到几乎覆盖全部情况的ACM模式写法,最后也给出了链表和二叉树的定义和输入输出。_acm模式

推荐文章

热门文章

相关标签